|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Affordance for Errors, part 1 |
| 4 | +date: 2020-01-29 23:00:00 +0400 |
| 5 | +description: In this first post of three, I highlight a few examples of affordance for errors in common APIs. |
| 6 | +categories: |
| 7 | +- Ruby |
| 8 | +--- |
| 9 | + |
| 10 | +A few years ago, I read [Sandi Metz's blog post about affordances](https://www.sandimetz.com/blog/2018/21/what-does-oo-afford) 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. |
| 11 | + |
| 12 | +In this first of three post, 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. |
| 13 | + |
| 14 | +## Why Bother? |
| 15 | + |
| 16 | +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 as 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. |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +Good APIs will naturally guide developers towards the best ways to use them, in the same subtle yet important way the bumps on the `F` and `J` keys guide your hands towards the good hand placement. |
| 21 | + |
| 22 | +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. |
| 23 | + |
| 24 | +## Examples of Surfaces for Errors |
| 25 | + |
| 26 | +Jump right ahead: |
| 27 | +- [ActiveRecord's Errors API](#activerecords-errors-api) |
| 28 | +- [N + 1](#n--1) |
| 29 | +- [Illegal States](#illegal-states) |
| 30 | +- [Ruby’s Visibility Modifiers](#rubys-visibility-modifiers) |
| 31 | +- [Ignored Values](#ignored-values) |
| 32 | +- [Missing Coupling](#missing-coupling) |
| 33 | +- [Extending Self](#extending-self) |
| 34 | +- [`super` vs `super()`](#super-vs-super) |
| 35 | + |
| 36 | +### ActiveRecord's Errors API |
| 37 | + |
| 38 | +I want to start this off witn an easy, approachable example: ActiveRecord's `errors` API, which requires a specific incantation. |
| 39 | + |
| 40 | +```ruby |
| 41 | +class User < ApplicationRecord |
| 42 | + validates(:name, presence: true) # 1 |
| 43 | +end |
| 44 | + |
| 45 | +User.new # 2 |
| 46 | +user.errors.any? # 3 |
| 47 | +# => false |
| 48 | + |
| 49 | +user.valid? # 4 |
| 50 | +# => false |
| 51 | + |
| 52 | +user.errors.any? # 5 |
| 53 | +# => true |
| 54 | +``` |
| 55 | + |
| 56 | +For those unfamiliar with Rails, in the previous example, we |
| 57 | +- We create a `User` class which specifies that the `name` attribute must be set, otherwise it's not valid (#1) |
| 58 | +- We then instantiate a `User`, without giving it a `name` (#2); we can then expect it to be invalid |
| 59 | +- However, we can see that it contains no errors (#3) |
| 60 | +- This is because ActiveRecord requires the programmer to use `valid?` (#4) before calling `errors`, which mutates the `User` to set `errors` (#5) |
| 61 | + |
| 62 | +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). |
| 63 | + |
| 64 | +### N + 1 |
| 65 | + |
| 66 | +Some APIs, like ActiveRecord, allow mistakenly loading data serially instead of in a batch manner, without guiding the user towards the more performant approach. |
| 67 | + |
| 68 | +```ruby |
| 69 | +investor.accounts.each do |account| |
| 70 | + puts account.fund.name |
| 71 | +end |
| 72 | + |
| 73 | +# Fund Load (1.2ms) SELECT "funds".* FROM "funds" WHERE "funds"."id" = $1 LIMIT $2 [["id", 6], ["LIMIT", 1]] |
| 74 | +# Fund Load (1.4ms) SELECT "funds".* FROM "funds" WHERE "funds"."id" = $1 LIMIT $2 [["id", 7], ["LIMIT", 1]] |
| 75 | +# ... and so on for each account |
| 76 | +``` |
| 77 | + |
| 78 | +Because the code that loaded the `investor` object did not `preload` the `fund`, every iteration of the loop will issue a query to load one `fund`. |
| 79 | + |
| 80 | +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)](https://github.com/flyerhzm/bullet) was written to help developers detect when they cause it. |
| 81 | + |
| 82 | +### Illegal States |
| 83 | + |
| 84 | +If I were to choose, this would be the most important one. |
| 85 | + |
| 86 | +> Make illegal states unrepresentable |
| 87 | +
|
| 88 | +Attributed to [a 2011 post by Yaron Minksy](https://blog.janestreet.com/effective-ml-revisited/) (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. |
| 89 | + |
| 90 | +__If no illegal state can be represented, we never have to worry about the validity of objects__. |
| 91 | + |
| 92 | +More often than not, however, we write code that allows represending invalid state. ActiveRecord initialization is a good example, but there are so many others. All APIs that use `Hash`es 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. |
| 93 | + |
| 94 | +#### Incomplete Booleans |
| 95 | + |
| 96 | +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). |
| 97 | + |
| 98 | +The problem is that this API has 4 configurations, but only 3 are valid: |
| 99 | +- `(inventory_is_managed: true, allow_negative: true)` the inventory is managed, but can go negative |
| 100 | +- `(inventory_is_managed: true, allow_negative: false)` the inventory is managed, and cannot go below zero |
| 101 | +- `(inventory_is_managed: false, allow_negative: _)` the inventory is not managed; whether or not it can go below zero is meaningless |
| 102 | + |
| 103 | +Users of this API should not be able to set any value for `allow_negative` if `inventory_is_managed` is `false`. |
| 104 | + |
| 105 | +#### Email is a String, Struct is a Hash |
| 106 | + |
| 107 | +Ruby code is [plagued with primitive obsessions](https://refactoring.guru/smells/primitive-obsession). 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. |
| 108 | + |
| 109 | +### Ruby's Visibility Modifiers |
| 110 | + |
| 111 | +Theres an entire category of errors stemming from APIs allowing you to do the wrong thing, while seemingly doing the right thing. |
| 112 | + |
| 113 | +Visibility modifiers (`private`, `protected`, `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: |
| 114 | + |
| 115 | +```ruby |
| 116 | +class Foo |
| 117 | + private |
| 118 | + |
| 119 | + def self.bar |
| 120 | + :bar |
| 121 | + end |
| 122 | +end |
| 123 | +``` |
| 124 | + |
| 125 | +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]({% post_url 2019-02-01-the-ruby-object-model %}). |
| 126 | + |
| 127 | +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. |
| 128 | + |
| 129 | +## Ignored Values |
| 130 | + |
| 131 | +Some APIs will use values that are easy to ignore, once again leading to errors. |
| 132 | + |
| 133 | +While I don't want to seem like I pick on ActiveRecord, it contains examples within reach. |
| 134 | + |
| 135 | +```ruby |
| 136 | +def update |
| 137 | + user.update(user_update_params) |
| 138 | + user.save |
| 139 | + |
| 140 | + respond_with(user) |
| 141 | +end |
| 142 | +``` |
| 143 | + |
| 144 | +In this example, the return value of `user.save`–a `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. |
| 145 | + |
| 146 | +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. |
| 147 | + |
| 148 | +### Missing Coupling |
| 149 | + |
| 150 | +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: `hash` and `eql?`. However, this is not encoded at runtime. To make things worse, the `Object` class defines an implementation based on object equality. |
| 151 | + |
| 152 | +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. |
| 153 | + |
| 154 | +### Extending Self |
| 155 | + |
| 156 | +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: |
| 157 | + |
| 158 | +```ruby |
| 159 | +module Foo |
| 160 | + extend(self) |
| 161 | + |
| 162 | + def foo # 1 |
| 163 | + :foo |
| 164 | + end |
| 165 | +end |
| 166 | + |
| 167 | +Foo.foo # 2 |
| 168 | +# => :foo |
| 169 | +``` |
| 170 | + |
| 171 | +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. |
| 172 | + |
| 173 | +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. |
| 174 | + |
| 175 | +### `super` vs `super()` |
| 176 | + |
| 177 | +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). |
| 178 | + |
| 179 | +```ruby |
| 180 | +class Parent |
| 181 | +end |
| 182 | + |
| 183 | +class WithParentheses < Parent |
| 184 | + def initialize(var) |
| 185 | + @var = var |
| 186 | + super() |
| 187 | + end |
| 188 | +end |
| 189 | + |
| 190 | +class WithoutParentheses < Parent |
| 191 | + def initialize(var) |
| 192 | + @var = var |
| 193 | + super |
| 194 | + end |
| 195 | +end |
| 196 | + |
| 197 | +WithParentheses.new(1) # 1 |
| 198 | +# => #<WithParentheses:0x000...> |
| 199 | + |
| 200 | +WithoutParentheses.new(1) # 2 |
| 201 | +# => ArgumentError: wrong number of arguments (given 1, expected 0) |
| 202 | +``` |
| 203 | + |
| 204 | +This example shows the semantic difference between both versions. |
| 205 | + |
| 206 | +The reason is that `super` (without parentheses) sends the arguments verbatim, whereas `super()` (with parentheses) sends no argument. |
| 207 | + |
| 208 | +## Conclusion |
| 209 | + |
| 210 | +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. |
| 211 | + |
| 212 | +### Post Scriptum |
| 213 | + |
| 214 | +My list contained many other affordances that didn't make the cut for this post. To name a few: |
| 215 | + |
| 216 | +- `HashWithIndifferentAccess` blurring the line between `String` and `Symbol` |
| 217 | +- `validates_uniqueness_of` is racy by default |
| 218 | +- `assert(obj, message)` is easily mistaken with `assert_equal(a, b)` |
| 219 | +- `config(default_value: :false)` (using a symbol where a boolean was expected) |
| 220 | +- `ActiveRecord` scopes behaving like `Array` but not quite |
| 221 | +- Using of structureless markup languages (YAML) |
| 222 | + |
| 223 | +If you wish to contribute with more examples, please share them in the comments. |
| 224 | + |
| 225 | +[Comment or Like](https://github.com/gmalette/gmalette.github.io/pull/7) |
0 commit comments