Sitemap
Better Programming

Advice for programmers.

Mastering Software Engineering in iOS: Liskov Substitution Principle

LSP: Software development foundations easily explained

6 min readApr 30, 2020

--

Press enter or click to view image in full size
Photo by Daniel Korpai on Unsplash

SOLID

Let’s move on to the third SOLID principle called Liskov substitution principle or LSP for short. As with previous articles, I will stick to the same convention:

  • I’ll give a simple definition
  • I’ll give an example of a bad code, which in some way breaks discussed the principle
  • I’ll explain why the principle is broken in the given example
  • I’ll refactor the code, so the example complies with the discussed principle
  • I’ll explain why now it complies with it

This time we will even write some simple unit tests to check if our code is conforming to LSP. If you never wrote them, don’t worry, I’ll guide you through the whole process.

Liskov substitution 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.”

- Barbara Liskov

Wait, what? While this sound complicated it’s actually really simple. Robert Martin’s paraphrase (that abandons formal language for the sake of clarity) hits the bull’s-eye.

Derived classes must be substitutable for their base classes.

- Robert C. Martin

All this principle is saying, is that it should always be possible for the subclass to be used the same way as the base class (with more or less the same effect).

The simplest example would be a Dog subclass of Animal base class, and a function that allows you to pet any animal ( pet(animal: Animal)). You should always be able to use this function on both Animal class and Dog class. And with inheritance and polymorphism, it automatically does work correctly. You could say that this is Object-Oriented Programming 101.

So why should you even care about the principle that all modern programming languages conform to by default?

The thing is: you may change the internal logic of the subclass so much, that it doesn’t work as a substitute for the base class anymore. It will compile and work on a code level, but it will provide bad and unexpected results, which is actually far worse than a simple compile error.

To picture that we will use the two most popular examples and one example from our own UIKit backyard.

Example 1: Overused Duck Example

This is by far the most common explanation of the problem. Let’s make a Bird base class and some subclasses:

Now let’s try out how will our birds behave:

EmperorPenguin broke the LSP as a derived class ( EmperorPenguin) must be substitutable for their base ( Bird), yet it isn't.

You can't use fly(birds:allBirds, to:) with EmperorPenguin class. The code compiles and runs, but it simply doesn't do what it promised to do. That's why our sanity-checking assert crashed the application in runtime.

The main mistake we made was the assumption that every bird flies. That is simply not a correct depiction of the real world. We need to remember that the base class should have only the behaviours (methods) that every sublass will share and be able to use correctly.

In Swift we can repair this mistake by using protocol-oriented programming:

Now we see that compiler will instantly inform us if we tried to make fly some birds that simply cannot do that.

In this implementation, the Bird base class does not assume behaviors that are not shared among every existing type of a bird.

It simply assumes that every bird is "somewhere" (in our simplified world model, there are only two places they can be) and that it has some number of wings (assigning 2 by default, as every bird that is not extinct apparently has one pair of wings).

If an object is both a Bird and conforms to a Flyable protocol, it automatically gets the default implementation of fly(to city:) function.

This way we have the best of both worlds: We don't have to repeat code and implement fly(to city:) for each Bird subclass separately, and yet we do not break the LSP.

Example 2: In programming, square is not a rectangle

Here we will write the promised unit test.

Open Xcode, create a Single View App and make sure that you leave Include Unit Tests checkbox checked. Let’s name the project LearningLiskov:

Press enter or click to view image in full size

Once you have that, create a new Swift file named SquareExample.swift that we will use to create our example. Implement the rectangle class here:

And now to create unit tests for that class, open LearningLiskovTests.swift and replace its content with the following code:

Now press ⌘+U or select Product ➡ Test and after a while you should see this:

Our simple test has passed successfully.

Let’s go back to SquareExample.swift and create a Square, that will inherit from Rectangle (because according to math Square is a subtype of Rectangle).

The first thing we will see is that the Square doesn't have both x and y. It only has x. But we inherit from the class that does have y! To handle this, we would need to write something like this:

This way whenever anybody will change the length of any side of the Square, it will change all the other sides as well.

Let me remind you that according to LSP “derived classes must be substitutable for their base classes”, so provideTestObject() should be able to provide any Rectangle or it's subtype and the test should pass!

So just change the provideTestObject() to return Square() and play tests (⌘+U).

You will see that test failed:

XCTAssertEqual failed: ("100") is not equal to ("50")

As it turns out we changed the behaviour of the Sqare class so much, it is not substitutable for Rectangle. It broke the Liskov substitution principle. And there is no easy fix here.

Square should not inherit from Rectangle

Inheritance in object-oriented programming means that subclassed object will get both properties (class variables) and behaviours (functions and things like didSet) of the base class.

Behaviours of Square (changing either x or y will change both x and y) directly contradicts the behaviour of Rectangle (changing x or y, changes only themselves), so we can't substitute one with the other.

We simply cannot model rectangle and square using inheritance without breaking the Liskov substitution principle.

That’s the key thing that makes this example so unintuitive: In object-oriented programming, when you say that “X is Y” (as in “Square is a rectangle”), you mean inheritance and that comes with a very strict meaning that is not always the same as in the real world.

For this example, it would be best to use structs and make the x and y constant values. Structs don't have the inheritance so we instantly avoid the LSP problem. We'd straight away see that there should be separate tests for both and that both structs have different constructors (you only need one side in the square!).

Example 3: UIKit's own "duck"

I̵n̵ ̵U̵I̵K̵i̵t̵ ̵f̵r̵a̵m̵e̵w̵o̵r̵k̵ ̵U̵I̵S̵t̵a̵c̵k̵V̵i̵e̵w̵ ̵i̵s̵ ̵b̵r̵e̵a̵k̵i̵n̵g̵ ̵L̵S̵P̵ ̵b̵e̵c̵a̵u̵s̵e̵ ̵o̵f̵ ̵t̵h̵e̵ ̵b̵a̵c̵k̵g̵r̵o̵u̵n̵d̵C̵o̵l̵o̵r̵ ̵p̵r̵o̵p̵e̵r̵t̵y̵.̵ ̵T̵h̵i̵s̵ ̵p̵r̵o̵p̵e̵r̵t̵y̵ ̵w̵o̵r̵k̵s̵ ̵f̵o̵r̵ ̵t̵h̵e̵ ̵b̵a̵s̵e̵ ̵c̵l̵a̵s̵s̵ ̵(̵ ̵U̵I̵V̵i̵e̵w̵)̵ ̵a̵n̵d̵ ̵y̵e̵t̵ ̵d̵o̵e̵s̵n̵’̵t̵ ̵f̵o̵r̵ ̵t̵h̵e̵ ̵d̵e̵r̵i̵v̵e̵d̵ ̵c̵l̵a̵s̵s̵ ̵(̵ ̵U̵I̵S̵t̵a̵c̵k̵V̵i̵e̵w̵)̵.̵ ̵U̵I̵S̵t̵a̵c̵k̵V̵i̵e̵w̵ ̵s̵i̵m̵p̵l̵y̵ ̵i̵g̵n̵o̵r̵e̵s̵ ̵t̵h̵e̵ ̵b̵a̵c̵k̵g̵r̵o̵u̵n̵d̵ ̵c̵o̵l̵o̵r̵ ̵w̵h̵i̵c̵h̵ ̵i̵s̵ ̵i̵n̵ ̵c̵o̵n̵t̵r̵a̵d̵i̵c̵t̵i̵o̵n̵ ̵o̵f̵ ̵A̵p̵p̵l̵e̵’̵s̵ ̵o̵w̵n̵ ̵d̵o̵c̵u̵m̵e̵n̵t̵a̵t̵i̵o̵n̵ ̵f̵o̵r̵ ̵t̵h̵i̵s̵ ̵p̵r̵o̵p̵e̵r̵t̵y̵.̵

EDIT: They’ve fixed it, so let me give another example: UINavigationController is inheriting from UIViewController but you can’t use the pushViewController(_:animated:) function on UINavigationController instance. If you try, this will happen:

Press enter or click to view image in full size
Apple breaking the LSP

To Wrap Up

Liskov substitution principle is one of the easiest to follow, and yet, once broken it’s one of the most difficult to fix.

When subclassing and especially when extensively using override (without calling the same method from super) think twice if you are not breaking the LSP with this particular subclass. If you will keep that in mind, you should be fine!

--

--

Adrian Zyga
Adrian Zyga

Written by Adrian Zyga

Software Engineer 👷‍♂️, iOS Developer 📱, Swift enthusiast 🐦, gamer 🎮.

Responses (2)