Skip to content
Sven Nilsen edited this page Sep 21, 2015 · 2 revisions

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.

Rust Guidelines

Properties

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 with get_
  • Methods without a prefix are either read only or take self and returns Self

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 a set_ method
  • foo_mut when there is no set_ method

Default trait vs builder pattern

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.

Rust API guidelines

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.