POSTS

Clean Boundaries

In Clean Code, Robert C. Martin (a.k.a Uncle Bob), devotes a short chapter to what he calls “boundaries”, which are the places in your code that directly interact with third-party code like APIs, packages, or libraries that you do not have control over.

Two key ways to keep your boundaries clean, according to the chapter, are to:

  1. limit the amount of your code that interacts directly with this foreign code, and
  2. write tests that confirm it works the way you expect it too

By encapsulating third-party packages in your own well-tested classes, you can be alerted to any breaking changes caused by package upgrades early and limit the amount of code you need to refactor to get things working again if they have broken.

For example, our Node API uses the package mongo-models, which is like a lighter-weight version of mongoose. It provides methods to interact with MongoDB collections as classes and returns query results as instances of those classes.

Let’s say that we started using this package when they were on version 1.0.0 and that our application only used two of the package’s methods, find and update. If we were following Uncle Bob’s suggestion, we would have created a wrapper class that simply returns a call to the underlying package methods.

// A wrapper class for protecting against future breaking changes
const MongoModels = require('mongo-models');

class NvMongoModels {
  static find (filter, callback) {
    return super.find(filter, callback);
  }

  update (filter, update, callback) {
    return super.update(filter, update, callback);
  }
}

Now let’s suppose, mongo-models releases version 2.0.0 of their package, which results in these breaking changes.

If we had written tests to confirm our NvMongoModels methods did what we expected, upgrading the package would cause them to fail, alerting us that something had changed.

Dealing with breaking changes is a breeze

Most of the breaking changes linked above are pretty straightforward. We would have to change the key collection to collectionName in all of our models, and the parameters for connecting to the database have changed. And a few changes don’t affect us at all because they are related to methods we’re not using.

But one of these changes would cause us a bit of headache by requiring a lot of tedious refactoring, which is the removal of callbacks in favor of async/await. (At current count we have 147 references to these two methods across both our application and our test files).

Luckily, we followed Uncle Bob’s advice, and now our refactor will be a breeze. Because we only really have to change 2 methods to get everything working. We can maintain the call signature and response of our internal find and update methods, but refactor their internal implementation to accommodate mongo-model’s async/await requirements.

// A wrapper class for protecting against future breaking changes
const MongoModels = require('mongo-models');

class NvMongoModels {
  // A temporary change until we can refactor all references to these methods
  static async find ( filter, callback ) {
    try { 
      const results = await super.find( filter );
      return callback( null, callback );
    } catch ( err ) {
      return callback( err );
    }
  }

  async update ( filter, update, callback ) {
    try { 
      const results = await super.update( filter, update );
      return callback( null, results );
    } catch ( err ) {
      return callback( err );
    }    
  }
}

If we run our tests and they pass then we’re good to go! We can now refactor all of our calls to find and update to utilize async/await at our convenience.

Wrapping your boundaries in a class you control, and testing those methods thoroughly, means that you can upgrade your packages with confidence, and limit the scope of any breaking changes. This in turn increases the likelihood you will actually keep your package up-to-date.

comments powered by Disqus