Liskov Substitution Principle in Swift

Rodrigo Maximo
Movile Tech
7 min readOct 26, 2020

--

Third article of the series of five about SOLID and its use in Swift. The next article of this series can be found here, and it’s about the Liskov Substitution Principle.

Introduction

If you are a developer and you are interested in Software Engineering, you have probably already heard about a concept called SOLID.

In case you have never heard about it, do not worry, the first article of this serie has an introduction here.

Liskov Substitution Principle (LSP)

The original and mathematical definition from Barbara Liskov for this principle is

"If for each object o1 of type S there is an object o2 of type T, such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T."

it may be easier to understand, specially for people that are studying those principles for the first time, if we don’t look into that math definition. Another way and probably easier to state this could be

Programs that references an object from a base class must be able to use an object of a derived class without behavior differences and without knowing about its existence.

Remembering, in the previous article of this serie we talked about the Open-Closed Principle (OCP), that basically says that software entities should be opened for extension, but closed for modifications, what makes the code maintainable and reusable. Besides, to obtain those results, we should develop our code using abstractions, such as inheritance and interfaces.

So, in this article we're going to focus on inheritances for presenting the benefits of respecting the LSP. We’re going to show some examples that breaks this principle, and also show that when we don’t follow the LSP, we often break the OCP also. That’s why those principles can be co-related.

Examples in Swift

Example 1 — Unexpected Behaviors

Consider the following example, where we have the superclass Rectangle and a subclass Square. This is a really simple inheritance, in order just to demonstrate a LSP break, and the problems doing that.

Here we have a class Rectangle with two properties, width and height, and a method area() to calculate the area of the rectangle, based on the previous properties.

We also define the subclass Square, but with a small difference, we override those properties to guarantees that when setting each one of them, we assign the other one with the same value. This seems correct and nice, since we are guaranteeing squares would have the same value for width and height, what is definitely according to the square math definition.

Finally, imagine we consume those classes above as we do in the following main() function.

First, we define a square with sides value of 10. Then, and the tricky point of this example, we use from polymorphism to create a reference to this square, but from superclass Rectangle type. So now, we have an object of type Square that will be handled as an object of type Rectangle.

With that in mind, we assign the height of this rectangle to be 7, and then we set its width to be 5. Since we are manipulating a Rectangle type object (supposing we don’t know that in run time we are actually manipulating a square), the expected result for area would be 7 x 5 = 35. However, if the reader run this code, they’ll get 25 for the area after print() method being executed. And that’s why we’re breaking Liskov Substitution Principle, because we are not able to use a subclasse type object without different behaviors.

Example 1 — Problems in breaking the LSP

It may be a little hard to understand. The reader could think “I know it was a square object which had their properties set, and I know that a Square will have always their equal edges. So, at my opinion, the expected result should be 35 as executed, not 25, as it was mentioned”.

If you thought that, don’t worry, it’s totally normal. We’ll try to clarify why this is not right, and after that, why this could be a really huge problem in real cases.

The first point here is that we’re usually helped by polymorphism when we want to abstract things. For example, if we have a marketplace application, and in this application we have a screen that list all the items the user could buy. When implementing this, we’d probably have an abstraction that would group all the different items types.

Doing this would be really important to make easier when parsing the data from an API, for example. Also, abstract the different items would make the app agnostic from what could be sent from the server, and this means the server could change the response any time, with the app being still able to handle it without any problem.

What we’re trying to say is that if we get a single item and change some of their properties, or call one of its methods, we don’t know what item type we would be handling with. So, it would be really nice if nothing unexpected happen, right?

Now, coming back to our example, imagine that the rectangle is our single item, which we don’t know what type it can be, a square, a rectangle, or even another types, if we have it. The only thing we know about it is that it’s a rectangle. So, it’s fair enough that this object would behavior as a rectangle, right? And that’s where the problem in this example is. The object is not behaving as a rectangle.

Another really good example where breaking this principle can be really awful to us developers. For example, when we’re consuming a framework. We don’t want and we don’t need to know all the private structures this framework has, specially when using a public one. So, it would be really nice if we have expected behaviors for those public structures, which wouldn’t depend on our knowledge about the private ones.

Example 1 — Respecting the LSP

In order to respect the LSP in that example, we could implement it differently, as shown below.

Basically, if we prefer composition over inheritance, we’ll solve that problem in an easier way. Creating a protocol to centralize the same behavior both structures should have is going to guarantee that. When consuming them, we don’t use from properties or methods that we shouldn’t. And because of that, we won’t have unexpected behaviors.

Example 2 — LSP & OCP

Consider now this new example, where we have a Shape superclass, and two subclasses, Square and Circle. Basically, in this example, the Shape class works similar to an abstract class implementation.

Furthermore, we have a method draw(shape:), where we consume those classes and their methods of drawing. In this method we receive an object of type Shape, and we try to cast it as each one of our subclasses types, to be able then to draw it.

We’re breaking the LSP in this code because an object of subclass type Square behaves differently than an object of superclass type Shape, as a parameter of this method. If the method receives the child one, it draws, and if It receives the parent one, it doesn’t do anything. The same difference happens if the method receives objects of subclass type Circle in comparison with an object of superclass type Shape.

Also, it’s not the point of this article, but something interesting in this example is that it’s also breaking the OCP, because this method is not closed for modification if we decide to extend our types of shapes. For example, if we decide to have a Triangle, we would have to add an if statement in that method to be able to draw it.

Example 2 — Problems in breaking the LSP

It will not be required a so detailed explanation as in the first example, because basically the problems are really close. As developers, we don’t want to know everything other developers are implementing, and we definitely can’t do it. Here is the importance in having great interfaces between softwares.

A really important first step to accomplish is for sure developing software where private subclasses doesn’t have any different public behaviors in comparison to the public base class (or superclass). This goes to methods, classes, and also frameworks and APIs. And this is the main problem we have in the previous example.

Example 2 — Respecting the LSP

A possible way to make the second example to respect the LSP is creating a protocol with a common method draw().

This would avoid having different behaviors for an object of superclass type and an object of subclass type, solving the LSP breaking.

Also, this would make the draw(shape:) method closed for modification when extending new shapes. Because the new shape would have to implement the protocol, and so it would must have the draw() method because of that.

Overview LSP

  • This principle says Programs that references an object from a base class must be able to use an object of a derived class without behavior differences and without knowing about its existence.
  • Ensure this condition is positive because of:
  1. This will avoid other developers having to know private structures and their behaviors when consuming a framework, an API or any software entity.
  2. This for sure will help to reduce bugs, since unexpected behaviors are definitely synonymous of bugs.
  3. This will make other developers that will have to maintain or change some of your code lives easier.
  • This principle can be really co-related to the OCP, since when we break LSP, we often beak the OCP simultaneously. So, all the disadvantages read in the previous article of this series would be applied, what’s not really nice.
  • Finally, to avoid breaking this principle, the better recommendation is to prefer composition over inheritance for the abstractions. For sure it’s not a silver bullet, but we really believe it solves the most of cases.

References

  1. What is SOLID
  2. Who is Uncle Bob
  3. Robert C. Martin. Design Principles and Design Patterns, 2000

This was the third article of the series of five about SOLID and its use in Swift. I hope you have enjoyed and feel free to leave feedbacks, suggest improvements or even send me some messages.

--

--

Rodrigo Maximo
Movile Tech

Lead Mobile Engineer at Nubank |  iOS Engineer