Most of the OO developers should have heard about the principals in OOD: Open-Close, Liskov Substitution, Dependency Inversion, Interface Segregation, and Single Responsibility. Last weekend, I spent two days on reading and thinking of these principals.
This article contains my summarization about these principals.
1. The Open-Close Principal
I think the open-close principal is the basis of the other principals. This principal is at the heart of many of the claims made for OOD.
As Martin said, the modules that conform to the open-close principal have the following two primary attributes:
1. Open For Extension. This means that the behavior of the module can be extended. The module behavior can be changed to new and different ways as the requirements of the application change, or to meet the needs of new applications.
2. Closed for Modification. The source code of such a module is inviolate. No one is allowed to make source code changes to it.
In this principal, the abstraction is the key. Using abstraction can gain explicit closure. It’s possible to create abstractions that are fixed and yet represent an unbounded group of possible behaviors, which is represented by all the possible derivative classes. If a module manipulates an abstraction, that module can be closed for modification since it depends upon an abstraction not the implementation detail.
At the end of Martin’s article, he mentioned that conformance to this principle isn’t achieved simply by using an object-orient language; rather, it requires a dedication on the part of the designer to apply abstraction to those parts of the program that the designer feels are going to be subject to change.
Actually, the primary mechanisms behind the open-close principle are abstraction and polymorphism.
2. The Liskov Substitution Principle
In the object-orient language, one of the key mechanisms that support the abstraction and polymorphism is inheritance. By using inheritance, we can create derived classes that conform to the abstract interfaces, which is the key of this principle. According to the Martin’s article, the definition of this principle is:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
If there is a function that doesn’t conform to this principle, it definitely violates the open-close principle, because it must be modified whenever a new derivative of the base class is created. This principle provides guidance for the use of public inheritance.
In his article, it gives us an example of square and rectangle. Square class inherits from Rectangle class, this seems valid. But if there is a method that takes a reference to one Rectangle object as parameter:
void Func(Rectangle& r)
If we pass a reference to Square object to this method, it shows us the problem since the width and height should be same for square. This leads us to a very important conclusion. A model, view in isolation, can’t be meaningfully validated. The validity of a model can only be expressed in terms of its clients. Thus, when considering whether a particular design is appropriate or not, one must not simply view the solution in isolation. One must view it in terms of the reasonable assumptions that will be made by the users of that design.
This principle makes clear that in OOD the ISA relationship pertains to behavior. Not intrinsic private behavior, but extrinsic public behavior; behavior that clients depend upon. It’s important when we decide whether or not one class should inherit from another class.
And when redefining a method in a derivative classes, their behaviors and outputs must not violate any of the constraints established for the base class. Users of the base class must not be confused by the output of the derived class.
3. The Dependency Inversion Principle
In traditional software development methods, such as Structured Analysis and Design, tend to create software structures in the way that high level modules depend upon low level modules, and the abstractions depend upon details. Indeed one of the goals of these methods is to define the subprogram hierarchy that describes how the high level modules make calls to the low level modules. But when the lower level modules are changed, they force the high level to change. And it’s hard to reuse the high level modules, because they depend upon the low level modules.
A design is bad if it exhibits any or all of the following three traits:
1. Rigidity. It’s hard to change because every change affects too many other parts of the system.
2. Fragility. When you make a change, unexpected parts of the system break.
3. Immobility. It’s hard to reuse in another application because it can’t be disentangled from the current application.
Conforming to dependency inversion principle can solve these issues. Any module conform to this principle, it has the following requirement.
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
In this principle, the high level modules only depend upon the abstractions of the low level modules, when the implementations of the low level modules is changed, the high level modules don’t need to change, since the abstractions of low level modules are fixed. And the high level modules are independent of the low level modules, and then the high level modules can be reused quite simply. It’s impossible that a module that doesn’t conform to this principle comply with open-close principle, since any change to the low level module can force the module to change.
4. The Interface Segregation Principle
The interface segregation principle deals with the disadvantages of “fat” interfaces. Classes that have “fat” interfaces are classes whose interfaces aren’t cohesive. In other words, the interfaces of the class can be broken up into groups of member functions. Each group serves a different set of clients. Thus some clients use one group of member functions, and other clients use the other groups.
This principle acknowledges that there are objects that require non-cohesive interfaces; however, it suggests that client should not know about them as single class. Instead, clients should know about abstract base classes that have cohesive interfaces.There is an example in his article. TimedDoor class extends Door class, which extends TimerClient class, as the following class diagram.
Each time a new interface is added to the base class, that interface must be implemented in the derived classes. Actually, there is an associated practice that is to add these interfaces to the base class as nil virtual methods rather than pure virtual methods; specifically so that derived classes are not burdened with the need to implement them. This solution can lead to maintenance and reusability problems. When a change in one part of the program affects other completely unrelated parts of the program, the cost and repercussions of changes become unpredictable. And the risk of fallout from the change increases dramatically.
ISP provides us a correct solution to this problem:
Clients should not be forced to depend upon interfaces that they don’t use.
When clients are forced to depend upon interfaces that they don’t use, then those clients are subject to changes to these interfaces. It results in an inadvertent coupling between all the clients. In order to avoid such coupling, we need to separate the interfaces. In Martin’s article, he provides us two ways: separation through delegation and separation through multiple inheritances.
4.1 Separation through delegation
We can employ the Adapter pattern to the TimeDoor problem.
In this diagram, there is a new class named DoorTimerAdapter, whose responsibility is to delegates the message from Timer to the TimedDoor. This solution prevents the coupling of Door clients to Timer. But it involves the creation of a new object. It’s better to use this solution when the Adapter needs to translate the message.
4.2 Separation through multiple inheritances
In this solution, the TimedDoor extends from both Door and TimerClient.
I prefer to use this solution. The structure is more meaningful. TimerClient class and Door class take separate responsibilities; TimedDoor class combines these two responsibilities to complete the work.
5. The Single Responsibility Principle
Each responsibility is an axis of change. When the requirements change, that change will be manifest through a change in responsibility among the classes. If a class assumes more than one responsibility, that class will have more than one reason to change.How can two responsibilities be separated correctly? That depends on how the application is changing. If the application changes in ways that cause the two responsibilities to change to change at different times, then these two should be separated; otherwise, they are changed at the same time, there is no need to separate them. There is conclusion here. An axis of change is an axis of change only if the change occurs. It’s not wise to apply this principle or any other principle, for that matter if there is no symptom.