Don't marry your little dependencies

Iacami Enapupê Gevaerd
5 min readSep 18, 2018

--

In this post, I'll talk about one simple mistake that developers often do and will certainly call for refactoring sooner than later: dependency marriage.

What do you mean by marrying a dependency?

In modern JavaScript (i.e. using JS module imports and possibly a bundler) it's very common to consume third party code from npm. Whenever you see a line like this:

import somePackage from 'some-package'

You are actually getting this:

Now imagine your codebase hand's with more rings than fingers can take.

The first and foremost advice one can give you on this subject is: avoid marring your dependencies at all cost, unless when you really don’t have a choice.

This post was not about selecting a good proponent

But I guess this topic lately has been is in needs of more attention.

When you do that import mentioned above, you are immediately inheriting all third-parties' features, this is practical and awesome. But in my opinion, not just the most important part of the deal.

If the first signal you take into account before putting the ring, is the number of the package’s stars, you are doomed! This is like falling in love at first sight and getting married the same day.

It's not just the features that matter, the dependency you just married has history, bugs, roadmap and who knows what else it may be hiding from you?

But I love it, what could go wrong?

Well, so many things I'll have to make it a list:

  • The package could be abandoned — JS is the hype of the moment, it means new packages will be appear and fade away on a blink. The ecosystem is changing too fast, something that seems crucial today may become obsolete tomorrow.
  • The package could stop supporting the only feature you consumed from it — some package starts with some specific goal but as it grows it starts to embrace more things until this initial goal is shaded by non-crucial things. The author now wants to get back to its original course and decides to deprecate everything non-crucial, i.e. that only feature you make use of.
  • The package gets fat: it happens. A lot. Your app is an athlete, weights about nothing and loads super fast, but then one of your dependencies gets fat and no longer can keep up if your speed and weight.
  • The package you consume married so many packages that it's spending all it's time trying to counsel with other packages. No progress can be done meanwhile.

How to protect yourself from a painful divorce

Analogies apart, let's get practical.

We'll now cover some practices that will make this marriage last longer and certainly help when the divorce is due. We'll be using https://github.com/simov/slugify as real-life package example.

The first step is to always "proxy" (or adapt) your marriage through a local file:

export default from 'slugify'

This is your postnuptial agreement, by doing that you assume control, you can now rest assured your divorce options will be much improved. When things go south, you'll no longer have to touch all the files that consumed that dependencies's feature.

Adapting the marriage so it works both ways

Say you want to tweak a default option for this package. Before you'd just have multiple calls like this:

import slugify from 'slugify'const mySlug = slugify(someString, { lower: true })

But since you always want to have a lowercase slug, it's better to use that adapter file and tweak this option standard for good so, instead of just returning the default export, we'll create a function wrapper on top of that package function:

import slugify from 'slugify'const makeSlug = (string, options) =>
slugify(string, {
lower: true,
...options,
})
export default makeSlug

With the chunk of code above, we maintained the same API, but switched a default parameter to our taste. This is very flexible because you can always override lower, passing it as a secondary parameter option and set it false.

Adding to the chemistry

Now say you want to limit the length of the slug string, but the package itself doesn't provide that feature. What are you gonna do? Break up? Find another proponent?

No! You are covered, just improve your wrapper function so it handles it for you. By using ES6 spread operator you can safely keep the same API while adding more options on top if it.

import slugify from 'slugify'const makeSlug = (string, { maxLength = MAX_SLUG_LENGTH, ...options } = {}) =>
slugify(string, {
lower: true,
...options,
}).substr(0, maxLength)
export default makeSlug

We enhanced the second parameter with an additional maxLengthoption, while keeping compatibility.

Make sure it passes the test

When adding a wrapper on top of any package, it's good to add unit tests that cover your usage expectations but also any additions/tweaks you added on top of that package.

import slugify from './slugify'test('slugify', () => {
expect(slugify('UPPER CASE string')).toBe('upper-case-string')
expect(slugify('UPPER CASE string', { lower: false })).toBe('UPPER-CASE-string')
expect(
slugify('something with more length then allowed which should be trimmed to the default maxlen')
.length
).toBe(MAX_SLUG_LENGTH)
})

We’re just not right for each other

Sadly the moment you are now prepared for has come: The package you married has gone private and you cannot afford its license. Too bad?

Hopefully you will quickly find another proponent (not based on ⭐️️️⭐️). Now you are better experienced and less likely to make a bad choice. Adapting should be much less painful.

Wrapping up

  • Choose wisely and you may only have to choose once.
  • Always wrap the dependency in a small adapter/driver/proxy file.
  • Add tests cases that cover the features you consume
  • Try to make things work before breaking up

--

--