In this post I go over the principles that govern package (library) design and one specific issue I have come across several times.
Robert C Martin has proposed six Package Principles. Personally I find the (linked) description on Wikipedia rather confusing, especially if you do not already understand the principles. Here is my take on library design using short and simple points:
- cover a single responsibility
- provide a clean and well segregated interface to their users
- hide/encapsulate implementation details
- not have cyclic dependencies
- not depend on less stable libraries
The rest of this post focuses on the importance of the last point.
Stable Base Libraries
I am assuming usage of a package manager such as Composer (PHP) or NPM. This means that each library defines its dependencies (if any) and the version ranges of those that it is compatible with. Libraries have releases and these follow semver. It also means that libraries are ultimately consumed by applications that define their dependencies in the same way.
Consider the scenario where you have various applications that all consume a base library.
If this library contains a lot of concrete code, it will not be very stable and it will now and then be needed or desired to make a major release. A major release is one that can contain breaking changes and therefore does not automatically get used by consumers of the library. These consumers instead need to manually update the version range of the library they are compatible with and possibly adjust their code to work with the breaking changes.
In this scenario that is not a problem. Suppose the library is at version 1.2 and that all applications use version 1.x. If breaking changes are made in the library, these will need to be released as version 2.0. Due to the versioning system the consuming applications can upgrade at their leisure. There is of course some cost to making a major release. The consumers still do need to spend some time on the upgrade, especially if they are affected by the breaking changes. Still, this is typically easy to deal with and is a normal part of the development process.
Things change drastically when we have an unstable library that is used by other libraries, even if those libraries themselves are unstable.
In such a scenario making breaking changes to the base library are very costly. Let’s assume we make a breaking change to the base library and that we want to use it in our application. What do we actually need to do to get there?
- Make a release of the base library (Library A)
- Update library B to specify it is compatible with the new A and make a new release of B
- Update library C to specify it is compatible with the new A and make a new release of C
- Update library D to specify it is compatible with the new A and make a new release of D
- Update our application to specify it is compatible with the new version of A
In other words, we need to make a new release of EVERY library that uses our base library and that is also used by our application. This can be very painful, and is a big waste of time if these intermediate libraries where not actually affected by the breaking change to begin with. This means that it is costly, and generally a bad idea, to have libraries depend on another library that is not very stable.
Stability can be achieved in several ways. If the package is very abstract, and we assume good design, it will be stable. Think of a package providing a logging interface, such as psr/log. It can also be approached by a combination of following the package principles and taking care to avoid breaking changes.
Keep the package principles in mind, and avoid depending on unstable libraries in your own libraries where possible.