In the previous chapter, we discussed the Acyclic Dependencies principle , which helps us prevent cycles in our dependency graphs. The greatest danger of cyclic dependencies is that problems in one of your dependencies might backfire after they have travelled the entire cycle through the dependency graph.
Even when your dependency graph has no cycles, there’s still a chance that dependencies of a package will start causing problems at any time in the future. Whenever you upgrade one of your project’s dependencies, you hope that your project will still work as it did before. However there’s always the risk that it suddenly starts to fail in unexpected ways.
When your project still works after an upgrade of its dependencies, the maintainers of those dependencies are probably aware that many packages depend on their package. So in each patch or minor release, they will only fix bugs or add new features. They never push changes that would cause failure in a dependent package.
If, however, something is suddenly broken in your project after an upgrade of one of the dependencies, its package maintainers apparently made some changes that are not backward compatible. These kinds of changes bubble up through the dependency graph and cause problems in dependent packages.
When a dependency of your project suddenly causes failures, you must first rethink your choice of dependencies instead of blaming the maintainers. Some packages are highly volatile, some are not. It can be in the nature of a package to change frequently, for any reason. Maybe those changes are related to the problem domain, or maybe they are related to one of its dependencies.
Likewise, before adding a dependency to your project, you need to decide: is it likely that this dependency is going to change? Is it easy for its maintainers to change it? In other words, can the dependency be considered stable, or is it unstable?
Semantic Versioning and Stability
As we discussed in the chapter about the Release/Reuse Equivalence principle (Chapter 6), the word “stable” is also used in the context of semantic versioning. A package is considered stable if it has a version that is at least 1.0.0, and is not in a development (or alpha, beta, RC) branch. Such a stable version promises to have a public API that does not change in backward incompatible ways.
The Stable Dependencies principle is also about the stability of a package, but isn’t necessarily related to semantic versioning. In this chapter, “stable” means “not likely to change”. A stable package in this context is a package on which many other packages depend, while it does not depend on other packages itself.
Stability
The stability of a package is all about how easy it is to change something in its code. This is not about clean code, or if the code can be easily refactored. It is about how responsible the package is with respect to other packages and if the package is susceptible to changes in any one of its dependencies.
The maintainer of such an irresponsible package is free to change anything they like. On the contrary, a package with many dependents can be called responsible since its maintainer cannot just change anything they want. Any change should be expected to have an impact on depending packages.
At this point, it makes sense to not only take into account the number of packages depending on your package, but to also consider the number of applications that are depending on the package. As a package developer, you can’t always get an accurate view of this, but package managers usually track the number of downloads for a package. If it’s high, you can be certain that the package has many users. In that case, you have a responsible package , meaning that it needs to be stable for its users.
Not Every Package Can Be Highly Stable
These highly stable packages are usually small libraries of code that implement some abstract concepts that are useful in many different contexts.
These highly unstable packages are likely to contain concrete implementations that are, for example, coupled to a specific persistence library, or they may contain detailed implementations of business rules that are liable to change. Code that is only useful in the context of a certain application framework is also likely to be inside an unstable package, since a framework is itself highly unstable according to the definition used in this chapter.
Finally, there are packages that have no dependencies, but no other packages (or applications) depend on them too. These packages are independent and irresponsible. These are useless packages. Most packages, however, are somewhere between highly independent and responsible and highly dependent and irresponsible.
Unstable Packages Should Only Depend on More Stable Packages
Intuitively it would be alright for an unstable package to depend on a stable package. After all, the stable package is unlikely to have negative effects on an already unstable package. However, the other way around—a stable package that depends on an unstable package —would not be acceptable. The volatility of an unstable package would pose a threat to the stability of the stable package and would in fact make it less stable.
The dependencies between packages in a design should be in the direction of the stability of the packages. A package should only depend upon packages that are more stable than it is. 1
In other words, less stable packages may depend on more stable packages. Stable packages should not depend on unstable packages.
Measuring Stability
Stability is actually a quantifiable unit, which we can use to determine if any package in a dependency graph satisfies the Stable Dependencies principle.
The conventional way of expressing stability is by calculating the I metric for packages. First you need to count the number of classes outside a package that depend on a class inside the package. We call this value C-in. Then you need to count the number of classes outside the package that any class inside the package depends on. We call this C-out.
You can then determine the I metric for the package by calculating C-out divided by C-in + C-out. This means that I will be between 0 and 1, where 1 indicates that the package is maximally unstable and 0 indicates that it is maximally stable.
A highly stable package is responsible: it has many dependents, so C-in is a high number. At the same time it’s independent: it has no dependencies, so C-out = 0. This means that I = 0 since C-out / (C-in + C-out) = 0.
A highly unstable package is very dependent: it has many dependencies, so C-out is a high number. But it’s also irresponsible: it has no other packages depending on it, so C-in = 0. Then I = 1 since C-out / (C-in + C-out) = 1.
Decreasing Instability, Increasing Stability
Question: Should We Take Into Account All the Packages in the Universe?
It’s an interesting question. The more packages that depend on a package, the more responsible that package will be, and therefore the more stable it becomes. But when calculating package coupling metrics, it would be practically impossible to take all the other packages and applications in the world into consideration. So, when we do calculate the I metric, and later the A metric, we can and should only look at all the packages that are installed in a given application. We can put them all into one big dependency diagram, and start verifying how well they follow the package coupling principles.
In the following sections, we discuss some violations of the Stable Dependencies principle and how you can fix them (if you have the power to do so!).
Violation: Your Stable Package Depends on a Third-Party Unstable Package
In the following example, I use the Gaufrette library,2 which offers an abstraction layer for filesystems. It allows you to switch from a local filesystem to an in-memory filesystem, or even to Dropbox or Amazon storage, without the need to make changes to your own code.
The FileCopy Class
List of Dependencies of the filesystem-manipulation Package
When we make the same calculation for the knplabs/gaufrette package, we need to count the number of classes outside that package that are depended on by classes inside the package. This package contains lots of adapter classes to make its filesystem abstraction work with all kinds of external storage solutions. All of these classes need extra dependencies to do the work. So this explains the high number of outgoing dependencies, which after counting turns out to be 54. So C-out = 54. Within the current project, only the FileCopy class depends on one of the classes of knplabs/gaufrette, so C-in = 1. This results in a fairly high value for I, namely 54 / (54 + 1) = 54/55, which is almost 1.
The reason why knplabs/gaufrette is such an unstable package is that it contains many concrete filesystem adapters for Dropbox, Amazon S3, SFTP, etc. These adapters are not used by everyone at the same time. So, according to the Common Reuse principle , they should have been in separate packages.
The filesystem-manipulation package does not need all those specific filesystem adapters; it only needs the Filesystem class, which provides generic methods for communicating with any specific filesystem.
Solution: Use Dependency Inversion
In order to fix the dependency graph and force the arrows to point in the direction of stability, we would very much like to take the Filesystem class (which is the actual filesystem abstraction layer) and put it inside a separate package: knplabs/gaufrette-filesystem-abstraction. Then the adapter classes should be placed inside other packages, like knplabs/gaufrette-amazon-adapter, knplabs-/gaufrette-sftp-adapter, etc. We could then change the dependency on knplabs/gaufrette to knplabs/gaufrette-filesystem-abstraction and this would do the trick.
The FilesystemInterface
FileCopy Uses the New FilesystemInterface
Now we can actually remove the dependency on knplabs/gaufrette from the package definition of our filesystem-manipulation package. As a matter of fact, the package has become independent at once: it has no dependencies at all. This means that it now has an I of 0 and it is to be considered highly stable.
The GaufretteFilesystemAdapter
List of Dependencies of the gaufrette-filesystem-adapter Package
The C-out of this new gaufrette-filesystem-adapter package is 2, because it uses two classes outside the package. Its C-in is 0, since no other package uses a class from this package. This means I = 2 /(2 + 0) = 1. It’s highly unstable (i.e., easy to change), which is totally fine for an adapter package.
The packages are now sorted in the direction of stability, and all dependency arrows are pointing downward, which means that no package in this system violates the Stable Dependencies principle anymore.
All of this was accomplished without making any changes to third-party code. We applied the Dependency Inversion principle to the FileCopy class by letting it depend on something abstract instead of something concrete. This automatically makes the FileCopy class easily extensible: others can implement their own adapters for Filesystem and make it compatible with, for instance, the Flysystem filesystem abstraction library.3 It also makes the filesystem-manipulation better maintainable, since changes in knplabs/gaufrette will not affect it anymore.
Staying unaffected by external changes makes the filesystem-manipulation package very stable. It’s unlikely to change because of its dependencies (since it has no dependencies anymore). Its previous instability is pushed away to the more concrete gaufrette-filesystem-adapter package , which is now susceptible to changes in knplabs/gaufrette. But even though the code inside the gaufrette-filesystem-adapter package is likely to change, it poses no threat to other parts of the system, since no other package depends on it.
A Package Can Be Both Responsible and Irresponsible
As I already quickly pointed out, the knplabs/gaufrette package has some design issues. It contains classes that would not be used by everyone who uses the package in their project, so it violates the Common Reuse principle . It also contains classes (the same classes actually) that are not closed against the same kinds of changes, so the package violates the Common Closure principle.
Now that we are looking at the knplabs/gaufrette package from the perspective of stability, it becomes clear that grouping those classes that actually don’t belong together is the reason why this package has become very unstable. It introduces many external dependencies, which makes it no longer safe for other packages to depend on it.
Not being safe to depend on is not a good property for packages that are supposed to be highly reusable. In fact, a reliable package should be very safe to depend on: it should be stable. In other words, it should be independent and responsible.
In the previous section we discussed a solution for this stability problem. It entailed the introduction of an interface and an adapter to rearrange the dependency directions. We needed to resort to this solution because we could not do what was really necessary—to split the package into a package containing the more generally reusable parts (like the GaufretteFilesystem class and the GaufretteAdapter interface) and one or more packages containing the more specific and concrete parts (like the filesystem adapters for SFTP, Dropbox, etc.).
The great thing is that in such a constellation of packages, knplabs/gaufrette-filesystem-abstraction would be very stable, and the filesystem-manipulation package containing the FileCopy class could safely depend on it. The filesystem-manipulation package itself has an I of 0.5, while knplabs/gaufrette-filesystem-abstraction has an I of 0, which is lower. All package dependencies would follow the direction of stability, so the Stable Dependencies principle would not be violated.
Conclusion
According to the Stable Dependencies principle, packages should depend in the direction of stability. This means that every package should depend only on packages that are more stable than the package itself is. The stability of a package is a measurement of how likely it is to change.
A stable package will be both independent (it has only a few dependencies, or none at all) and responsible (many classes depend on it). An unstable package will be dependent (it has many dependencies) and irresponsible (no classes, or just a few, depend on it).
With the I-metric, (in)stability can be quantified as C-out / (C-out + C-in), where C-out is the number of classes the package depends on, and C-in is the number of classes that depend on a class in this package. If I gets closer to 1, the package is relatively unstable. If it gets closer to 0, it’s relatively stable.