SOLID – (LSP) The Liskov Substitution Principle

Previous post in this series:
SOLID – (OCP) The Open-Closed Principle

FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.

The importance of this principle becomes obvious when you consider the consequences of violating it. If there is a function which does not conform to the LSP, then that function uses a pointer or reference to a base class, but must know about all the derivatives of that base class. Such a function violates the Open-Closed principle because it must be modified whenever a new derivative of the base class is created.

The Liskov Substitution Principle, Robert C. Martin

To me a part of the LSP is about understanding the subtleties that result of a language trying to handle OOP. The LSP seems obvious, code should be able to use a base class without any need of knowing about any derived classes. This post will cover a subtle and specific violation of the LSP that happens in C++, C# and other OOP languages. As with other code in this series it derives from Uncle Bob’s source material, porting his C++ to C#.

To get started, let’s begin with the idea of a “Is-A” relationship. In OOD generally we define parent/child relationships with a “Is-A”. For instance, a car is a vehicle. This means that vehicle would be the root or parent object and car would be a child. Uncle Bob’s source material (as noted in the citation above) uses “a square is a rectangle” relationship to show violations of the LSP.

Example 1: Square and Rectangle, a More Subtle Violation (based on source material)

public class Rectangle
{
	public double Width { get; set; }
	public double Height { get; set; }
}

public class Square : Rectangle
{
	public new double Width
	{
		get { return base.Width; }
		set
		{
			base.Width = value;
			base.Height = value;
		}
	}

	public new double Height
	{
		get { return base.Height; }
		set
		{
			base.Width = value;
			base.Height = value;
		}
	}
}

The code above is pretty straight forward. A square is a rectangle, right? Therefore a square should inherit from a rectangle. But a square is a special kind of rectangle. Whereas a rectangle can have any length to it’s width or height, a square must have the same width and height. To ensure that, when setting the height and/or width the square class will make sure that everything is even. But since Width and Height aren’t virtual, we have to use the “new” keyword on the Width/Height in square. Let’s throw together some quick code to test our squares.

Example 1a: Testing squares and rectangles.

public class TestRectangles
{
	public void Test()
	{
		Square s = new Square();
		s.Width = 1; //Height and Width set to 1
		s.Height = 2; //Height and Width set to 2

		ExampleF(s);
	}

	public void ExampleF(Rectangle Rectangle)
	{
		Rectangle.Width = 32;
	}
}

In the beginning of the Test method everything goes as expected. In the ExampleF method is where things fall apart. Setting the width in the ExampleF, even when a square is passed in, will only set the width. In Rectangle, because Width/Height is not marked as virtual, they cannot be inherited. To make sure you understand the consequences of overriding a value that is not marked as virtual, the C# compiler will issue a warning and tell you to use the “new” keyword so you are aware of what you are doing. So extending the base class without having Height/Width as virtual violates the LSP since a square is not really a substitution for a rectangle. The TestRectangles class would have to know that a square is a square to ensure that Width/Height remain the same.

Fortunately this is an easy fix, let’s identify Width/Height as virtual so we can substitute a square anytime we would use a rectangle.

Example 2: Square and Rectangle, The Fix (based on source material)

 public class Rectangle
{
	public virtual double Width { get; set; }
	public virtual double Height { get; set; }
}

public class Square : Rectangle
{
	public override double Width
	{
		get { return base.Width; }
		set
		{
			base.Width = value;
			base.Height = value;
		}
	}

	public override double Height
	{
		get { return base.Height; }
		set
		{
			base.Width = value;
			base.Height = value;
		}
	}
}

There is nothing above that any of us hasn’t run into. We know that it’s really easy to violate the LSP when we “new” properties, so we fix the issue by marking Width/Height as virtual and truly override the properties. But, does this really make sense? Consider if I was a new software engineer and I didn’t really understand the subtleties of the LSP. Let’s add a method to our test class that anyone of us might write.

Example 2a: Testing squares and rectangles

public class TestRectangles
{
	public void Test()
	{
		Square s = new Square();
		s.Width = 1; //Height and Width set to 1
		s.Height = 2; //Height and Width set to 2

		ExampleF(s);
		ExampleG(new Square());
	}

	public void ExampleF(Rectangle Rectangle)
	{
		Rectangle.Width = 32; //Height and Width set to 32
	}

	public void ExampleG(Rectangle Rectangle)
	{
		Rectangle.Width = 5;
		Rectangle.Height = 4;

		Debug.Assert(Rectangle.Width * Rectangle.Height == 20);
	}
}

Here we have added “ExampleG”. The developer of this method made an assumption that I think any of us would. We should be able to set the width and height of a rectangle and get the area expected. But in this case the Assert will throw. So we’re back to a violation of the LSP where an operation that works on a Rectangle will not work on a Square.

Validity is not Intrinsic

This leads us to a very important conclusion. A model, viewed in isolation, can not be meaningfully validated. The validity of a model can only be expressed in terms of its clients. For example, when we examined the final version of the Square and Rectangle class in isolation, we found that they were self consistent and valid. Yet when we looked at them from the viewpoint of a programmer who made reasonable assumptions about the base class, the model broke down.

Thus, when considering whether a particular design is appropriate or not, one must not simply view the solution in isolation. One must view it in terms of the reasonable assumptions that will be made by the users of that design.

The Liskov Substitution Principle, Robert C. Martin

So what is the solution? Uncle Bob discusses that the issue here is that when considering the “Is-A” relationship you need to consider the behavior of the objects. If we’re going to treat child classes as the parent classes we need to understand the behavior of the objects. In the case of our Square, it does not behave like a Rectangle. By the literal definition a square is a rectangle. But for all practicality that doesn’t work.

Now, stepping away from the source material, we can force things to work by utilizing immutability, making it obvious to any users exactly how the object operates.

Example 3: Squares and Rectangles

public class Rectangle
{
	public double Width { get; private set; }
	public double Height { get; private set; }

	public Rectangle(double Width, double Height)
	{
		this.Width = Width;
		this.Height = Height;
	}
}

public class Square : Rectangle
{
	public Square(double Side)
		: base(Side, Side) {}
}

The above code works in that it allows us to treat a Square as a Rectangle. The LSP is held up. The problem here is that we’ve done it by removing any behavior. Remember that the problem with the original code was that a square behaved differently then a rectangle. So why not do this? Well, I have to fall back to the quote from above. Working in isolation were we rigidly apply the rules of OOD can actually lead to cases where we violate them. Through your own understanding and experience of the rules of OOD as well as those around you, you have to know when to ignore the rules on the paint can. In this instance there may very well be a reason to implement the code as it is. I can also come up with with several cases where this rigidity can cause future problems.

Uncle Bob’s source material discusses utilizing “Design By Contract” where we can define pre- and post- conditions that would have fallen apart with our samples above. It also goes into a a real example from Uncle Bob about where he had a problem with the LSP and actually ended up with a rather hackey solution that violates the LSP. As with all these posts covering the SOLID principles, I would encourage you to read the source material to get a bigger-picture understanding of the SOLID principles.

Thanks,
Brian

Leave a Reply