Definition
Liskov substitution principle (LSP) has the most difficult definition of five SOLID principles. Especially if we look at the original definition which is practically mathematics:
Subtype Requirement: Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T. (Barbara Liskov)
That is something I can’t figure out, especially that it has something to do with object-oriented programming. Robert C. Martin has modified it to a more user-friendly version:
Derived classes must be substitutable for their base classes. (Robert C. Martin)
But this doesn’t either make any sense of what LSP really is. Luckily LSP is much easier in practice. I will start by violating LSP because that is easier than to start by explaining what it is.
Example: Violating LSP
Consider we have an IBird
interface with a Fly
method:
public interface IBird { void Fly(); }
But when we try to code a Penguin
class, we can’t implement the Fly
method:
public class Penguin : IBird { public void Fly() { throw new NotSupportedException("Penguins can't fly"); } }
This is a quite typical violation of LSP. This was a really simple example but think if we have a good class and we want to extend its behavior. We inherit it (like Penguin
above) and code something more. But then we realize that something that we inherited doesn’t fit anymore and we throw NotSupportedException
.
LSP is often violated by attempts to remove features. (Mark Seemann)
(This example is based on Tom Dalling’s blog post about LSP and Mark Seemann’s Pluralsight course about SOLID principles.)
Easier Definition
LSP defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. (Thorben Janssen)
Now if we look at the IBird
and Penguin
example again with this more reasonable definition, we will understand what LSP is. I will explain how the example violated this.
If we have the following method:
public void LetThemFlyAway(IBird[] birds) { foreach (var bird in birds) bird.Fly(); }
And we have a Penguin
object in birds array it will throw a NotSupportedException
. Penguin
object, which is the subclass of IBird
, will break the application. If IBird
and Penguin
were coded according to LSP it wouldn’t throw an exception.
Practically what LSP says is that if we use superclass in our code, we should be able to replace it with its subclass, an application will still run successfully.
LSP requires the objects of your subclasses to behave in the same way as the objects of your superclass. (Thorben Janssen)
Desing by Contract, and We Will Follow LSP
If it feels difficult to write code that follows LSP, try to follow design by contract. In brief (and little simplified) design by contract means that methods have:
- Preconditions for input parameters and
- postconditions for output parameters.
For input parameters, this means that subclass has to accept the same input parameters in the overridden method as superclass accepts in its method. We can make validation looser in subclasses but we are not allowed to make it more strict. It would violate LSP like previous Penguin
example and throw an exception if some subclass won’t accept some input that superclass accepts.
We should think output parameters as a return value. The rule is basically the same as with the inputs: the return value of subclass’ method should follow the same rules as the return value of superclass’ method. Unlike with inputs we are not allowed to loosen the rules with outputs. But we can make them more strict if we want. Loosening output requirements also would violate LSP. Think that we have a superclass with the following method:
public class MySuperClass { public virtual int GetValue() { var random = new Random(); return random.Next(1, 10); // returns 1...9 } }
And then we have a method that uses it to get the sum of numbers:
public int CalculateSumOfValues(MySuperClass[] myClasses) { int sum = 0; foreach (var myClass in myClasses) sum += myClass.GetValue(); return sum; }
Remember that MySuperClass
expects that return value of GetValue
method is between 1 and 9. With three input objects, the return value of CalculateSumOfValues
method should be a value between 3 and 27. This is the postcondition contract of the CalculateSumOfValues
method. If we now create a subclass that violates design by contract and loosens return value:
public class MySubClass : MySuperClass { public override int GetValue() { var random = new Random(); // Returns 0...9 even if design by contract says should return 1..9. return random.Next(0, 10); } }
Now if we call CalculateSumOfValues
method with three MySubClass
objects, it will return a value between 0 and 27. That breaks the postcondition contract of the CalculateSumOfValues
method which says that the return value should be between 3 and 27. And we have also violated LSP! So if we follow design by contract, we can’t loosen postcondition requirements.
But vice versa we can make return values more strict. For example, if we change MySubClass
‘s GetValue
method to return a value between 2 and 8 it wouldn’t be a problem. Because then CalculateSumOfValues
would return a value between 6 and 24 and that fits into return value’s requirement to be between 3 and 27.
If we follow these two rules when overriding methods in a subclass, our code probably follows LSP.
Relationship With the Open/Closed Principle
While writing this blog post I have been thinking that isn’t LSP practically same as the open/closed principle (OCP)? My opinion is that they are quite similar but they underline different things. OCP is more about extending classes and LSP is more about using objects. If you want to read a deep analysis why LSP and OCP are not same I suggest you read this question in Software Engineering StackExchange.
What Makes LSP Important?
At first glance, LSP seems little difficult and unpractical. The definition isn’t easy to understand. And even if we see code example it is still not easy to understand. It needs some time to understand it.
How can it be so important that it has its own letter in SOLID principles? The reason is that it makes it more easy to use objects because we can trust that they behave as they should. If our code follows LSP, we can change to use any subclass of the given superclass and be sure that our code still works as it is supposed to. This is because of LSP guarantees that every object of subclasses behaves just like superclass.
Sources and Influences
- Design by contract (Wikipedia).
- Design By Contract by David Stotts in Team Software Engineering course.
- Encapsulation and SOLID by Mark Seemann in Pluralsight course.
- SOLID Class Design: The Liskov Substitution Principle by Tom Dalling in his blog.
- SOLID Design Principles Explained – The Liskov Substitution Principle with Code Examples by Thorben Janssen in stackify.com.
- The Principles of OOD by Robert C. Martin in butunclebob.com.
5 thoughts on “Liskov Substitution Principle (SOLID 3/6)”