We’ve reached the last of the design principles related to package coupling, which means we have in effect reached the last of all the package design principles. This principle, the Stable Abstractions principle, is about stability, just like the Stable Dependencies principle. While the previous principle told us to depend “in the direction of stability,” this principle says that packages should depend in the direction of abstractness.
Stability and Abstractness
The name of the Stable Abstractions principle contains two important words: “stable” and “abstract”. We already discussed stability of packages in the previous chapter. A stable package is not likely to change heavily. It has no dependencies so there is no external reason for it to change. At the same time, other packages depend on it. Therefore, the package should not change, in order to prevent problems with those depending packages.
In the previous chapter, we learned that you can calculate stability and that you can verify that the dependency graph of a project contains only dependencies of increasing stability, or decreasing instability. In this chapter, we learn that we also have to calculate the abstractness of packages and that the dependency direction should be one of increasing abstractness, or decreasing concreteness.
The concept of abstractness is something we also encountered in previous chapters. For example, in the chapter about the Dependency Inversion principle (Chapter 5), we learned that our classes should depend on abstractions, not on concretions. We discussed several ways in which a class can be abstract. The most obvious way is when a class has abstract (also known as virtual) methods. These methods have to be defined in a subclass. This subclass is a concrete class because it is a full implementation of the type of thing that the abstract class tries to model. When a class only has abstract methods, we usually don’t call it a class, but an interface. Classes that implement the interface eventually have to provide an implementation for all of the abstract methods defined in the interface.
And this is where the two concepts, stability and abstractness, meet. If we consider stability to be the likeliness that something is going to change, then what is true for classes is also true for packages. As we know now, it’s better to depend on stable packages than to depend on unstable packages. Stable packages are less likely to change, so a depending package won’t be negatively influenced by changes in its dependencies. In the same way it’s safer to depend on abstract classes or interfaces because they are less likely to change.
We can follow the same reasoning while we apply the concept of abstractness to packages: it would be better to depend on an abstract package than on a concrete package. For the same reason—an abstract package would contain no particular implementation details that would be susceptible to change. Over a longer period of time it will stay the same.
How to Determine If a Package Is Abstract
The question is: is it possible to mark a package as either abstract or concrete? It is possible, even though “being abstract” is not a Boolean value. There are many degrees of abstractness. We might consider a class to be abstract if it contains at least one abstract method. Then a class is concrete if it has no abstract (or virtual) methods.
We can determine the abstractness of packages in a similar way. A package is abstract if it contains no regular classes, only interfaces and abstract classes. And a package is concrete if it has at least one fully implemented, concrete class.
Still we’d need a little nuance here. According to this definition of abstract and concrete packages, a package with 10 interfaces and 1 concrete class would count as a concrete package, even though it contains many more abstract things than concrete things. Therefore, we should take the total number of classes and interfaces into account.
The A Metric
When the value of the A metric for a package is equal to or near 0, it’s a highly concrete package. It contains (almost) no interfaces, only concrete classes, so it’s full of implementation details, and therefore liable to change.
When, on the other hand, the A metric is equal to or near 1, it’s a highly abstract package. It contains (almost) no concrete classes, but mostly abstract classes and interfaces. It’s likely that these abstract things will stay the same over time. After all, only concrete classes and consequently concrete packages are liable to change.
Abstract Things Belong in Stable Packages
Packages that are maximally stable should be maximally abstract. Instable packages should be concrete. The abstraction of a package should be in proportion to its stability. 1
We already know that a package should depend only on packages that are more stable. Now we also know that abstract things are likely to be more stable, i.e., they change less often and less dramatically. Hence concrete classes can safely depend on them. But what if an interface is part of a highly unstable package? Then it’s consequently not safe to depend on that package. The unstable package is likely to change. The interface inherits the instability of its containing package.
Interfaces and abstract classes are better off in a stable package. The stability of the package itself will be beneficial for the abstract things it contains. At the same time, the abstract things are good for the stability of the containing package. Packages that apply the Dependency Inversion principle start to depend on it because of the abstract things it contains. This turns it into a more responsible package and will thereby force it to become more stable.
The opposite is also true: concrete classes are better off in unstable packages. The implementation details of the classes are likely to change anyway and this would better happen in an unstable package, which has less responsibility toward depending packages. If, however, a concrete class would be inside a highly stable package, it would make that package more unstable because a concrete class is liable to change.
Abstractness Increases with Stability
The Stable Abstractions principle adds one extra requirement. It wants to unify abstractness and stability into this simple rule: a package should be as abstract as it is stable.
Now you only need to verify that each dependency arrow leads to a package with a higher value for A. In other words, dependencies should have an increasing abstractness.
Strictly speaking, the values for I and A added together should be exactly 1. This would mean that all packages are as abstract as they are stable. But this is completely unrealistic. There is always some margin to this. However, I + A should not be too far away from 1. In general, highly abstract packages should be highly stable, and concrete packages should be unstable.
The Main Sequence
It’s possible that stable packages contain concrete classes and unstable packages contain abstract classes or interfaces. These packages might be easy to spot, but there are many degrees of stability and abstractness. To find out which packages have imbalanced values for I and A, we can plot the packages in a diagram called the main sequence diagram.
You’ll know when you have applied both the Stable Dependencies principle and the Stable Abstractions principle correctly if all the packages are near or on the diagonal, i.e. the main sequence. According to this rule, the previously shown main sequence diagram looks pretty good.
If you find any package that lies quite far from the main sequence, you should take a closer look at it. Consider its surrounding packages too and try to make some changes to its dependencies or its dependents in order to achieve the right amount of stability. It may also be necessary to change the abstractness of the package by relocating some abstract classes or interfaces.
Once you’ve fixed the biggest issues with stability and abstractness, you shouldn’t forget to regularly come back to see if packages have not started to drift away from the main sequence. After some time the nature of existing packages may change because of new features being added to them and this may eventually cause an imbalance.
Types of Packages
When you travel down the main sequence from the top-left to the bottom-right corner, you will first come across highly concrete, highly unstable packages. These should all be application-level packages. They are full of implementation details, specific to the actual project you’re working on. They are allowed to be concrete, because no other package depends on them. Hence, they are unstable packages bound to change as often as the business changes.
Taking some more steps on the main sequence you will find in the middle packages that are somewhat abstract and somewhat stable. They are much less affected by external changes, but to a certain degree they are allowed to change themselves without bringing the whole project in danger.
When the journey on the main sequence ends, you will be in the realm of abstract, stable packages: the foundational blocks of your application. These contain lots of interfaces and abstract classes, which is why many classes (and consequently packages) depend on them. These classes apply the Dependency Inversion principle correctly in order to be less susceptible to change. Hence, these abstract packages should be stable too. And they are, because they are responsible and have no dependencies themselves.
Interface Packages
When you want to create highly abstract, stable packages, you may end up simply extracting the interfaces from existing packages and putting them all in one big interface package.
This is not the best thing you can do for your project. By putting all the interfaces in one package, you are going to violate the Common Reuse principle: some clients need just one or two interfaces from the package, but still they would have to depend on the entire package.
Besides, not all interfaces are meant to be depended on outside of a given package. You may have lots of interfaces that are only for private use within the same package (depending on your programming language, you may be able to mark an interface as private).
One last thing to be aware of: before moving interfaces to a separate project, make sure that you have properly applied the Interface Segregation principle to all of them. That way, clients won’t be forced to depend on interfaces with methods they do not or should not want to use.
If you properly apply these rules, go ahead and create lots of small interface packages, each in support of one specific feature.
Strange Packages
Irresponsible (no other packages depend on them)
Dependent (they depend on lots of other packages)
Abstract (they contain only abstract classes or interfaces)
This is a very strange kind of package. It’s very unlikely that such a package can be found in your project, because it would most likely contain dead code. It’s never used by any part of the project, yet the code is abstract, meaning that it cannot be used standalone—someone has to provide an implementation for it. In other words, packages like these are useless and you should try to get rid of them.
Responsible (many packages depend on them)
Independent (they have no dependencies)
Concrete (they contain only concrete classes)
This kind of package would be heavily used all over the project. Because it doesn’t need any other packages to do its work, this is probably some kind of low-level library. However, it doesn’t offer any abstractions for the things it does. This means that it’s hard for classes in other packages to naturally comply to the Dependency Inversion principle: they have to depend on the concrete classes from this package instead of interfaces.
The solution for getting this package back in shape is to apply the Dependency Inversion principle to depending packages. They should not depend on the concrete classes from this package anymore, but instead depend on their own interfaces or interfaces defined in some more stable and abstract package.
Conclusion
In this chapter, we discussed the intimate relation between stability and abstractness. We learned that the more stable a package is, the more abstract things it should contain. The counterpart of this is that the more concrete things a package contains, the more unstable it becomes.
We looked at the main sequence diagram to get a grip on different kinds of packages and where they are on the sliding scale between concrete/unstable and abstract/stable. Using this diagram, we are able to spot packages with extraordinary characteristics.