Third-party code boundaries

09 Oct 2014

Let's say we have a Rails application and we want to interact with Twitter. There are probably a few good open source gems already that could help us out with this task.

class HomeController < ApplicationController
  def index
    twitter_client = SomeTwitterClient.connect(ENV['TWITTER_API_KEY'])
    feed = twitter_client.feeds.get("some_user")
    @tweets = feed.tweets
  end
end

Then we just render @tweets in our view. Although our code is not too bad, we're probably going to run into unwanted issues if we repeat this throughout our application. Consider what happens if we need to update the gem for security fixes, and we discover it also changed the way it connects and interacts with Twitter.

# Notice we call `tweets` directly from the client now
client = Twitter::Client.connect(api_key)
tweets = client.tweets("some_user")

Even a minor change like this will force us to update every piece of code that referenced the old API. Instead, we can write our own application specific interface, with the methods we wish the third-party API had.

module TwitterClient
  def tweets(username)
    client.tweets(username)
  end

  private

  def client
    @client ||= SomeTwitterClient.connect(ENV['TWITTER_API_KEY'])
  end
end

Then in our controller:

def index
  @tweets = TwitterClient.tweets("some_user")
end

Now we isolated the code, so if the third-party library changes we only have to update it in a single place. This is known as the adapter design pattern and allows two otherwise incompatible interfaces to work together.