Beyond SOLID: Affordance

There’s an increasing awareness among software developers of the SOLID principles – that is the Single responsibility, Open/closed, Lyskov substitutability, Interface segregation and Dependency inversion principles. Beyond these another principle that is valuable in designing software components is that of affordance, which draws from accessibility and ergonomics contexts. The concept could be simply expressed “it should be intuitive and obvious how to use something correctly”. A good example from the real world would be offering flat plates on doors that open by pushing, and handles on doors that are to be pulled. It is quite common if a door has a handle but should be pushed to see people first try to pull, and then fall back to pushing, which is an indication that the intent of the designer is ambiguous or even misleading.

In software, the same design principle applies. The design of the contract an object exposes should tell another developer using it as clearly as possible what the correct usage is. It is one of these things where if this is done right, the result looks quite unremarkable – and that’s how you know it is right; by contrast, bad designs are obvious by the friction they cause. There is a strong link to the principle of encapsulation which is where internal concerns, usually data specifically, are hidden.

One example that comes up quite often in a .Net context is where a method returns List<T>. The internal implementation may be a list, but it is probably not appropriate to declare the return type as such. Lists offer methods like add, remove, clear and so on – do you as a designer of the contract intend the consumer to use these methods? What behavior should they expect if they do use them? Are they mutating the original data source, or their own private cloned copy? If you don’t intend these methods to be used, but are simply offering a read only list, a more appropriate return type could be IEnumerable<T>. The general rule is to return the simplest and most restrictive set of operations, all of which should be semantically meaningful to a consumer.

Another example is where objects have a range of methods that need to be called in a specific order – Initialise is the one commonly seen – with the usual pattern being to throw if the methods are called in any other order. Feedback that could have been given at compile time is being deferred to runtime, and the behavior of the object is harder to understand than it has to be. As one alternative, this is an example of somewhere that the Factory pattern could be appropriately used. In order to use the object that requires some kind of setup, the consumer uses a factory, which orchestrates the initialization and configuration, and always returns an object (of a different type) that is ready to use.

Showing empathy for the consumers of objects you design and considering affordance – along with the other principles of object design – can reduce or even remove a lot of errors downstream, and will make your and your teams’ overall experience of working on the codebase more productive and enjoyable!