In Progress
Unit 1, Lesson 21
In Progress

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_pickle 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