It’s not easy to dismiss one way or another. But we should know when and how to use dependencies, and what hidden costs may apply.
When to use another dependency
The concept of stdlib differs significantly between programming languages. Two of the languages I use have rich and useful stdlibs: Go and Java.
Different types of dependencies
Dependencies come in many forms and sizes, I categorize them into 3 groups:
- heavy hitters
- “everyone is using it” / “just use a dependency instead
Some dependencies are the core of our project, so we can’t argue much about not using them. If we want to build a React application, we need to include the React dependency. A Java Spring application is hard to do without Spring.
Choosing our heavy hitter is an entirely different question than choosing other dependencies. The necessity to use the dependency was decided when we settled on the technology stack. And it became an essential requirement that forms the concept and base of our project. It will influence our code and even our other dependency choices, so choose wisely.
We don’t have to reinvent the wheel every time we start a new project. Many dependencies are considered the defacto standard for doing certain things.
Apache Commons or Google Guava for Java are good examples for comprehensive collections of functionality to help us to do many standard and mundane tasks we might encounter in a project, without risking introducing bugs by implementing them ourselves. They are battle-tested by literally a bajillion projects, have good and comprehensive documentation, and if you read the code, we might even understand what they are doing and learn a thing or two about best-practices.
Most programming languages have such dependencies, and it’s mostly safe to use them, their cost is little for what you gain. But be aware that some functionality might be redundant and should not be used if a language feature can solve it, too.
Of course, we could use Google Collections to create a new List:
It’s simple and easy to read/understand. But why not just use the provided stdlib solution, which is just as clear (at least since Java 7):
Actually, it’s 2 characters shorter and just as readable as before. It’s the supposed standard way to do it, and you don’t need to call another library for doing a simple task.
Everyone is using it / just use a dependency instead
Some languages with small stdlibs encourage you to use dependency for every little problem.
By merely using React as the core of our new project, we included 666 dependencies that might get deprecated or incompatible at any time. Or even worse, introduce bugs and security problems.
Do you want to know if your variable is a number? Here’s a dependency for that.
Check if it’s an array? Here is another.
Or this one.
Or that one.
Functions shouldn’t be packages. They are too small, pure functions are just some snippets of code.
Instead of many different dependencies for mathematical functions, like a sine-dependency and another cosine-dependency from different authors with varying grades of quality and maintenance, we should strive for a trigonometry-dependency bundling those functions, instead.
Just a single one has to break down to kill our project.
Do you remember LeftPadGate?
A single developer broke Node, Babel, and thousands of other projects by unpublishing his project with 11 lines of code for left-padding a string.
A new colleague
With every dependency in our project, we gain a new colleague working with us on it. They wrote a lot of support code, maybe even the central core is built by them. If they are nice and a good developer, they also included a lot of documentation and comments or even wrote some unit tests to ensure everything is working fine.
But it’s also possible that our new colleague is the worst.
We might not be able to contact them, ask questions, request new features, or have a say which way the code should go in the future.
Undecipherable code without any documentation, she even abandoned the project. But we depend on the code and can’t remove it without A LOT of work of our own. Sometimes we didn’t even hire them ourselves; they just tucked along when we hired their friend.
To ensure the work of our new colleague is the right decision, we must find a way to interview them beforehand, to see if they are a good fit for our project.
How to evaluate dependencies
So how can and should we effectively evaluate a dependency? At my company, we came up with a few criteria for considering a dependency worthy of inclusion in any of our projects.
Not a mundane problem
First, we have to think about the problem the dependency is supposed to solve.
Do we really need a dependency, or can we solve the problem myself in a right, reusable, testable, and secure way?
We rather code a solution ourselves, and maybe get a better understanding of the underlying problem, than including a dependency prematurely. This holds especially true if we only need a tiny fraction of the dependency.
Sometimes it makes more sense to code it ourselves, even copy some of the code of the dependency in our project directly (check the license first) than using a whole dependency.
The less magic is used to solve the problem, the better.
Many dependencies do great things by utilizing all the obscure language features we don’t know much about. For example, IoC-containers use all the magic they can find to build service proxies, on-the-fly code-generation, etc.
It’s great as long everything works as intended. But as soon as things aren’t working as intended anymore (and they will), we need to understand how the dependency is solving our problem, so we can fix it or even be able to replace it if necessary. If we need to debug through layer on top of layer of magic, in a black-box, that will be a problem.
We don’t need to understand every tiny bit of a dependency to use it. But it shouldn’t be totally opaque either.
It’s dead, Jim
Every dependency has a lifespan. Some might live forever, others are almost dead on arrival. The more active a dependency is maintained, the more likely we will use it.
- What if we run into a problem?
- Can we create an issue somewhere that is actually read?
- How fast are bugs fixed?
- How fast is a new version of your other framework/programming language adapted?
Removing a dead dependency is time-consuming and in some cases, not even possible.
And often it’s not even the dependency itself, think about all those transitive dependencies. Even with mature or complete dependencies, we could run into problems later on if the language itself changes.
Take Java for example. With version 9, it changed how dependencies work by introducing a module-system and moved some previously stdlib code into dependencies. If one of our dependencies relies on another dependency that might be unmaintained, we end up with being incompatible to the new language version and are cut off from all the new features, unless we remove the dependency.
So hopefully, it’s not our heavy-hitter…
Documentation & Code Quality
We always check the quality of the project’s documentation, and most of the time, we look at some of the code itself.
- Is the documentation complete and up-to-date?
- Is the code tested, does CI/CD exist?
- Is it easily readable?
A dependency with a good foundation will be a worthy addition to your project. Building on sand is never a good idea.
It’s not easy to choose dependencies, because we might depend on them for a long time.
Never use dependencies because we can do it only if we must, and only if they satisfy our standards. The time we might save from using a bajillion tiny dependencies doing mundane tasks will quickly be outweighed by maintaining and fixing problems later on.