A few years ago, I read Sandi Metz’s blog post about affordances and it stuck with me. I started minding how the APIs I write can be misused; I started noticing the surface I leave for the users of my APIs to make errors. Soon after I started noticing how the APIs we use every day are full of affordance for error.
In this first of three posts, I want to show a few examples of how common APIs (mostly in Ruby and Rails) can be accidentally misused. Maybe this will spark a discussion, or will incite people to share other issues they’ve seen. The second post will explore solutions to the problems from this post. In the final post of this series, I will show how I approach API design to minimize the affordance for errors while offering the best ergonomics I can.
You may be wondering why this matters, why can’t just write better code, without mistake. After all, we’re smart people, we should be able to learn these tools and to not make mistakes… If you are the kind of developer that can write bug-free code, this blog post is even more so for you, as your coworkers cannot. Developers are humans. We get tired, we forget, our attention lapses, we’re juggling umpteen other things in our heads. For many reasons, we end up making mistakes. It is then our shared responsibility to make your APIs human-friendly.
Moreover, good APIs minimize the amount of mistakes we can make, they remove the pain and frustration of using them. They lower the cost of entry. They don’t require you to understand how things are implemented “on the metal”. Providing good APIs with minimal affordance for errors is about making the life of your fellow developers just a little better, making them just a little more productive, easing their cognitive load just a little.
Good APIs will naturally guide developers towards the best ways to use them, in the same subtle yet important way the bumps on the
J keys guide your hands towards the good hand placement.
My hope is that after reading this post, you too will start taking notice of what errors you afford your users, and that we can all use this as a starting point to write better, human-friendlier APIs.
Jump right ahead:
- ActiveRecord’s Errors API
- N + 1
- Illegal States
- Ruby’s Visibility Modifiers
- Ignored Values
- Missing Coupling
- Extending Self
I want to start this off with an easy, approachable example: ActiveRecord’s
errors API, which requires a specific incantation.
For those unfamiliar with Rails, in the previous example, we
- We create a
Userclass which specifies that the
nameattribute must be set, otherwise it’s not valid (#1)
- We then instantiate a
User, without giving it a
name(#2); we can then expect it to be invalid
- However, we can see that it contains no errors (#3)
- This is because ActiveRecord requires the programmer to use
valid?(#4) before calling
errors, which mutates the
APIs like this can often be exposed in ways that completely suppress the problems, so much so that I’ve always been stricken that Rails kept choosing not to do anything (reminder: exploring solutions will be the topic of the second post).
Some APIs, like ActiveRecord, allow mistakenly loading data serially instead of in a batch manner, without guiding the user towards the more performant approach.
Because the code that loaded the
investor object did not
fund, every iteration of the loop will issue a query to load one
If you have used Rails with ActiveRecord for more than a few days, the probability that you’ve encountered or written code with this mistake are high. This problem is so common that a gem (Bullet) was written to help developers detect when they cause it.
If I were to choose, this would be the most important one.
Make illegal states unrepresentable
Attributed to a 2011 post by Yaron Minksy (which I became aware of only much later), this quote completely changed the way I approach software design. This practice eliminates so many possible mistakes that it’s hard to even explain the implications.
If no illegal state can be represented, we never have to worry about the validity of objects.
More often than not, however, we write code that allows representing invalid state. ActiveRecord initialization is a good example, but there are so many others. All APIs that use
Hashes to represent state immediately allow illegal state. There are many, many variations of APIs offering this kind of surface for errors, I encourage you to keep your eyes peeled and take notice.
One example, I’ve recently used an APIs for inventory management using a number of booleans to represent possible states, but the number of allowed states is less than the booleans combined values allow. Inventory items can be configured with the boolean values
inventory_is_managed (the inventory counter can be set and is decremented after a sale), and
allow_negative (the item can continue selling once after it the counter reached zero).
The problem is that this API has 4 configurations, but only 3 are valid:
(inventory_is_managed: true, allow_negative: true)the inventory is managed, but can go negative
(inventory_is_managed: true, allow_negative: false)the inventory is managed, and cannot go below zero
(inventory_is_managed: false, allow_negative: _)the inventory is not managed; whether or not it can go below zero is meaningless
Users of this API should not be able to set any value for
Ruby code is plagued with primitive obsessions. We represent emails using
String, and “complex” data using
Hash. Neither of these classes allow preserving the invariants of our system (ex: the email matches a regular expression). As a result, most codebases I’ve seen build defenses; they validate the state more than once.
There’s an entire category of errors stemming from APIs allowing you to do the wrong thing, while seemingly doing the right thing.
Visibility modifiers (
public) are prime examples; they are the source of many mistakes, by newcomers’s mismatched expectations and veterans’s lapse of attention. To say nothing of the fact that
protected means something different than in other languages, many devs make mistakes when it comes to singleton methods, often called “class methods”. Here’s an example:
Many devs would expect the
Foo.bar method to be private to
Foo–perhaps because many call it a class method–, however it is not. The reason is that
private is a method on
Foo, telling it to mark the methods defined after as
private. It does not affect
Foo's singleton class. To learn more about the relation between classes and their singleton classes, refer to The Ruby Object Model.
That it is such a common error pains me the utmost, because it is so unnecessary. Approximately 100% of other languages do not have this problem, by having visibility modifiers at the method definition level. In fact, it even works in Ruby, but I digress.
Some APIs will use values that are easy to ignore, once again leading to errors.
While I don’t want to seem like I pick on ActiveRecord, it contains examples within reach.
In this example, the return value of
Boolean indicating whether the persistance was successful or not–is not checked. The error case, when
save fails, is not handled. ActiveRecord doesn’t help nor guide towards handling it, either.
If you think your code is impervious to this problem, I suggest you look at your test suite. If you’re using
save and not
save! to setup objects, and unless you have very high code hygiene, chances that at least one started failing since you wrote it.
APIs sometimes need things to be coupled, but don’t express it. For example, in Ruby, any object can be a
Hash key by defining two methods:
eql?. However, this is not encoded at runtime. To make things worse, the
Object class defines an implementation based on object equality.
As a result, it’s easy for developers to redefine either of the two methods but not both. This can easily lead to situations where the objects seem to be valid hash keys, until there two items end up in the same bucket, or, conversely, when two items should fold together, but don’t.
The usage of
extend(self) in module is probably one of my biggest pet peeves. It’s used to add methods to the singleton class of a module, effectively allowing the module to receive the methods it defines. Ex:
In the previous example, the module
Foo defines the method
foo (#1) which is available on
Foo (#2) due to the
extend(Foo). Not only does this idiom produce a complicated object, the fact that it’s an idiom affords users some errors.
I think people fail to consider the implications of these modules. Should
Foo be used as a singleton, or should it be
include'd, as modules are? In the previous example, it doesn’t matter. When you start using instance variables and state, it does.
In Ruby, as we know, parentheses are optional, unless they aren’t. The usage of
super is such an example, where
super (without parentheses) is semantically different to
super() (with parentheses).
This example shows the semantic difference between both versions.
The reason is that
super (without parentheses) sends the arguments verbatim, whereas
super() (with parentheses) sends no argument.
I hope this post has opened your eyes on the amount of affordance for errors offered by our languages and APIs. I hope you’ll start noticing them in the code you’re writing, and you’ll strive to remove as many as possible.
My list contained many other affordances that didn’t make the cut for this post. To name a few:
HashWithIndifferentAccessblurring the line between
validates_uniqueness_ofis racy by default
assert(obj, message)is easily mistaken with
config(default_value: :false)(using a symbol where a boolean was expected)
ActiveRecordscopes behaving like
Arraybut not quite
- Using of structureless markup languages (YAML)
If you wish to contribute with more examples, please share them in the comments.