SOLID for packag... err, namespaces
The SOLID principles are a set of guidelines that drives good object-oriented design. They were defined from different people along the years, like Bertrand Meyer (OCP) and Barbara Liskov, but as far as I know Uncle Bob was the first to collect them together.
While studying for the advanced software engineering exam at PoliMi, I discovered there is also a set of principles akin to SOLID for the design of packages, and not only of classes and interfaces. Uncle Bob published them after the SOLID series.
Note that package here can be intended as a zip or any deliverable binary unit, not necessarily as a PHP namespace or Java package, or a source folder. Though, the principles still apply to the latter examples (but not only to them). In fact, I think that PHP namespaces will play the role of packages soon, as they provide a mechanism for encapsulation: use statements are not necessary when you refer to entities in the same namespace, just like java imports.
There are 3 principles on cohesion of packages and other 3 on dependencies and coupling. In this article we will explore them with examples (and pain points) from the PHP world, were they were either applied or ignored.
Many principles are the equivalent of a corresponding SOLID principle, but over different metrics (e.g. SAP).
RES: Reuse Release Principle
The reuse granularity and the release granularity shall be the same.
- Reuse is about the classes I instantiate and call directly or indirectly (via factories and collaboration.
- Release is about what you download or include in your system, or what has an independent version number and release cycle.
For example, consider Zend Framework, which is frequently cited as a library and not only a framework. It is downloadable only as two packages, standard and minimal edition; however, if I only want to reuse Zend_Cache and nothing else, I am forced to download and maintain the whole distribution, because the release granularity is lower than the reuse one.
For Zend Framework 1, the packageizer was retrofitted on the full distribution: a mechanism for selecting classes from the framework and automatically generate a package containing all the dependencies. For Zend Framework 2 a better system would probably be involved.
There is a trade-off between granularity and ease of use: imagine Zend_* components all with different version numbers: we will transport Jar hell in the PHP world.
CRP: Common Reuse Principle
Classes of a package are re-used together. If you reuse one, you reuse all the package.
The sense here is that you are are forced to grab the package as a whole, and cannot separate some classes because of possible compile/runtime dependencies (PHP compilation: it just means class source code loading.)
In Zend Framework, Zend_Form elements are all grouped together so that when using one you're forced to download other elements, and not for example classes that access the database. This is a straightforward case, but it's not so immediate to decide what should go in a package as well, for example in Zend_View or Zend_Controller.
CCP: Common Closure Principle
Classes of a package should be closed against the same modifications.
This principle is the equivalent of the Single Responsibility Principle for classes. Its take home point is that package maintainers should not be afraid or moving classes between your packages (before publishing the Api of course) if changes are a pain point.
PHPUnit packages are in the initial phase of their definition: phpunit-mock-objects has been extracted by Sebastian Bergmann from the main phpunit one, and are distributed as PEAR dependencies; thus installing PHPUnit still takes a single command.
However any time I have tried to contribute something to phpunit-mock-objects I ended up working also on phpunit to make the new feature available. With all the issues of the case: commits and pull requests to be made on two different repositories which are not atomic anymore, double and duplicated work for branching and pushing, ecc.
Hopefully in the future PHPUnit packages will get more stable and we will able to contribute to a single repository at the time.
ADP: Acyclic Dependencies Principle
A package dependency graph shall not contain cycles.
Mathematically speaking, it must be a Directed Acyclic Graph. Initially this principle was created for ease of building in languages with a one-time compilation step (C++, Java). However it's still a good practice to follow it even in PHP and other dynamic languages: if two packages are mutually dependent, even if the dependency involves only some classes, to make sure they do not explode at runtime you must always deploy them together.
Keeping two packages always together defeats the purpose of separate packages: again, you have to add duplicated work on keeping them separate while conceptually they are a unique package (or they need to be broken down in more segregated packages.)
The problem with mutual dependencies means that Zend_Form cannot be distributed without Zend_View, and Zend_View, which contains form element helpers, cannot be distributed without Zend_Form. This is a surprise for everyone using Zend Framework as a library, especially for people not using the ZF packageizer.
SDP: Stable Dependencies Principle
Stability of a package should be proportional to the number of dependencies on that package.
For example, you can pursue the goal of keeping the less stable packages up in the dependency hierarchy, so that their changes impact a little part of the rest of the system.
A simple target for stability can be the afferent coupling ca normalized by total package interactions (which is in turn the sum of afferent and efferent coupling ce). In classic formulation, ce/(ce+ca) is used which is 1 minus the metric cited before; so it's a measure of instability instead of stability which goes from 0 to 1.
SAP: Stable Abstraction Principle
The more abstract a package is, the more stable it should be.
Abstractness is the target of dependencies: implementations, inheritance, composition. So it should be stable, otherwise it would cause many other packages to break.
SAP and SDP are the equivalent of the Dependency Inversion Principle: dependency should be oriented towards abstractions.
A measure of abstraction for packages can be inferred from its classes. A class would be either abstract (interfaces also count) or not. A package abstractness can be measured as a 0 to 1 metric, like the number of interfaces and abstract classes over the total number of classes and interfaces (for normalization).
In current PHP frameworks, almost all packages that contain abstractions also contain many concrete classes, such as Zend_Auth, or Zend_Db. This is wonderful for easy of adoption, but the classes in those packages change at very different paces: interfaces like Zend_Auth_Adapter_Interface are frozen for all the 1.x branch, while the other classes can tune their implementation over different releases. There are no stable packages you can refer to, but only stable interfaces.
I'm not going to include the famous Main Sequence graph as it would only add some mathematics to the discussion. Here are the take home points for the packages principle:
- cohesion and coupling exist also for, and between, packages.
- when you depend even on a single particle of dust from a package, you depend on the whole of it.
- you can introduce abstraction-related packages which can be easily kept stable with respect to the rest of the library.
- packages boundaries (like the ones of Zend_Auth / Zend_Auth_Adapter) are often ignored, but with the introduction of namespaces and use statements the picture may change quickly as the external dependencies are exposed at the top of source files, making a bit hard to exit from the current namespace.