Robert C.Martin wrote an interesting article about a set of metrics that can be used to measure the quality of an object-oriented design in terms of the interdependence between the subsystems of that design.
Here’s from the article what he said about the interdependence between modules:
What is it that makes a design rigid, fragile and difficult to reuse. It is the interdependence of the subsystems within that design. A design is rigid if it cannot be easily changed. Such rigidity is due to the fact that a single change to heavily interdependent software begins a cascade of changes in dependent modules. When the extent of that cascade of change cannot be predicted by the designers or maintainers the impact of the change cannot be estimated. This makes the cost of the change impossible to estimate. Managers, faced with such unpredictability, become reluctant to authorize changes. Thus the design becomes rigid.
And to fight the rigidity he introduces metrics like Afferent coupling, Efferent coupling, Abstractness and Instability.
The number of types outside this project that depend on types within this project.
The number of types outside this project used by types of this project.
The efferent coupling and afferent coupling could be applied also on namespaces and types. For example the efferent coupling for a particular type is the number of types it directly depends on. Types where TypeCe is very high are types that depend on too many other types. They are complex and in general have more than one responsibility.
The ratio of the number of internal abstract types (i.e abstract classes and interfaces) to the number of internal types. The range for this metric is 0 to 1, with A=0 indicating a completely concrete project and A=1 indicating a completely abstract project
A = Na / Nc
A = abstractness of a module
Zero is a completely concrete module. One is a completely abstract module.
Na = number of abstract classes in the module.
Nc = number of concrete classes in the module.
The ratio of efferent coupling (Ce) to total coupling. I = Ce / (Ce + Ca). This metric is an indicator of the project’s resilience to change. The range for this metric is 0 to 1, with I=0 indicating a completely stable project and I=1 indicating a completely instable project.
I = Ce/(Ce + Ca)
I represent the degree of instability associated with a project.
Ca represents the afferent coupling, or incoming dependencies, and
Ce represents the efferent coupling, or outgoing dependencies
Abstractness vs Instability Graph and the zone of pain
Here’s as example the Abstractness vs Instability Graph of the C++ POCO libray
The idea behind this graph is that the more a code element of a program is popular, the more it should be abstract. Or in other words, avoid depending too much directly on implementations, depend on abstractions instead. By popular code element I mean a project (but the idea works also for packages and types) that is massively used by other projects of the program.
It is not a good idea to have concrete types very popular in your code base. This provokes some Zones of Pains in your program, where changing the implementations can potentially affect a large portion of the program. And implementations are known to evolve more often than abstractions.
The main sequence line (dotted) in the above diagram shows the how abstractness and instability should be balanced. A stable component would be positioned on the left. If you check the main sequence you can see that such a component should be very abstract to be near the desirable line – on the other hand, if its degree of abstraction is low, it is positioned in an area that is called the “zone of pain”.
How to fight the rigidity when using the OOP approach?
As Robert C Martin wrote in its article, we have to use the abstract classes and the interfaces to make our projects more flexible and reduce the high coupling between the code elements.
The coupling in OOP could be introduced by:
– Inheritance: Which is overused when choosing the OOP paradigm and unfortunately in many cases it makes the code more rigid. Some design patterns are useful to resolve the rigidity introduced by the inheritance like the Adapter pattern which minimize the rigidity introduced by the inheritance.
– Using directly concrete implementation: In this case the code become also rigid because it’s difficult to change the code if we needed to use another library or framework for some reason. Like inheritance there are some design patterns to minimize the rigidity like the Bridge or the Proxy.
When using the OOP approach it’s recommended to master the GOF structural pattern, they help to fight the rigidity introduced by the coupling. However you have to add more interfaces , classes and indirections just to resolve the rigidity issues. However using more classes and indirections comes with a price, the project become more complicated.
Generic programming to the rescue
Here’s what say Andrei Alexandrescu about the Modern C++ design:
Modern C++ Design defines and systematically uses generic components – highly flexible design artifacts that are mixable and matchable to obtain rich behaviors with a small, orthogonal body of code.
Two assetions are interesting in his point of view:
- Modern C++ Design defines and systematically uses generic components.
- highly flexible design.
Indeed using the generics reduces the coupling between the code elements, no need that a class A uses a concrete implementation B or even an abstract class IB, but it could uses a pointer which it’s kind is specified by a template param.
Generic programming sound more natural and flexible, and it can provides more possibilities than OOP, however many developers found it very complex and difficult to learn and use.
- The code became illisible and hard to maintain.
- The compiler errors are very hard to understand.
To resume the generic programming resolve the rigidity issue but comes also with a price.