There’s a really good software engineering practice that for some reason doesn’t seem to be widely known or adopted: isolating dependencies. I’ve talked to many experienced devs that don’t seem to be aware of this practice or why it’s useful. Let’s start with an example to show what I mean (this is some React code, but it applies similarly to other code):
import Button from ‘third-party-ui’;
export default function MyComponent() {
return (
<div>
Actions:
<Button label=”Delete” />
<Button label=”Edit” />
</div>
);
}
Here we have a simple component that renders two Button
s, from some third-party UI library. This type of thing is very common for any UI. I will argue that this is (usually) the wrong thing to do. Instead, we should isolate the third-party code behind a thin wrapper:
// thin wrapper in MyButton.tsximport Button from ‘third-party-ui’;
export default Button;'
// then use it like so in MyComponent.tsximport Button from ‘MyButton’;
This might seem basically pointless, since the MyButton
wrapper doesn’t do anything. However there are several advantages:
Easier to handle API changes. Let’s say the
third-party-ui
library decided to make a breaking change in v2. You really want to upgrade to get access to some new feature, but now instead of<Button label=”Delete”>
you need to write<Button>Delete</Button>
. You could rewrite all your code to use the new format, but that may be difficult if it’s nontrivial. But if we have theMyButton
wrapper it becomes very easy, we simply update the wrapper so that it transparently handles the new API. This allows us to upgrade to the latest version without a painful process of updating all callsites.Easier to change libraries. Let’s say your designer comes to you and says actually we need to change our UI to some other look. You realize that
third-party-library
can’t handle the needed features, butnew-shiny-third-party-library
can do it. Since you have the wrappers you can simply update them to use the new library, handling any API differences as in #1. You even get the benefit of being able to update gradually, so for example you can first migrateButton
, thenCard
, etc. This allows you to reduce the risk that things will break.Easier to add custom functionality. Let’s say your designer comes to you and says we now need Buttons to look different when they are grouped (maybe margin is less and the internal borders are not rounded). You realize this can be done with some CSS adjustments to
Button
. Fortunately, because you haveMyButton
, you can simply apply this change there, and avoid littering the whole codebase with duplicated logic that then gets copy/pasted forever.Easier to mock in tests. Mocking third-party libraries can unnecessarily mock a large amount of code and makes all your tests potentially sensitive to API changes (as in #1). Having wrappers give you flexibility to only mock the specific pieces you are actually using, and keep the API stable so tests don’t break.
Better control over internal use. Some third-party libraries are very large. It may be that you want to limit internal use to some subset of the library. Perhaps this is because there are security or performance concerns that need to be considered and you were only able to check them for a subset. Whatever the reason, wrappers make it easy to limit use to only those things that are exposed. Code review can ensure that adding a wrapper is a higher visibility change than merely using it, so you don’t need to audit every single place calling the library, but only the infrequent times someone adds a new wrapper.
Control over the API. There are many third-party libraries that are great in functionality but have bad APIs. Perhaps they were written a long time ago and have to maintain compatibility. Perhaps they just don’t use the same patterns that your own code uses (for example: functional/immutable vs. stateful objects). Perhaps you are using PHP for some strange reason and everything returns false on errors. Whatever the case, having a wrapper allows you to modify this as can benefit your own application.
This principle can be applied to more than just third-party libraries. It makes sense when dealing with any code that you don’t own/control, so for example if you have a logging microservice, then it likewise makes sense to wrap any calls to it.
While I do believe that in general isolating dependencies is good practice, there are some cases in which it doesn’t make sense. As with anything in engineering, there are merely guidelines that you should still violate when it’s the right tradeoff. Consider some of the following points when deciding whether to avoid this practice:
It’s not used very much. If you only call some library in one place, then having a wrapper isn’t adding anything. It probably makes sense to wait until there are a few callsites before adding the wrapper. However, be careful with this approach as it’s easy to forget to do this (because no one wants to spend the extra time) and end up in the case of 100 unwrapped callsites that you then have to migrate.
The API surface is very large. If it’s a huge API and you use almost all of it, then it might be impractical to separately wrap all of it. It will just be an unwieldy amount of code. Since it’s uncommon to use the entirety of a huge API you could consider wrapping just the common parts, and any rarely-used features can be accessed directly. Sometimes this is done with a special call on the wrapper like
getRawImpl()
which gives you full power to access the original implementation. Conversely, something like an ORM would probably not be good to wrap, since you would essentially have to re-implement it yourself, and it’s very uncommon to switch.Dependencies are very stable. If you are working in an ecosystem that tends to be much more stable (like C or maybe Java) then there is of course less benefit for wrapping (although #3-6 still apply). However, be careful, as many things that seem to be stable for a long time can suddenly go away and leave you with a large amount of sudden migration work.
It’s very esoteric. Libraries that are very unusual or have no clear alternatives are much less likely to be replaced, and you are also less likely to want to make the type of changes that wrappers allow.
In general, the types of things that are most useful to wrap are “generic” libraries like UI components, logging, networking, DB access, or data manipulation utilities (string, array, etc. processing). When working with any of these you should strongly consider isolating them as soon as you add that dependency.