Public Methods != Public API

I love designing and building APIs. Usually those APIs are in the form of REST web services. A lot of care goes into the interface of a web service because it's how your application is presented to the outside world. But what about the interfaces of your internal code?

Help Your Developers

In the same way that a clean, easy to understand web service can help the world interact with your application, a well designed interface to a Ruby class helps your application's current and future developers work more effectively.

Define Your Public API

Public Methods

The most common mistake in defining the public API for a class is to assume that the public API already exists as the collection of all public methods. Active Record models are the biggest offender.

When you define an Active Record model, you automatically get 537 public class methods and 292 public instance methods for free. Even a vanilla Ruby class inherits 177 public class methods and 101 public instance methods.

Using inherited public methods as part of your public API limits your code's flexibility as those methods may (read: "will") change or disappear in the future.

Public API

If your public API isn't defined by the collection of public methods, how is it defined?

Your public API is the collection of methods you agree to support.

It's a contract between you and future developers that any public API method is well supported and predictable. That means…

Testing and Documentation

Having defined how any public API method should behave, that behavior needs to be covered with tests. Decide what arguments the method will accept, what action it will take and what value it will return. Be sure to document these points, whether in code comments or more formal documentation.

Why Is This Important?

Refactoring

With thorough unit testing and documentation, future developers are empowered to refactor public API methods without fear.

Consider the case where the ORM of an application must be swapped out for another. This can be a terrifying ordeal, but with a well defined public API, each method of each model can simply be rewritten to suit the new ORM. Your tests change minimally and code outside of the models remains completely untouched.

Clarity

The process of defining a good public API forces you to name your methods carefully. If each method is to be supported indefinitely into the future, naming should be descriptive, erring on the side of verbosity.

Testing

Defining a good public API also pushes you to shrink your boundaries. In the same way that a good web service consists of a small collection of clear, simple and powerful endpoints, you should seek to minimize the number of methods in your public API.

Finding the right balance is an exercise in trial and error. Too many and your public API becomes hard to maintain. Too few and your methods become too complex.

With a small set of public API methods, testing becomes easier and faster.

Having thoroughly unit tested and documented your public API methods, you can safely anticipate the behavior of those methods in other areas of your test suite. Your public API methods become the boundary lines of your class. This allows you to stub out those methods when testing elsewhere.

Example

In a Rails application, you might see basic user authentication like this:

class ApplicationController < ActionController::Base
  before_action :authenticate

  attr_accessor :current_user

  private

  def authenticate
    authenticate_or_request_with_http_basic do |email, password|
      if user = User.find_by(email: email)
        self.current_user = user if user.authenticate(password)
      end
    end
  end
end

This is not bad code. It works well and is not terribly difficult to understand. The problem is that Active Record provides the find\_by method and Active Model's secure password library provides the authenticate method. These are implementation details that are subject to change and are out of your control.

Instead, hide your implementation details behind a public API method that you control:

class ApplicationController < ActionController::Base
  before_action :authenticate

  attr_accessor :current_user

  private

  def authenticate
    authenticate_or_request_with_http_basic do |email, password|
      self.current_user = User.authenticate(email, password)
    end
  end
end

class User < ActiveRecord::Base
  has_secure_password

  # Find a user by given email and authenticate against given password.
  # Return the user if found and authenticated.
  # Return nil if the user is not found.
  # Return nil if the user is found but cannot be authenticated.
  def self.authenticate(email, password)
    user = find_by(email: email)
    user && user.authenticate(password) ? user : nil
  end
end

Of course, your new User.authenticate method will be covered by unit tests (not pictured) so if you ever decide to switch to Mongoid or different crypto in the future, your controller and its tests are still good to go!

steve@collectiveidea.com

Comments

  1. February 19, 2014 at 14:03 PM

    Ok, but how do you enforce a stable API, while keeping the code, documentation, and tests in sync?

  2. February 19, 2014 at 14:25 PM

    Benjamin: Stability is important in defining an internal API but it doesn’t have the same weight as stability in your external API (e.g. web service). Real world web service consumers aren’t as forgiving of external API changes as your own developers will be of internal changes. And while you can version a web service, you don’t have the same luxury internally.

    Long story short: your internal APIs will change, through refactoring or changes in business logic. Methods may drop from your public API. The key is to pay attention during those changes and to continue defining clear boundaries.

    I hope that answers your question!

  3. February 19, 2014 at 21:03 PM

    Not exactly. I was actually referring to what you are calling the external API in my question.  Here’s my use-case that’s making me think about it:

    I maintain metric_fu and and implementing a SimpleCov formatter. Currently, the coverage metric is named ‘RCov’, but as part of my work, I renamed the metric and all its related files ‘TestCoverage’, except for a few RCov-specific cases.  The tests passed and I was ready to merge, when I remembered that the current documentation is to refer to the coverage metric as ‘:rcov’.  If I hadn’t remembered that, I would have introduced a breaking change.  Since then, I’ve been trying to think of how I can either write Usage documentation that I can run as tests, or export documentation from running a test, or perhaps just ensure that example code returns the same example results.

    I consider the documentation a contract with the end-user that I don’t want to break. But if I don’t regularly read it, I might break that contract.  I’ve looked around at various tools, yard, yardstick, fdoc, cane, rcodetools/xmpfilter, specdown, etc.