What is the Liskov Substition Principle (LSP)? Well, honestly, it’s a lot of things and I’m not going to try and explain them all in this article because I’m not even sure I understand it all lol. However, the one part of it I get is this:
If T is S, then S should be swappable with T. Now that’s a paraphrase of an even more awkward explanation, and I’ve heard other people interpret this differently, but I’m going to discuss what it means to me. To me it simply means that you have be careful when sub-classing. When you override functionality, you have to make sure that you aren’t changing anything that would affect the object’s behavior if we thought this object was actually its parent. Here’s what I mean:
If a Square is a Rectangle, then why is it a Square? Well the difference is that a Square is simply a Rectangle that has equal width as it does length. Ok great. So can I change the width of the Square? Sure, but if you only changed the width and not the length, then it would be a Rectangle again, not a square. So as developers, we start thinking in terms of constraints. If we’re going to go through the hassle of making a Square class, then we’re going to ensure that it’s always playing by Square rules! Consider the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Rectangle { public virtual int Length { get; set; } public virtual int Width { get; set; } public Rectangle(int length, int width) { Length = length; Width = width; } public int GetArea() { return Length * Width; } } |
This rectangle is pretty basic. It has getters and setters for the length and width, and a function that calculates its area. So now let’s create that subclass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// this square implementation assumes that length and width // are always the same, so we override the set method to keep // them in sync public class Square : Rectangle { private int _length; public override int Length { get { return _length; } set { _length = value; _width = value; } } private int _width; public override int Width { get { return _width; } set { _width = value; _length = value; } } public Square(int width) : base(width, width) { } } |
Here’s we enforce our square rules (that length and width are always equal) by overriding the auto properties. Cool. Now any time we change the length, the width is updated to match and vice versa. And now since it inherits from Rectangle, we should be able to use our square anywhere that we are asked for a rectangle. Let’s see what happens:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Program { public static void Main() { var rect = new Rectangle(3, 8); var sq = new Square(8); GrowWidthBy5AndOutputArea(rect); // should output 3 * 13 (39) GrowWidthBy5AndOutputArea(sq); // should output 8 * 13 (104) but ends up outputting 13 * 13 (169) } public static void GrowWidthBy5AndOutputArea(Rectangle r) { // method takes a rectangle, so it assumes rectangle rules apply // rectangles allow you to change just the width r.Width += 5; Console.WriteLine("The area is now {0}", r.GetArea()); } } |
Here we create a method that accepts a rectangle. And since it takes a rectangle, we as developers would assume that Rectangle rules apply. As such, we attempt to grow one side by 5 and then calculate the area. But what happens? Well, polymorphism takes over, and the property override on the Width property sets the Length to the same value. This method didn’t know any better because it was working with a rectangle, not a square. So now we’re outputting inaccurate data!
This is a textbook example of a LSP violation. Can you find another? How do we fix this? Simple, square shouldn’t inherit from rectangle because it is not a rectangle in that it has a different set of rules. But why not fix the implementation of the Square’s length and width property? Well… then how do you know that the square object is in a valid state? Why bother if you can’t enforce it?
Here is a fiddle showing this example in motion: https://dotnetfiddle.net/hxPntB