API Design for productivity
I’ve spent so much time in the Ruby community that sometimes I take for granted the community values that make the Ruby world unique and special. Sometimes it takes stepping outside that context to crystalize the unique perspective that Ruby practitioners bring to the developer world.
One of those unique strengths is the Ruby emphasis on *Developer Experience*. Aja Hammerly, our guest chef for today’s episode, is in a perfect position to observe and highlight this distinction. Aja is a longtime Rubyist who works as a Developer Advocate at Google. In this episode, she throws a spotlight on some general best practices for API design, as well as some distinctly Ruby-ish idioms for making APIs approachable and comfortable to work in. Enjoy!
Video transcript & code
Designing Good Developer Interfaces
Script
The Ruby community has taught me a lot over the years but one of the biggest lessons I've learned is about how important developer experience is. Ruby has developer happiness as a primary goal and it is a joy to use.
You likely have already thought about the UI or UX of your product. I'd like you to think about the UX of the Developer Experience.
WTF APIs & Context Setting
Let me give you a concrete example...
I have a simple API for tracking the pickles and jams I've made. It tracks the date the item was made and the quantity. Nothing amazing here. But the developer experience for using the API could be a lot better.
class PickleTracker
def initialize
@inventory = Hash.new(0)
@dates = Hash.new { |h, k| h[k] = [] }
end
def add_pickle(type, quantity, date)
@inventory[type] += quantity
@dates[inventory] << date end def eatPickle(quantity, type) @inventory[type] -= quantity oldest = @dates[type].min @dates[type].delete(oldest) end def check_inventory(item) @inventory[item] > 0
end
def expires_soon(n)
expiring_soon = []
target_date = Date.today + n
@dates.each do |item, dates|
dates.each do |d|
expiration = d.next_year
if expiration < target_date
expiring_soon << item
break
end
end
end
expiring_soon
end
end
Good naming and Aliases
One of the first things I notice when I look at this API is that the naming is all over the place.
I have an add_pickle
method.
I have an eatPickle
method that oddly uses camel case instead of Ruby convention of snake case.
There's a check_inventory
that returns a true or false value.
And my expires_soon
method takes an argument and at first glance I have no idea what it is for.
Let's see what we can clean up.
The easiest thing to do is to make the eat pickle
method use snake case.
It also isn't obvious to me that eat
is the opposite of add
in this case. Usually the opposite of add is remove. People who are using my API are probably going to be looking for a remove
method so let's rename eat_pickle
to remove_pickle
.
But I liked the name eat_pickle
and in some contexts that actually makes more sense. So I'm going to add an alias.
One of the quirks of Ruby is how often aliases are used through the standard library.
This is easiest to see in the Enumerable
module.
Enumerable#reduce
and Enumerable#inject
are aliases for the same functionality.
The same is true of map
and collect
.
irb(main):001:0> [1, 2, 3].reduce(:+)
=> 6
irb(main):002:0> [1, 2, 3].inject(:+)
=> 6
irb(main):003:0> [1, 2, 3].map { |x| x**2 }
=> [1, 4, 9]
irb(main):004:0> [1, 2, 3].collect { |x| x**2 }
=> [1, 4, 9]
Ruby supports using either name because depending on your background one may be more familiar, make more sense, or just be more comfortable to any individual developer.
Consistency
Another thing that stands out to me in the pickle API is that it isn't particularly consistent with itself or with other libraries written in Ruby.
For example, the check inventory method returns either a true or false. By convention, in Ruby, methods that return true or false end in a question mark. So the check inventory method should end in a question mark.
I learned from folks at Seattle.rb to pronounce the question mark at the end of a method name in ruby as Eh? to indicate it is a question. Check inventory eh? doesn't make sense so it probably needs a better name. The actual question being asked is "is this thing in inventory" or "do we have blah". So I'll rename it to "do we have?".
The other big inconsistency with the current API is that the method arguments are in different orders for similar methods.
The add_pickle
method has the type, then the quantity, then the date.
The remove_pickle
method has the quantity first then the type.
This is the type of small thing that can be really frustrating to developers. It isn't the way we expect APIs to work and you can end up with confusing and subtle bugs from having inconsistently ordered arguments. So I'll fix that to make the add and remove methods as similar as possible.
This is already a lot better but there are still a couple things that will make this developer interface even better.
Logical Defaults
Adding logical defaults wherever possible can make things significantly easier for your users. In my remove_pickl
e method I can default quantities to one since I probably only eat one jar of pickles at a time.
I can also default the date on the add_pickle
method today since most likely I'm adding pickle to the inventory on the same day I bought or made them.
And where I can't use logical defaults I can ensure that the parameter names describes how the parameter is used.
require 'date'
class PickleTracker
def initialize
@inventory = Hash.new(0)
@dates = Hash.new { |h, k| h[k] = [] }
end
def add_pickle(type, quantity, date = Date.today)
@inventory[type] += quantity
@dates[type] << date end def remove_pickle(type, quantity = 1) @inventory[type] -= quantity oldest = @dates[type].min @dates[type].delete(oldest) end alias_method :eat_pickle, :remove_pickle def do_we_have?(item) @inventory[item] > 0
end
def expires_soon(num_days)
expiring_soon = []
target_date = Date.today + num_days
@dates.each do |item, dates|
dates.each do |d|
expiration = d.next_year
if expiration < target_date
expiring_soon << item
break
end
end
end
expiring_soon
end
end
Conclusion
The changes needed to fix my pickle API were relatively small. Renaming methods, adding aliases, ensuring consistent argument names and argument order, and adding logical defaults. But these small changes make the API much more usable.
You may have heard of the idea of minimizing surprise. That is another way of saying "things should work the way I expect them to". If things work they way we expect them to we're more productive and generally more productive programmers are happier programmers. So, when you are designing an API or a developer surface for an application. Think about minimizing surprise and having things work the way programmers expect. You'll probably find that folks are happier and that the code they write is less buggy when you make the experience predictable and pleasant for them.
Responses