Use of SOLID principles in tackling object-oriented design dilemmas: an example
There are 5 basic heuristic guidelines in object-oriented design. They will provide you with the solution to most of the problems you’re likely to encounter when designing your components, classes and interfaces. They are:
- Single Responsibility
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
The so-called S.O.L.I.D. principles of object-oriented design.
For this article, we shall be looking at two of them in more detail: the Open-Closed Principle and Liskov’s Substitution Principle. For more information on all 5 SOLID principles, see, for example, the Principles of OOD blog.
The Liskov Substitution Principle (Barbara Liskov, 1988)
Liskov’s Substitution Principle (LSP) states that if your application is using a base class, then you should be able to replace the base class with a subclass without changing the behaviour or functionality of your application. In other words, your application should be unaware that a subclass has replaced its parent base class.
The LSP gives you a test for subclasses. If your design assumes that S is a subclass of base class C, but substituting S for C leads to a change of behaviour, then you must reject the assumption that S is a subclass of C.
For example, your application may assume that a Kindergarten is a subclass of the base class School. However, if in your application the organization of a prom is included in the behaviour of a School, then LSP says you may have to reject Kindergarten as a subclass of School in your design.
The Open/Closed Principle (Bertrand Meyer, 1988)
This advises you to design your software unit (component, class or function) such that it is open for extension, but closed for modification. That means you should design such that users of the software unit cannot change it. The only way they should effect the changes they want is by extending the unit. That is, by specializing or subclassing it.
Suppose you violate the OCP, say, by letting N special users of your software unit modify its design. Then the resulting tailor-made software will become rigid, not easily adaptable to new users. Worse, it can become fragile and unpredictable. That is because new modifications made by some of the users will sooner or later conflict with the functionality others were already used to.
The usual way to apply the OCP is by implementing base classes as abstract classes or interfaces. The components representing business entities in your application are then defined as subclasses of the base abstract classes. As a result, the base classes will be closed for modification, but open for extension.
For example, consider the Report class in your employee-evaluation module. The employee as well as her supervisor submits a report.
In this design, you will want the Report class to be closed for modification. But how can you achieve this, given that the reports from employee and supervisor are different?
OCP comes to the rescue. According to the OCP, you should design Report as an unchanging abstract class or interface. Then derive from it subclasses EmployeeReport and SupervisorReport. Now, Report is open for extension but closed for modification. But we now have a dilemma.
According to LSP, we should be able to replace the class, Report, with either of the subclasses EmployeeReport or SupervisorReport without any side-effects. Obviously, we cannot do this because Report is an abstract class, whereas EmployeeReport and SupervisorReport are concrete classes. How do we get out of this design dilemma?
Answer: by making EmployeeReport and SupervisorReport abstract classes or interfaces, of course, not concrete classes!
This answer is in fact a suggestion of another of the SOLID principles, the Dependency Inversion Principle (Robert C. Martin, 1995). As Erich Gamma and others put it in their classic book, Design Patterns, “program to an interface, not an implementation”.