This blog post is an adaptation of an answer I posted to Programmers.Stack Exchange. You can find the original post here.
The Liskov Substitution Principle is one of the five major OO design principles described by the SOLID acronym. Quite simply, it says that when we design our class hierarchy, child objects should behave like their parent objects.
An easy way to conceptualise the LSP is to imagine a scenario where the LSP is violated. Consider the following:
public class Rectangle
{
public virtual double Length { get; set; }
public virtual double Width { get; set; }
public double CalculateArea()
{
return Length * Width;
}
}
public class Square : Rectangle
{
private double length;
private double width;
public override double Length
{
get
{
return length;
}
set
{
width = value;
length = value;
}
}
public override double Width
{
get
{
return length;
}
set
{
width = value;
length = value;
}
}
}
When we are taught inheritance at university, we are taught to consider whether one object is another object. If so, we should use inheritance to describe the relationship. If so, the above code makes sense. However, it violates the Liskov Substitution principle.
Consider the following problem:
Sally has been given a group of Rectangles and has been asked to increase their area by 10%. So she decides that the least work to solve the problem is to set the length of the rectangle to 1.1 times what it was before.
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
rectangle.Length = rectangle.Length * 1.1;
}
}
Now in this case, all of Sally’s rectangles now have their length increased by 10%, which will increase their area by 10%.
Sally’s unit tests pass because she wrote all of her unit tests to use a collection of rectangles (the least derived type applicable for this function).
Now this is all fine and dandy until someone passes this method a mixture of squares and rectangles. Doing so will introduce a subtle bug into the application which can go unnoticed for months.
Worse still, Jim from accounting sees Sally’s method and writes some other code which uses the fact that if he passes squares into the method, he gets a very nice 21% increase in size. Jim is happy and nobody is any wiser.
Jim gets promoted for excellent work to a different division. His accounting code gets lost in the anals of the sourcesafe archives, which are no longer maintained as the company has since moved to git for its source control. Alfred joins the company as a junior. In his first bug report, Jill from Advertising has reported that passing squares to Sally’s method results in a 21% increase and wants the bug fixed. Alfred sees that Squares and Rectangles are used everywhere in the code and realises that breaking the inheritance chain is now impossible without effecting hundreds of different methods and classes. He also does not have access to Accounting’s source code, or even know that it exists. So Alfred fixes the bug like this:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
if (typeOf(rectangle) == Rectangle)
{
rectangle.Length = rectangle.Length * 1.1;
}
if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
Alfred is happy with his uber hacking skills and Jill signs off that the bug is fixed.
Next month nobody gets paid because Accounting was dependent on being able to pass squares to the IncreaseRectangleSizeByTenPercent
method and getting an increase in area of 21%. The entire company goes into “priority 1 bugfix” mode to track down the source of the issue. They trace the problem to Alfred’s fix. Poor Alfred nearly gets fired. Alfred knows that he has to keep both Accounting and Advertising happy. So under orders from his manager, he fixes the problem by identifying the user with the method call like so:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
IncreaseRectangleSizeByTenPercent(
rectangles,
new User() { Department = Department.Accounting });
}
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
foreach(var rectangle in rectangles)
{
if (typeOf(rectangle) == Rectangle || user.Department == Department.Accounting)
{
rectangle.Length = rectangle.Length * 1.1;
}
else if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
And so on and so forth.
Eventually the code gets very ugly, very messy and becomes extremely brittle. All because the initial design created the Square as a type of Rectangle.
This anecdote is based on heaps of real-world code that’s currently out there. Violations of the Liskov Substitution principle can introduce very subtle bugs that only get picked up years after they’re written, by which time fixing the violation will break a bunch of things and not fixing it will anger your biggest client because you have to say “no” to the new functionality that they wanted delivered in three weeks.
There are two realistic ways of fixing this problem.
The first way is to make Rectangle immutable. If the user of Rectangle cannot change the Length and Width properties, this problem goes away. If you want a Rectangle with a different length and width, you create a new one. Squares can inherit from rectangles happily. This would be done by injecting the length and width values into the rectangle object through its constructor. The Square object can then call the Rectangle’s constructor and pass the side length in as both length and width.
The second way is to break the inheritance chain between squares and rectangles. If a square is defined as having a single SideLength
property and rectangles have a Length
and Width
property and there is no inheritance, it’s impossible to accidentally break things by expecting a rectangle and getting a square. In C# terms, you could seal
your rectangle class, which ensures that all Rectangles you ever get are actually Rectangles.
In this case, I like the “immutable objects” way of fixing the problem. The identity of a rectangle is its length and width. It makes sense that when you want to change the identity of an object, what you really want is a new object. If you lose an old customer and gain a new customer, you don’t change the Customer.Id
field from the old customer to the new one, you create a new Customer
.
Violations of the Liskov Substitution principle are common in the real world, mostly because a lot of code out there is written by people who are incompetent/ under time pressure/ don’t care/ make mistakes. It can and does lead to some very nasty problems. In most cases, you want to favour composition over inheritance instead.
What a fantastic article!
It has implications for Test, as well as Dev, because it demonstrates how reliance on test-coverage tools may cause a test escape. Both statement/line and decision based coverage could show 100% coverage and yet a very real bug may not been found! This is where the test author needs an understanding of the code implementation (IE. What it is supposed to do) and/or perhaps Test being involved early (IE. A technical tester doing code-review may spot that using the rectangular area increaser for a square could cause issues; pointing this out to Sally – ‘Hang on Sal, Square inherits from this and as the 10% would be 21% for a square….’)
Thanks Stephen – good morning grey-cell exercise 🙂
Hi Mat,
Thanks for your comments!
Yes this has got testing implications, particularly if you are exposing your libraries to third parties. As I mentioned in the article, Microsoft seal almost every class in the .NET framework so that they can be sure that a violation of the LSP won’t cause a bug in one of their inbuilt libraries.
I tend to prefer composition over inheritance for most tasks anyway, as it provides a much cleaner application which is easier to test.
-Stephen
You need more posts! Some of the best explanations of SOLID principles I’ve found. Keep it up… people are reading
+1 for what Richard said. People are reading. I Like your posts and I want to read more from you. Keep it up.
I have every intention of updating this blog again soon. The last 6+ months have been crazy busy with a number of real life disruptions.
First time I have seen the implications of not following the Liskov Substitution Principle explained in such detail. Really drives home the point. Keep it up
?? But where is then the solution of the right way to do it?
Hi there Niels,
In the third and fourth final paragraphs I identified two solutions to the problem – break the inheritance chain (i.e. don’t derive Square from Rectangle) or create immutable objects (i.e. don’t allow people to set the values of the length and width of a rectangle after the object has already been constructed).
You would need a third class to give you a new Rectangle if you wanted to implement the IncreaseRectangleSizeByTenPercent method (maybe it could be a ShapeFactory class).