Understanding SOLID Principles: Liskov Substitution
7 oktober 2020
The principle relates to two main aspects of object-oriented programming. First of all, it highlights the importance of correlation between the classes with the same parent. Secondly, it helps to understand correctly the essence of object-oriented programming and virtual mechanisms, which support the process.
That one famous statement
Everything starts with that famous statement by Barbara Liskov:
Subtype Requirement Let F(x) be a property provable about objects x of type T. Then F(y) should be true for objects y of type S where S is a subtype of T.
In short, the method presented by the basic type indicator, should work correctly on the object even if the object is of the derived type.
Breaching the principle
The main breach of the above principle usually violates OCP principle at the same time – one type of indicator needs to be coded with the understanding of the other type of indicator. And it will have to know about the subsequent type of indicator during the system extension.
Let’s take another look at the class layout from the previous article about OCP.
With the collection of different objects deriving from PayCalculator, the Pay calculation with breach of LSP would look as follows: we would need to calculate the commission rate for each employee before calculating their salary (for example), and in the case of drivers, we would need additionally to re-calculate the kilometres they did in Germany, as it is crucial to calculate salary at no lower rate than at the minimum rate used in Germany.
In our example, the system for calculating the salary for all employees that would breach the LSP principle would have the following code:
public class PayCalculator
{
public void CalculatePay() { }
}
public class TraderPayCalculator : PayCalculator
{
public void CalculatePay() { }
public void CalculateCommissionThreshold() { }
}
public class DriverPayCalculator : PayCalculator
{
public void CalculatePay() { }
public void GermanWageRate() { }
}
public class B2BPayCalculator : PayCalculator
{
public void CalculatePay() { }
}
foreach (var employeeCalculator in employeeCalculatorList)
{
if (employeeCalculator is TraderPayCalculator)
(employeeCalculator as TraderPayCalculator).CalculateCommissionThreshold();
else if (employeeCalculator is DriverPayCalculator)
(employeeCalculator as DriverPayCalculator).GermanWageRate();
employeeCalculator.CalculatePay();
}
In such a case when using the basic object type, we can’t entirely use the functions which are designed for specific implementations of derived objects types. We cannot substitute the basic object type with the derived types. In .NET environment the above code would additionally generate a very serious error. The conjectural methods are not virtual. Therefore, the CalculatePay Method, from PayCalculator will be activated every single time. Only marking the method in the basic class as virtual, and in the derived classes as override will give the expected result.
The Amendment
The correct implantation should enable the use of code in the following way:
foreach (var employeeCalculator in employeeCalculatorList)
{
employeeCalculator.CalculatePay();
}
In this case, the derived class can be replaced by the basic class. Besides, the basic class does not need to know anything about its derived classes. When extending the system we don’t need to modify the code, which uses the basic class.
The objectivity in the real world
There is one more thing worth mentioning in the object- oriented programming, which relates to the substitution principle. The best way to illustrate it is to use the square and rectangle symbols. From math classes we know that the square is a type of rectangular – with the equal height and width. In the object-oriented programming the real world observation and the attempt to implement the class based on that observation, may lead to unexpected situations/behaviours. Let’s imagine we have a square and rectangle class:
public class Rectangle
{
public virtual int Height { get; set; }
public virtual int Width { get; set; }
public int Perimeter () => 2 * (Height + Width);
public int Surface() => Height * Width;
}
public class Square : Rectangle
{
}
What should be the correct behaviour in the case of the following code:
public void Test()
{
Rectangle square = new Square();
square.Height = 5;
square.Width = 7;
Console.WriteLine(square.Perimeter());
}
Even if the implementation of the Square class will be changing its height while attributing the width or changing its width while attributing the height, will the above code be clear and readable? In this particular case the natural correlation between the height and the width of the square will not be taken into account during the inheritance process. The classes have their individual features but the same functionality to calculate the surface area and the perimeter. Look at the picture below:
A few words about the architecture
Discussed principle points the direction towards which the dependencies in IT systems should be built. Base element, which is a specific interface for derivative elements cannot be dependent on them. Each time when the base element has to „know” the details of implementing derivative elements in the system this rule will be violated and its development will be difficult and error prone. This principle works both in case of base and derivative classes as well as in case of any other elements of the system. Let’s imagine the situation in which, for instance, Google API depends on the implementation of the systems that depend on it.
The summary
The substitution principle explains that when the class correlations are programmed, the basic classes won’t need to know anything about their derived classes, especially when they work on the objects of the derived classes. This principle helps to understand the importance of the virtual methods, and always works with the methods related to the object type, stored by the object, not by the object indicators. When programming the hierarchy of the class in the system we should be aware of the risks during the real world implementation into classes, as the code that is created might be unreadable and counterintuitive.
Other SOLID principles:
Single Responsibility
Open-Closed
Interface Segregation
Dependency Inversion