© Matthias Noback 2018
Matthias NobackPrinciples of Package Designhttps://doi.org/10.1007/978-1-4842-4119-6_10

10. The Stable Dependencies Principle

Matthias Noback1 
(1)
Zeist, The Netherlands
 

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.

Changes in the dependencies of a package are likely to bubble up to the package itself. You will often need to make changes to your own package to accommodate for changes in its dependencies. If you have a lot of dependencies, it’s much more likely that an update of your dependencies will require you to modify your own package. Such a package would be called a dependent package (see Figure 10-1). When a package needs to be changed often to accommodate a change in one of its dependencies, it should be considered an unstable package.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig1_HTML.jpg
Figure 10-1

A highly dependent package

If a package has no dependencies, or just a small number of them, chances are that an update of your dependencies will cause no problems at all. Such a package is called an independent package (see Figure 10-2). Such a package isn’t very susceptible to changes in its dependencies, so it should be considered a stable package.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig2_HTML.jpg
Figure 10-2

An independent package

There’s another direction in the dependency graph that needs to be considered: the direction toward a package. In other words, how many other packages depend on a given package? If the number is high, it will be difficult to make changes to the package, because so many other packages are depending on it, and those local changes may require many modifications elsewhere. Such a package is called a responsible package (see Figure 10-3).
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig3_HTML.jpg
Figure 10-3

A responsible package is a package that has many packages depending on it

On the other hand, if the number of incoming dependencies is low or even lacking, it will be very easy for the package maintainer to make changes to it, since those changes will have little impact on other packages. We call a package with no other packages depending on it an irresponsible package, because it will not be held responsible for any changes that are made to it (see Figure 10-4).
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig4_HTML.jpg
Figure 10-4

An irresponsible package with no packages depending on it

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

Packages that are more independent and responsible should be considered highly stable. Those are packages that don’t need to change because of a change in one of their dependencies, but they also can’t easily change themselves because other packages heavily depend on them. See Figure 10-5.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig5_HTML.jpg
Figure 10-5

A highly stable package: no dependencies, only dependents

These highly stable packages are usually small libraries of code that implement some abstract concepts that are useful in many different contexts.

On the other side of the scale, packages that are more dependent but at the same time very irresponsible should be considered highly unstable . These packages are susceptible to changes in any of their dependencies, but they are not depended on by any other package, so it is no problem for them to change because a change would not ripple through. See Figure 10-6.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig6_HTML.jpg
Figure 10-6

A highly unstable package: many dependencies, no dependents

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.

To prevent package designers from introducing “bad” dependencies, the Stable Dependencies principle tells us that:

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.

Of course, these are very extreme examples. Most packages have an I that is not 0 nor 1, but somewhere in between. For example, the package in the center of Figure 10-7 has a C-out of 3 and a C-in of 2, so the value of I for that package is 3 / (2 + 3) = 0.6. This means that the package should be considered relatively unstable; the number of outgoing dependencies is higher than the number of incoming dependencies.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig7_HTML.jpg
Figure 10-7

Calculating C-in and C-out for the package in the center

Decreasing Instability, Increasing Stability

According to the Stable Dependencies principle, the dependencies between packages in a design should be “in the direction of the stability of the packages”. In other words, each step we take in the dependency graph should lead to a more stable package. More stable also means less unstable, so we are only allowed to take steps in the dependency graph leading to packages with a lower value for I (see Figure 10-8).
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig8_HTML.png
Figure 10-8

An example of packages that all depend in the direction of stability

When you draw such a diagram for your packages it’s useful to put packages with a low I near the bottom and packages with a high I near the top. Then every dependency arrow should point downward since that is the direction of stability. If an arrow would point upward, like in Figure 10-9, the Stable Dependencies principle has been violated (we will later discuss your options to force the arrow in the right direction again).
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig9_HTML.jpg
Figure 10-9

An example of packages that do not all depend in the direction of 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 in Listing 10-1 is part of my own package. Its naive implementation of a copy mechanism allows you to copy files between any two filesystems. It depends on the Filesystem class offered by the Gaufrette library.
use GaufretteFilesystem as GaufretteFilesystem
class FileCopy
{
    private $source;
    private $target;
    public function __construct(
        GaufretteFilesystem $source,
        GaufretteFilesystem $target
    ) {
        $this->source = $source;
        $this->target = $target;
    }
    public function copy($filename)
    {
        $fileContents = $this->source->get($filename);
        $this->target->write($filename, $fileContents);
    }
}
Listing 10-1

The FileCopy Class

The package that contains the FileCopy class , let’s call it filesystem-manipulation , has an explicit dependency on the knplabs/gaufrette package that contains the Filesystem class, as shown in Listing 10-2.
{
    "name": "filesystem-manipulation",
    "require": {
        "knplabs/gaufrette": "~0.1"
    }
}
Listing 10-2

List of Dependencies of the filesystem-manipulation Package

Currently, FileCopy is the only class in this package. It has one dependency on a class of another package, which causes the C-out of this package to be 1. In the project in which the filesystem-manipulation package is being used, there is one class that uses the FileCopy class, so C-in is also 1, which causes I to be 1 / (1 + 1) = 0.5.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig10_HTML.jpg
Figure 10-10

Calculating I for filesystem-manipulation

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.

So knplabs/gaufrette turns out to be a highly unstable package. Much more unstable than our own filesystem-manipulation package. Nevertheless, the filesystem-manipulation package depends on the entire knplabs/gaufrette package. So we clearly violate the Stable Dependencies principle, since our packages do not all depend in the direction of stability. Instead, our package depends in the direction of instability. This becomes even more clear when we order the packages according to their stability and then draw the dependency arrows (see Figure 10-11).
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig11_HTML.jpg
Figure 10-11

The filesystem-manipulation package depends on a less stable package

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.

However, we can’t do this, since we are not the maintainers of knplabs/gaufrette . So we need to resort to another solution, one which we’ve already discussed: we should apply the Dependency Inversion principle. First, instead of depending on the GaufretteFilesystem class , which is still inside a highly unstable package, we define our own FilesystemInterface (see Listing 10-3) inside our filesystem-manipulation package.
interface FilesystemInterface
{
    public function read($path): string;
    public function write($path, $contents): void;
}
Listing 10-3

The FilesystemInterface

Then we let the constructor of FileCopy accept objects that implement this new FilesystemInterface (see Listing 10-4).
class FileCopy
{
    // ...
    public function __construct(
        FilesystemInterface $source,
        FilesystemInterface $target
    ) {
        // ...
    }
    // ...
}
Listing 10-4

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.

As already mentioned, we’d still want to make use of the Gaufrette library. Therefore, we need to bridge the gap between FilesystemInterface and the GaufretteFilesystem class . We may accomplish this by introducing a new class, called GaufretteFilesystemAdapter (see Listing 10-5).
use GaufretteFilesystem as GaufretteFilesystem;
class GaufretteFilesystemAdapter implements FilesystemInterface
{
    private $gaufretteFilesystem;
    public function __construct(
        GaufretteFilesystem $gaufretteFilesystem
    ) {
        $this->gaufretteFilesystem = $gaufretteFilesystem;
    }
    public function read($path): string
    {
        return $this->gaufretteFilesystem->get($path);
    }
    public function write($path, $contents): void
    {
        $this->gaufretteFilesystem->write($path, $contents);
    }
}
Listing 10-5

The GaufretteFilesystemAdapter

This class uses Gaufrette’s filesystem object by composition and is at the same time a proper substitute for FilesystemInterface . We put this class in a new package, called gaufrettefilesystem-adapter. Since the class needs both the FilesystemInterface and the GaufretteFilesystem class, it depends on both knplabs/gaufrette and filesystem-manipulation (see Listing 10-6).
{
    "name": "gaufrette-filesystem-adapter",
    "require": {
        "knplabs/gaufrette": "1.*"
        "filesystem-manipulation": "*"
    }
}
Listing 10-6

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.

Take a look at Figure 10-12 to find out what all this did for the dependency graph and the arrows in it.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig12_HTML.jpg
Figure 10-12

Each package depends in the direction of stability

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 first package would have no dependencies, only dependents, which would make it independent and responsible, i.e. very stable. We would call it knplabs/gaufrette-filesystem-abstraction . The other packages would be named after the specific filesystems they provided an implementation for, like knplabs/gaufrette-sftp-adapter . Each of those packages could then have as many dependencies as needed by the specific filesystem implementation. And of course each of them would depend on knplabs/gaufrette-filesystem-abstraction because that package will contain the interface that each filesystem adapter needs to implement. It would make those adapter packages dependent and a bit less irresponsible. That is, the number of package depending on it will be smaller than the number of packages and applications depending on the core filesystem abstraction package.
../images/471891_1_En_10_Chapter/471891_1_En_10_Fig13_HTML.jpg
Figure 10-13

knplabs/gaufrette-* packages after refactoring

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.120.109