Dependencies are nearly unavoidable for non-trivial software projects. These can be large dependencies, such as web frameworks and databases, or smaller decisions, such as pagination helpers or simple math libraries. The inevitable inclusion of these dependencies, however, is not an excuse to shape the solutions of your system around what you do not own. Instead, there ought to be an emphasis on finding the right fit and then owning the way dependencies interface with your system. Furthermore, we ought to build our system such that dependencies are plugins and carry nearly the same cost to remove as to add in the first place.
Evaluating What Is Necessary
It's tempting to begin a feature by looking for a solution that already exists. Common software problems have been solved, and solved well, by the open-source community. It's easy to observe that some solutions are better and more accepted than others, but even with these observations you need to factor in a few more points before introducing new dependencies.
Remember that when you introduce a dependency you are placing the correctness of your system in the hands of some external piece of software. The authors did not have your system or specific use cases in mind and it might not be an exact fit for your system. Begin by asking yourself a few questions:
- “Can I build this myself with a reasonable amount of time and effort?”
- “Will the increased flexibility of what I could build be beneficial?”
Let these questions guide the weight of the pros and cons of dependency inclusion.
Wrap The Interface
You've weighed the pros and cons and have decided to include some dependency. What happens when your dependency falls down for your use cases and is no longer adequate? Do you smack your head and bemoan the work it's going to take to rip the dependency out? Why is that work so difficult? It's because the interface your code depenends on is usually the interface of the dependency, or at least heavily influenced by it. You've essentially leaked the details of the dependency into your codebase instead of hiding that detail. Then, when it comes times to remove that dependency, your codebase is completely dependent on code that does not fullfill your needs. Instead, we ought to decide what needs we have for the dependency. Once we are aware of the needs we can then make an interface corresponding to each way in which we intend to use it. Instead of conforming to the dependency, we have forced the dependency to conform to our needs making the dependency a detail.
Eric Smith has an excellent post on the subject of mocking only what you own. Everyone should go read it, he has great insight on why mocking third party interfaces is a bad idea. Step one to mocking only what you own, as Eric points out, is to wrap the dependency's interface.
When we wrap our dependencies' uses into coherent interfaces, then dependencies will be almost as easy to remove as they were to introduce. Furthermore, the penalty of switching the dependency out for another is significantly lower. Consider the following example with JQuery's AJAX feature.
This seems pretty standard, most applications that use JQuery tend to use these kinds of functions all over the code. The problem is that we are totally dependent on JQuery's ajax method all over our code. Instead, we can recognize that JQuery is a large depedency and that AJAX is a candidate for an interface that we can define in order to wrap a portion of JQuery. So, we write something like this:
Of course, this interface is possibly incomplete as we might want to pass data and configure other ajax settings, but over time the interface will become stable and reflect what uses we have for AJAX. Our code will refer to an idea, AJAX, and not to a specific implementation, namely, $.ajax. Once we have this interface and JQuery specific implementation in place then we can easily swap out JQuery by reimplementing the AJAX interface with our new implementation.
One big win with this approach is the ability to change out the dependency with something else when it comes times to test. For every invocation of $.ajax in the first example's style of coding there is usually a corresponding stub on $.ajax in the test. If we wrap and own the dependency then we are open to multiple implementations of the AJAX interface and can have confidence in swapping implementations.
If you are starting a new feature and feel a dependency is necessary then begin by specifying it's usefulness to you and your project in an interface. If you already have dependencies called all over your code begin refactoring them to an interface as you see them over time. Your future self will thank you immensely when a dependency is a detail and is easily managed.