Wrap your dependencies

We stand on the shoulders of giants. We use libraries and tools that other people created to speed up our work. That’s a good thing! But before you take a new toy and start using it in your code, consider wrapping it. Here are some ways you can do that:

Wrapping behavior into a function

Say we are using HtmlSanitizer to filter user input in our front end:

safeHtml = HtmlSanitizer.SanitizeHtml(someHtmlString);
// do something with the sanitized html

If we have to use that in multiple places in our code, we might want to wrap it in a function, but some people just won’t bother. After all, it’s just a simple function call, right?

What if we now also need to filter input in our JS back end? HtmlSanitizer is a front-end-only library, so let’s replace it with sanitize-html to keep fewer dependencies and just one API to learn. Bummer! Now we have to change every place we use HtmlSanitizer. If, instead, we had wrapped it in a function, that would be an easy change:

- import HtmlSanitizer from 'html-sanitizer';
+ import sanitizeHtml from 'sanitize-html';

function sanitizeHtml(html) {
-  return HtmlSanitizer.SanitizeHtml(html);
+  return sanitizeHtml(html)
}

Wrapping behavior into a module or class

If your needs are more complex than a single function, you might need to wrap behavior in a module or class. Let’s say we’re using an HTTP client in our Ruby code:

response = HttpClient.get('https://api.example.com/v1/resource')

And, again, let’s say we’re using this in multiple places and now we want to change the HTTP client library. Even if the APIs were identical, the libraries likely have different response objects and error handling.

If the previous library raised exceptions on errors, for example, but the new one returns a response object with a status code, we’ll have to change every place that uses it. Even if the new one also raises exceptions, we’d have to change which exceptions are rescued. Wrapping it in a module or class will make this easy to change in a single place:

module MyHttpClient
  extend self

  Error = Class.new(StandardError)
  Response = Data.define(:status, :body, :headers)

  def get(url)
    OtherHttpClient
      .get(url)
      .then { Response.new(_1.status, _1.body, _1.headers) }
  rescue OtherHttpClient::Error => e
    raise Error, e.message # an error class that we control
  end

  # other methods...
end

Wrapping behavior into a Stimulus controller

This one is quite common in Rails apps. Maybe it’s a jQuery plugin, maybe it’s a JS library to make select fields searchable. Our first instinct might be just to follow the docs and add some JS directly to the app:

import choices from 'choices.js';

document.addEventListener('DOMContentLoaded', () => {
  new choices('[data-choices]');
});

My advice here is to wrap that behavior into a Stimulus controller. It might take a few more lines of code in the beginning, but that will pay off in the long run. Using Stimulus’ Values API we can easily pass options and have default values for them.

// In app/javascript/controllers/select_box_controller.js
import { Controller } from 'stimulus';

export default class extends Controller {
  static values = {
    settings: { type: Object, default: { /* default options */ } }
  }

  connect() {
    new choices(this.element, this.settingsValue);
  }
}

And in the HTML:

<%=
  form.select(
    :field,
    options,
    multiple: true,
    data: { controller: 'select-box', select_box_settings_value: settings }
  )
%>

Ok, but why?

The previous examples bring several benefits. To name a few:

  • They make you define and agree on a common interface:
    • What are the inputs? Are you using keyword arguments or positional a hash of options?
    • What are the outputs? Does it raise exceptions or return a result object?
  • They are easier to test: You own the implementation of the wrapper now. You can mock or stub it and be confident that it will behave as expected.
  • They facilitate swapping the underlying implementation: This is a very common scenario. Today Choices.js might be the best library for your needs, but who knows what can happen in the future? Maybe it will be abandoned, have critical security issues, or you’ll just need different features that aren’t available. If you have a wrapper, you can just replace the implementation and keep the same interface (or at least minimize the changes). Think about it as private and public methods. You have an external facing API that you maintain, but the internal implementation isn’t exposed to the world.

When you control the interface of your dependencies via a wrapper, you are lowering the strength, degree, and locality of the connascence shared between your code and its dependencies. This makes your code more flexible and easier to maintain.