-
-
Notifications
You must be signed in to change notification settings - Fork 234
API design
What makes a robust library?
In Rust ecosystems, it is common to prefer small libraries that focuses on a particular problem.
- Narrow down the scope, because a library is not the center of the universe.
- Everything comes at a cost: Lines of code, structs, traits, everything!
- Use traits such that the implementation becomes reusable, e.g. a backend or a widget.
- Global functions are straight forward and little can go wrong with them.
- Too complex libraries often lead to dependency explosion of features that the users don't even need.
- Build on top of shared abstractions, e.g. backend agnostic design.
- Decouple dependencies, e.g. separate graphics and window.
- There should be a way of working around the library if necessary.
- Look at how people use the library.
When writing methods for getting and setting options, you can have &mut self
, self
, &self
etc. which can lead to inconsistent usage across libraries. The conventions here are used in the piston core and piston-graphics, and I suggest them as guidelines for all Piston libraries.
- Whenever a method starts with
set_
it should take&mut self
- Whenever there is a
set_
method and you need a get, it should start withget_
- Methods without a prefix are either read only or take
self
and returnsSelf
This means that builder patterns doesn't need set_
prefixes, for example:
let events = events.max_fps(60).ups(120);
The method without prefix never gets in the way for the read only version, because when a property is read only you do not need a builder version. It also makes it possible to add either a read only or builder method later without breaking the API.
For mutable references to the interior of an object:
-
get_foo_mut
when there is aset_
method -
foo_mut
when there is noset_
method
Some APIs use the Default
trait and public struct members instead of the builder pattern.
let a = Foo { field1: Some(val), ..Default::default() };
vs
let a = Foo::new().field1(val); // alternative `maybe_field1(Some(val))`
The first version has the benefit of being more explicit, but the builder pattern is easier for refactoring methods into a trait. It is possible to use both approaches within the same library.
The Rust API guidelines distinguishes between Builder patterns and ordinary objects, so the get_
prefix is removed for ordinary objects. See https://aturon.github.io/style/naming/README.html#getter/setter-methods-[rfc-344].
In several Piston libraries the distinction between builders and ordinary objects is blurry, so we use get_
to avoid breaking the API.