This is the first post in a series on the SOLID design principles for object oriented programming. The acronym SOLID represents five principles that are considered best practice for designing object oriented solutions. Robert Martin is credited with codifying these principles into this acronym. The five principles represented in the SOLID acronym are (1) the Single Responsibility Principle, (2) the Open-Closed Principle, (3) the Liskov Substitution Principle, (4) the Interface Segregation Principle and (5) the Dependency Inversion Principle. I have chosen to address the Liskov Substitution Principle first in this series, mainly because it is considered to be the most difficult to understand. My hope is that this post will show that it is, at its heart, a very simple concept and very useful in helping you design class hierarchies.

Before diving into the deep end, I think it’s beneficial to describe what the principle is trying to say in as simple terms as possible. It is actually such a simple concept that you may initially think, “that can’t be it”. I think this is why people get so confused, they look for something more when it is really as simple as follows. The Liskov Substitution Principle basically says that a SubType should actually be a true SubType of it’s parent. Meaning that a SubType should have the same characteristics and behaviors as it’s parent, if it does not then it isn’t a SubType.

The Definition

Now for the part that I think really confuses people, here is the official definition given for the principle: “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.” It is named the Liskov Substitution Principle because it was defined mainly by Barbara Liskov along with Jeannette Wing in a paper called “A behavioral notion of subtyping” that was published in 1994 in the ACM Transactions on Programming Languages and Systems (TOPLAS) journal.

So lets try and work out what the official definition is actually saying in laymen’s terms. Here is the definition with some color coding applied.

liskov definition

Here is the key to the color coded definition:

  • Red represents instances (aka Objects)
  • Blue represents a property of the (red) instance
  • Green represents Classes (aka types)

Explanation

So this is saying that if you have a characteristic that is found in an instance of a class (lets call it class T). Then that same characteristic should be found in an instance of a class (lets call it class S) that inherits from the first class (class T). For example, all Birds have wings. Therefore a seagull should have wings as well as a penguin and they both do. But, not all Birds fly a characteristic of some birds but not others. For example, both a seagull and penguin have wings but penguins don’t fly. And this brings to light a very important and interesting point. Modeling something in an OO fashion has its limitations when you are translating something from the real world to a programming paradigm. In the real world, both Penguins and Seagulls are Birds. But the Liskov principle says that it’s actually not a good idea to model a penguin and seagull both as subtypes of Bird. The reason for this is because programmatically a seagull and penguin have different characteristics and forcing a penguin to implement a fly() method would not make sense because Penguins don’t actually fly. At best, you would have to hack the fly method to make it work with the proposed class hierarchy. This closely relates to the Interface Segregation Principle which basically says that more small interfaces are better than a few large interfaces and gives us one possible solution to the anti-pattern demonstrated by the Liskov Substitution Principle.

Now this principle is a substitution principle. This means that the test for passing or failing the principle Is to substitute the parent type with the subtype and the behavior should be the same. A common example of this is the Rectangle and the Square where in the real world, a square is indeed a type of a rectangle but in the object oriented programming world that is actually not the case. This means that if you were to make square a subtype of rectangle and then change the length or the width, you would experience different behaviors in a rectangle vs. a square. The rectangle would simply adjust the width whereas the square would adjust the width and the height to keep it a square. This becomes really apparent when you look at a code example. Below I have provided some UML, java and unit tests that demonstrate this and thus show how inheriting from rectangle for square violates the Liskov Substitution Principle. Therefore, I have called the following code examples “BadRectangle”, “BadSquare”.

Bad Example UML

Here is the UML for the bad example:

liskov bad example uml

Bad Example Code (Java)

Here is the Java for the BadRectangle class:

public class BadRectangle {
	protected int width;
	protected int height;
	public BadRectangle(int width, int height) {
		this.width = width;
		this.height = height;
	}
	public int getWidth() {
		return width;
	}
	public int getHeight() {
		return height;
	}
	public void changeWidth(int aNewWidth) {
		this.width = aNewWidth;
	}
	public void changeHeight(int aNewHeight) {
		this.height = aNewHeight;
	}
	public int area() {
		return this.width * this.height;
	}
}

Here is the Java for the BadSquare which extends BadRectangle and is thus a subtype of BadRectangle. Notice that we have to override the changeWidth() and changeHeight() methods to change the behavior of square to adjust all the sides not just the length or the width. This is much like the fly() method I spoke about in the bird example:

public class BadSquare extends BadRectangle {
	public BadSquare(int aSide) {
		super(aSide, aSide);
	}
	@Override
	public void changeWidth(int aNewSide) {
		this.width = aNewSide;
		this.height = aNewSide;
	}
	@Override
	public void changeHeight(int aNewSide) {
		this.width = aNewSide;
		this.height = aNewSide;
	}
}

Now the Unit Tests for each. It is important to note that the BadSquareTest.java file extends BadRectangleTest. This is to make sure that our tests are truly evaluating the polymorphism present in our design such that the base type BadRectangle methods are applied to the more concrete type BadSquare.

class BadRectangleTest {
	protected BadRectangle r;
	@BeforeEach
	void setupTest() {
		r = new BadRectangle(4, 2);
	}
	@Test
	void testAreaCalculation() {
		Assertions.assertEquals(8, r.area());
	}
	@Test
	void testChangeWidthDoesntChangeHeight() {
		r.changeWidth(8);
		Assertions.assertEquals(8, r.getWidth());
		// Confirm that the Width has not changed
		Assertions.assertEquals(2, r.getHeight());
	}
	@Test
	void testChangeHeightDoesntChangeWidth() {
		r.changeHeight(6);
		Assertions.assertEquals(6, r.getHeight());
		// Confirm that the Height has not changed
		Assertions.assertEquals(4, r.getWidth());
	}
}
class BadSquareTest extends BadRectangleTest {
	@BeforeEach
	void setupTest() {
		this.r = new BadSquare(4);
	}
}

Bad Example Unit Test Results

The test results will show that the BadSquare violates the Liskov Substitution Principle because when the BadRectangle unit tests are run on a BadSquare they fail, but when they are run on a BadRectangle they pass. The substitution of the more concrete BadSquare does not work the same as its parent BadRectangle. The tests are specifically written to ensure that changing the length of the rectangle does not change the width and visa versa. Because a square’s width and length are always the same, they will change and the Rectangle tests will fail. Further, the area calculation failed because a Rectangle calculates area by multiplying length times the width whereas a square calculates area by multiplying aSideLength by aSideLenght. While this may not seem very different is it significant enough that unexpected behavior can result as these tests demonstrate.

liskov bad example unit test

So what’s the solution?

There are a couple of ways to solve this, some better than others and it depends on a number of different factors such as the language that you’re writing the solution in. In this case, it is really clear that in a programming sense a Square and a Rectangle are two different concepts entirely. The only similarity is that you can calculate the area() on both of them because they are a shape. So the solution that I’ve come up with for this example has an interface called IShape that contains a contract called area() and both the GoodRectangle and GoodSquare implement this interface. Otherwise, every other aspect of these two classes is independent from the other.

Good Solution UML

liskov good example uml

Good Solution Code (Java)

Here is the Java for the IShape interface which contains the area contract.

public interface IShape {
	int area();
}

Here is the Java for the GoodRectangle class which implements the IShape interface and thus overrides the area() contract with an implementation appropriate for a rectangle.

public class GoodRectangle implements IShape {
	private int width;
	private int height;
	public GoodRectangle(int aWidth, int aHeight) {
		this.width = aWidth;
		this.height = aHeight;
	}
	public int getWidth() {
		return width;
	}
	public int getHeight() {
		return height;
	}
	public void changeWidth(int aNewWidth) {
		this.width = aNewWidth;
	}
	public void changeHeight(int aNewHeight) {
		this.height = aNewHeight;
	}
	@Override
	public int area() {
		// TODO Auto-generated method stub
		return this.width * this.height;
	}
}

Here is the Java for the GoodSquare which also implements the IShape interface and thus overrides the area() contract with an implementation appropriate for a rectangle.

public class GoodSquare implements IShape {
	private int sideLength;
	public int getSideLength() {
		return sideLength;
	}
	public GoodSquare(int aSideLength) {
		this.sideLength = aSideLength;
	}
	public void changeSide(int aNewSide) {
		this.sideLength = aNewSide;
	}
	@Override
	public int area() {
		return this.sideLength * this.sideLength;
	}
}

Now the Unit Tests for each. Notice that the setupTest() method in both unit test classes initializes a Shape and casts that instance to a GoodRectangle/GoodSquare respectively. The shape is used to perform the area calculations on both the square and rectangle because that is the only thing that is common between them. Everything else is tested on the downcasted square or rectangle object directly:

class GoodRectangleTest {
	protected IShape shape;
	protected GoodRectangle goodRectangle;
	@BeforeEach
	void setupTest() {
		shape = new GoodRectangle(4, 2);
		goodRectangle = (GoodRectangle) shape;
	}
	@Test
	void testAreaCalculation() {
		Assertions.assertEquals(8, shape.area());
	}
	@Test
	void testChangeWidthDoesntChangeHeight() {
		goodRectangle.changeWidth(8);
		Assertions.assertEquals(8, goodRectangle.getWidth());
		// Confirm that the width is not changed
		Assertions.assertEquals(2, goodRectangle.getHeight());
	}
	@Test
	void testChangeHeightDoesntChangeWidth() {
		goodRectangle.changeHeight(6);
		Assertions.assertEquals(6, goodRectangle.getHeight());
		// Confirm that the height is not changed
		Assertions.assertEquals(4, goodRectangle.getWidth());
	}
}
class GoodSquareTest {
	protected IShape shape;
	protected GoodSquare goodSquare;
	@BeforeEach
	void setupTest() {
		shape = new GoodSquare(4);
		goodSquare = (GoodSquare) shape;
	}
	@Test
	void testAreaCalculation() {
		Assertions.assertEquals(16, shape.area());
	}
	@Test
	void testChangeSideLength() {
		goodSquare.changeSide(2);
		Assertions.assertEquals(2, goodSquare.getSideLength());
	}
}

Good Solution Unit Test Results

And here we see all the unit tests pass and everything works as expected.

liskov good example unit test

Conclusion

There are a lot of other factors that go into understanding the Liskov Substitution Principle. This spirit of this post is to try and help explain what it means in layman’s terms rather than be particularly pedantic on the “finer points”. Other areas to research would be pre-conditionspost-conditions and invariants as well as the robustness principle as they relate to and will probably deepen your understanding of what this post is about. I have put the Bad/Good Code Examples and Unit Tests on GitHub. You can use the following URL to get access to them (https://github.com/bradnjones/liskovSubstutionPrinciplePost). You will need to have Java installed (ideally using an IDE like Eclipse) and also be sure to have JUnit 5 installed. Be sure to look at the code found in the “src/com/brad/blog/lsp” folder on GitHub for the code/test examples. Keep an eye out for future posts in the SOLID series!

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *