Introduction
This post is a continuation from High Cohesion & Low coupling using SOLID Principles -- Part 1.
Liskov Substitution Principle [LSP]
The Liskov Substitution Principle states subtypes must be semantically substitutable for their base types.
In other words, it means that consuming any implementation of a base class should not change the correctness of the system.
Hopefully even in more simpler words, you should be able to replace any object with a sub class of that object and have all of the code still function properly.
LSP helps in loose coupling by allowing any code that uses subclass - objects does not need to know about the what & where part of these objects.
The benefits of this principle really shine when applied along with the Open/Closed principle. If a class is extended into multiple sub-classes, you can still use the same code with the main class within these new classes. This way, you prevent dependencies and increase the ability to re-use code.
Simple Example
A simple example of LSP is a square and a rectangle issue.
Let us say you have to calculate the area of square and rectangle. For calculating the area of a rectangle, you will need a height
and a width
. And for square, you need only one coordinate that is length
.
Mathematically thinking, all the squares are rectangle, but not all rectangles are square. Since a square “is a” rectangle, you could fall into a trap to create a rectangle as a base class for square. But what happens when you try to change the height or width of a square? Can you? No, you cannot. A square should have same length for all the four coordinates.
So as per LSP, If you try to inherit from rectangle to create a square, you end up changing the semantics of height and width (I mean overriding the code of properties or methods) to account for this. Or in other words, to demonstrate square is always a rectangle you might end up changing the semantics of superclass or subclass and making them non substitutable -- thus violating the LSP. -- Refer square - rectangle image below.
To solve this issue, one may use a simple IShape
abstraction and each rectangle and square implement this IShape
.
How LSP Helps Achieve Low Coupling and High Cohesion
An object inheriting from an abstraction must be semantically substitutable for the original abstraction. Now any code that uses this object need not care about the what part and where (this object is passing from) part to provide loose coupling. Also, a module designed in LSP principle is very easily integrated in a cohesive manner.
Another Illustration
Let's consider an implementation for the same. Let's say we have 3 classes: Super Class, Sub class (inheriting from Super Class ) and a Demo class (test harness).

SuperClass
contains 2 decimal fields: n1
& n2
along with a constructor and a Multiply
method to calculate n1*n2. SubClass
inherited from SuperClass
and contains a constant value to be used in Multiply
method - Demo Class contains
Go
method and a test harness Main
method Main
method first invokes GO
method with a superclass object and displays the result & then exactly the same operation with SubClass
object.
As of now, it is simple little inheritance example with no hint of LSP. (Refer to the image & code below.)
public class SuperClass
{
internal decimal n1;
internal decimal n2;
public SuperClass(decimal num1, decimal num2)
{
this.n1 = num1;
this.n2 = num2;
}
public virtual decimal? Multiply()
{
return this.n1 * this.n2;
}
}
public class SubClass : SuperClass
{
const decimal constVal = 1.1m;
public SubClass(decimal num1, decimal num2) : base(num1, num2) { }
public override decimal? Multiply()
{
try
{
return base.Multiply() * constVal;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
}
public class Demo
{
public static decimal? GO(SuperClass b)
{
return b.Multiply();
}
public static void Main()
{
SuperClass b = new SuperClass(-5, 20);
decimal? result = GO(b);
Console.WriteLine(result);
SubClass d = new SubClass(-5, 20);
decimal? result2 = GO(d);
Console.WriteLine(result2);
Console.Read();
}
}
So far so good with a trivial example.
But what if I introduce a condition to SubClass
-- Multiply
method. Could a small change of a condition break the semantics & thus violate LSP? Let's check this other code snippet below:
public class SubClass : SuperClass
{
const decimal constVal = 1.1m;
public SubClass(decimal num1, decimal num2) : base(num1, num2) { }
public override decimal? Multiply()
{
try
{
if (this.n1 < 0)
throw new InvalidOperationException("Number n1 cannot be negative");
return base.Multiply() * constVal;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
}
public static void Main()
{
SuperClass b = new SuperClass(-5, 20);
decimal? result = GO(b);
Console.WriteLine(result);
SubClass d = new SubClass(-5, 20);
decimal? result2 = GO(d);
Console.WriteLine(result2);
Console.Read();
}
In this, the overridden method adds an additional condition to check if field n1
is negative and if so, it throws an exception. Essentially, what happens now is the same old Main
method is used against this new overridden Multiply()
, you would recognize that instead of printing result for SubClass
it would throw an exception whereas Multiply
from SuperClass
would still execute properly. This additional condition required makes the base class and derived class not interchangeable anymore and therefore the Liskov Substitution Principle is violated.
But when you develop your derived class and specifically when the virtual method is redefined, you must pay attention to some situations, if they occur, involving the violation of the Liskov substitution principle. Specifically, a general rule to keep in mind is that a derived class should *not* require additional conditions or either provide fewer conditions than those required by the base class.
N.B.: This is just one of the examples of violating LSP. LSP could be violated in many other ways as well.
************************I will publish my Part-3 sometime day after tomorrow **********************