Solid is an adjective often associated with a desirable internal quality that a software product should manifest: its ability to be resilient to changes. The adjective has been also used as acronym to identify a family of principles conceived as hallmark of proper object-oriented design.
Unfortunately a principle is neither a design pattern nor a formal rule, so it is not always easy to follow it in practice. At least in software development, principles are often rule of thumbs followed only by those that are able to recognize them. This recognition is almost always based on a certain degree of subjectivity and experience. Consider, for example, the Single Responsibility Principle (SRP): the first principle in the SOLID family. The SRP is formulated as:
A class should have only a single responsibility.
Other ways to state the same tenet are:
Only one potential change in the software’s specification should be able to affect the specification of the class.
Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
A class should have only one reason to change.
Every redefinition is quite abstract in some way. What is a responsibility? What is a reason to change? Moreover, the application of the same principle in a certain situation by different designers can produce different results, similarly to what happens with design patterns. In an article about software coupling in object-oriented systems, I describe an application of the SRP which starts from the same context described by Robert Martin (the author of SRP), but deviates somewhat in the solution compared with that of Martin. Of course, it is not the intent, nor the general form of the solution to change. It is only the presence of minor details such as the level of scale (the principle is applied to hierarchy level, not to a single class level) and the presence of implementation details (an adapter that transform the internal representation of one entity for suitable use in another) to apparently change the form of the solution.
From these considerations, I start to think about SOLID principles in terms of a general structure that describes both the context in which they originate and the result of their application. In other words, I start to looking for a canonical form. Admittedly, this endeavor does not eliminate the degree of abstraction/subjectivity concerned to the meaning of terms in each principle’s definition. Nonetheless, having something like a canonical form is useful for :
- better understanding the structure of the problem (the context in which the problem materializes and the forces that constrain it);
- better understanding the structure of the solution.
I hope these two aspects can help to mitigate the level of generality and subjectivity intrinsic in the formulation of such principles. The presence of a concrete example depicted in both the initial context and the solution should reinforce the intent.
I start in this post describing the “canonical form” of the SRP. The context from which the SRP originates is that of a “heavy-duty” class that implements many responsibilities (at least two independent ones, as illustrated in the Component A-B of Figure 1). The coupled implementations are usually undesirable because a change in one can produce a domino effect in the other, and vice versa. The heavy-duty module becomes fragile.
The solution is to separate responsibilities in different modules, trying to reduce the coupling between the corresponding implementations. Most of the time it is very difficult to make each module totally isolated, so we need to prepare for a certain degree of coupling. In Figure 1 I have isolated one module (Duty-A class), keeping the second one dependent from the former (Duty-B). Total isolation of Duty-A enable selective reuse and testing. For what concerns Duty-A, every accidental coupling is vanished. The residual coupling in the Duty-B class could be alleviated by implementing a service adapter that takes the Duty-A implementation and transform it to the most suitable representation for the purposes of Duty-B. This adapter should be placed in the Duty-B class in order to shield Duty-A from such (non local) knowledge, as discussed in the example. Some designers, however, may prefer to implement the adapter service directly in the Duty-A class, avoiding the duplication of services. In such case, the adapter plays also the role of factory method for the internal representations. Both solutions are preferable to the initial context.
An example of the SRP application is described in the above-mentioned article. Here I recap the most important features. The example context is summarized in Figure 2. A single entity, C2dShape, plays both the roles of geometric and graphic shape. The accidental coupling comes from the option to share the same representation for coordinates in both type of primitives (graphic and geometric ones). However, the forces constraining the context are different in the two cases. Graphic shapes are constrained by the use of an external graphic engine which works with 16 bit precision, requiring a suitable internal representation (every coordinate implemented as float, for example). Geometric shapes implement precise maths algorithms and then should be implemented with high-precision internal representations (e.g. double or even more “large” data types). The Point2D class represents the coordinates shared by every “heavy-duty” shape. In this case, the coordinates are chosen in order to better suit the needs of the graphic engine (adopting an internal representation based on doubles would have a severe impact of the overall graphic efficiency).
The solution resulting from the application of SRP can be illustrated in Figure 3. Here the coordinates are saved in each geometric shape as double numbers, then are transformed in the graphic shapes as float numbers. The transformation can be done only the first time the service is invoked for each primitive (the transformation is time-consuming), but these are implementation details of the graphic engine. Each geometric entity ignores such details. What matters here is that changes in the formats in the graphic entities have local impact: the geometric entities are not affected. The SRP is applied in two steps:
- dividing the C2dShape entity in two abstractions more focused: Graphic Shapes (and its subclasses) for graphic entities, and Geometric entities, each paired with a correspondent graphic subclass;
- decouple the geometric abstractions from the graphic ones (there is a link only in the reverse direction).
A little note of “micro-design”: the degree of asymmetry between the graphic entities compared with the geometric ones (only the former are based on a hierarchy) is motivated by the asymmetric use of polimorphic behavior. Only in the graphic domain tasks like: “for each shape in the scene do shape.draw()” have sense. No such general use is conceived in the geometric domain for the application under development, so the hierarchy is not implemented there.
In the next installment of this series of articles about SOLID principles, I will investigate the canonical form of the Open-Closed principle.