Click here to Skip to main content
15,867,986 members
Articles / Programming Languages / C#

Liskov Substitution

Rate me:
Please Sign up or sign in to vote.
4.94/5 (43 votes)
26 May 2013CPOL15 min read 88.9K   36   79
A view on the principle including possible solutions to the Rectangle/Square example, also explaining why it is not a good example for real programming situations and presenting a more realistic example that you may find while programming.

Introduction

I am seeing a lot of articles talking about SOLID principles lately and one that is really getting my attention, because of the bad examples, is Liskov Substitution (also known as LSP).

The principle states that instances of a parent type should be replaceable by instances of sub-types without changing the correctness of the application. This can initially look silly as any sub-type instance can be given as the parameter to a method that receives instances of a parent type, but then there is the example intended to show how the principle gets violated: Rectangles and Squares.

Considering that inheritance means an is a relationship, the example says: A Square is a Rectangle (and so, it is a sub-class of Rectangle) and then it shows how we are breaking the Liskov Substitution, as for a Rectangle it is possible to change Width and Height independently, which is not valid for a Square.

The Bad Solutions

Most solutions I've seen lately say to create a base class, so both Rectangle and Square inherit from it, but they keep the independent Width and Height modifiable properties.

That's not a solution at all. First, the Square stops being a Rectangle and then the base class continues to have independent Width and Height properties, so such "solution" is only adding complexity (a new base type), is breaking the is a relationship between Square and Rectangle (and so it does not seem logical if compared to geometry) and keeps the problem that a Square shouldn't have independent Width and Height properties.

In fact, I think that the entire Rectangle and Square example is not meant to show a solution, only to show the problem.

Other Explanations to the Liskov Substitution

There are other explanations to the Liskov Substitution. Instead of saying that instances of a parent type should be replaceable by instances of sub-types without changing the correctness of the application, it is said that a sub-type can't change the behavior of a parent type.

Well, I should say that even if I do understand the idea of such sentence, it is not precise. Surely we can't change an expected logic (as this will make the program function incorrectly) but sub-types, in special method overriding, exist exactly to change a behavior. If we can't change a behavior at all, it is like saying we can only inherit to add new properties or methods, but that we can never override any method, which is clearly not the purpose.

But there is another explanation to the Liskov Substitution principle, which I consider better. Such explanation says that a sub-type should never have stronger pre-conditions or weaker post-conditions than the base type. OK, maybe the terms pre-conditions and post-conditions are not the easiest ones, but it means something like this:

  • If a method on the base type accepted null values as input, an overridden method on a sub-type should also accept null values. It can't simply start to throw exceptions saying null parameters are invalid as this will be a stronger pre-condition;
  • On the opposite side, if a base type method gave a guarantee that it will never return null, a sub-type can't override such method and return null. After all, anyone receiving the instance cast as the parent type considers that null will never be returned. An inheritor type still is a kind of the parent type, so it must still guarantee the same post-conditions (such post-condition is very common when returning collections, as in many situations it is expected to receive an empty collection instead of null).

How the Rectangle and Square Relate to the Principle?

The Rectangle gives us two properties, Width and Height and, if nothing else is told, we can assume they are independent properties. Here, we can say that we don't have an explicitly told guarantee but by simple logic, we can imagine that a property like TotalArea is calculated on something else (like Width and Height) but Width and Height seem completely independent of each other. Something that's not told, though, is if negative values are supported as in the real world, there aren't negative sizes (even the absolute zero is invalid, as that means the object does not exist) and in the programming world, those values are valid, but that's not the focus at this moment.

In my opinion, the Rectangle/Square situation is a "real world" comparison that simply fails in the OOP world. In special, the "is a" expression causes the problem. A Square is a Rectangle in geometry. Such is a expression is used to say that a sub-class is a kind of a base-class. But what happens if we have these two methods: DrawRectangle(Rectangle) and DrawSquare(Square) in static typed languages (C# is mainly a static typed language)?

Considering a Square is a Rectangle, we can call DrawRectangle() giving a Square object as parameter and this one works really fine in static typed OOP languages.

But now I ask you: Is a Rectangle that has equally sized Width and Height a Square?

If your answer is Yes (in geometry, it is), then a DrawSquare() could be called with a Rectangle instance where Width and Height are equal. But in a static typed language, that's not true. An instance of the Rectangle type is only a Rectangle, not a Square, even if it shares all the same properties of a Square (that is, equally sized Width and Height).

So, How Do We Solve the Problem?

My personal solution to this problem is: There should be only a Rectangle type. A Square is simply an observation of the properties of such Rectangle. So, a property like IsSquare will solve the problems if we compare it to how it works in Geometry.

That is: You can create a Rectangle of Width 100 and Height 200. The IsSquare will return false. Then, you can change Width to be 200 too. If you get the value of IsSquare, it will be true. Great!

So the code can be:

C#
public class Rectangle
{
  public int Width { get; set; }
  public int Height { get; set; }

  public bool IsSquare
  {
    get
    {
      return Width == Height;
    }
  }
}

But, by having only a Rectangle type, we can't create a method like DrawSquare(Square).

So, if we do a DrawSquare(Rectangle) that should only accept Square instances, it will still accept any Rectangle at compile-time. Of course, we can validate that at run-time (the same way we can validate if any parameter is null at run-time), but what if we want to solve the problem of a Square and a Rectangle using real types?

Solving the Problem with Real Types

Before presenting the possible solutions, I want to let it clear that I consider those solutions bad design. As just explained, any rectangle that has equally sized width and height is a square, so I consider it much simpler and logical to have a property that can tell if the actual Rectangle is also a Square or not.

But in static typed languages, we may want to give a compile-time guarantee that a parameter will have certain traits (in this case, that it will be a Square instead of being any kind of Rectangle). Even If I do know OOP situations that require similar compile-time constraints, I can't really say that I see real situations for the Rectangle/Square situation, especially because this seems to be a comparison to the real world geometry and, in the real world, we will never ask for a Square instead of a Rectangle without putting other requirements. For example: If we need to buy ceramic tiles to replace broken ones, we will ask for ceramic tiles of a specific size (for example, 10x10cm) with a specific color or texture. Yes, it is a square if the size is 10x10 but if I receive a tile with the same color/texture, square but with a size of 12x12 it will not work, so in the real world situation, we will validate the sizes even if the received object is a square. If we are already validating the right sizes, validating that it is a square is redundant.

But if we should make it work in OOP (maybe to say that we have found a solution and really understood the problem), OK, let's do it. But first, answer two questions:

  1. Do the types (Square and Rectangle) need to be modifiable?
  2. Is every Rectangle with equally sized Width and Height a real Square?

And accordingly to the answers, things can be solved differently (or they can't be solved at all).

So, for each combination of the answers, we have:

  • Need to be modifiable? No. Is a Rectangle(100, 100) a Square? No

    This is the easiest one: Make the types immutable (that is, properties are only set at creation time). The Square inherits from the Rectangle and in its constructor, it only receives a single parameter which it will use to fill both Width and Height of the Rectangle type. As you can't change the Width or Height later, everything will be OK, but if you do a new Rectangle(100, 100) instead of new Square(100), you will have a Rectangle that could be a Square, but it will not have the real Square type, so it will not be working exactly like in geometry.

    So, the code could be:

    C#
    public class Rectangle
    {
      public Rectangle(int width, int height)
      {
        // Validating if width and height are > 0
        // is optional for this example, but the
        // real classes will probably do such verification.
    
        Width = width;
        Height = height;
      }
    
      public int Width { get; private set; }
      public int Height { get; private set; }
    }
    public class Square:
      Rectangle
    {
      public Square(int size):
        base(size, size)
      {
      }
    }

    With this case, the following situations work:

    C#
    DrawRectangle(new Rectangle(100, 200));
    DrawRectangle(new Square(100));
    DrawSquare(new Square(100));

    While this will return false:

    C#
    Rectangle rectangle = new Rectangle(100, 100);
    return rectangle is Square;
  • Need to be modifiable? No. Is a Rectangle(100, 100) a Square? Yes.

    We still need the types to be immutable but we can't make the constructor of the Rectangle type public. We should always use a constructor method capable of creating a Rectangle (if sizes are different) or a Square (if sizes are equal). The constructor of a Square can be public as it does not risk being of another type. But note that even if the Rectangle.Create(100, 100) returns an instance of a Square, it will be returned as a Rectangle, so a cast to Square will be needed if you want to pass such Square to a method that statically requires a Square (in this case, I really consider a property IsSquare much cleaner than a cast).

    So, the code could be:

    C#
    public class Rectangle
    {
      public static Rectangle Create(int width, int height)
      {
        if (width == height)
          return new Square(width);
    
        return new Rectangle(width, height);
      }
      internal Rectangle(int width, int height)
      {
        Width = width;
        Height = height;
      }
    
      public int Width { get; private set; }
      public int Height { get; private set; }
    }
    public class Square:
      Rectangle
    {
      public Square(int size):
        base(size, size)
      {
      }
    }

    And then, all the following will work correctly:

    C#
    DrawRectangle(Rectangle.Create(100, 200));
    DrawRectangle(new Square(100));
    DrawSquare(new Square(100));
    
    Rectangle rectangle = Rectangle.Create(100, 100);
    bool isSquare = rectangle is Square; // this will be true.
    
    // But if we want to use this "square" as a real Square
    // we will need to cast it.
    DrawSquare((Square)rectangle);
  • Need to be modifiable? Yes. Is a Rectangle(100, 100) a Square? Yes.

    Impossible because if the user tries to create a Rectangle of Width and Height 100, he will receive a Square, then we will introduce a bug as the user will not be able to change the properties individually even if he asked to create a Rectangle, not a Square. Also, if he creates a Rectangle of Width 200 and Height 100 and later resizes the Width to 100, the object will continue to be of type Rectangle, as the static type can't change after the object is created.

  • Need to be modifiable? Yes. Is a Rectangle(100, 100) a Square? No.

    In this case, we need to make the Rectangle type somehow aware that it may be a Square (this is already a violation of the Single Responsibility Principle but not of the Liskov Substitution principle). A possible solution is to have a TrySetWidthAndHeight() method instead of letting both properties to be modified independently. This way, when we have a Square of Width and Height 100, we can replace both values to 200 in a single step, so there is no risk of receiving an exception when changing Width only (as in such case, the setter does not know if you will change Height or not) and there is no risk of Height being changed when it was not requested to change. We can say that a TrySetWidthAndHeight() solves the problem without breaking the Liskov Substitution because:

    • If we set equally sized Width and Height values when resizing, the Square can validate that it will continue to be a valid square before changing both properties (so it will never be corrupted) and;
    • As the method is a Try method, it lets clear that changing the value may fail. Some people may still argue that there is a change in behavior but as already explained, a change in behavior is valid as long as the pre-conditions are not stronger or the post-conditions weaker. It is always clear that a call to TrySetWidthAndHeight() may fail so, if it fails, well, it is expected.

    So, the code could be:

    C#
    public class Rectangle
    {
      public int Width { get; protected set; }
      public int Height { get; protected set; }
    
      public virtual bool TrySetWidthAndHeight(int width, int height)
      {
        Width = width;
        Height = height;
        return true;
      }
    }
    public class Square:
      Rectangle
    {
      public override bool TrySetWidthAndHeight(int width, int height)
      {
        if (width != height)
          return false;
    
        Width = width;
        Height = height;
        return true;
      }
    }

Note: There are other possible examples, like making a Rectangle and a Square completely independent or making the Square the base class (with only a Size property) and then making the Rectangle to be a "more versatile" Square, but in such cases, we will not keep the idea that a Square is a Rectangle so, even if it solves the programming problem, I don't think it is worth showing them as the whole idea is to make the classes represent the geometry shapes.

A More Realistic Example

I hope that my explanation up to this point is clear enough. I really hope that I am helping anyone that was lost with those Rectangle/Square examples to get a real idea of the problem and of the possible solutions, yet I think the entire situation is far from the problems we will find in real applications, so I think it is better to show an example that happens when actually programming applications.

So, let's see a real programming example that happens a lot: Controls that allow multiple child controls.

In WPF, the built-in controls that allow that are the layout controls. They don't have any special visual but they are responsible for presenting other controls in different manners. A Grid allows you to specify rows and columns sizes (be them fixed sizes, proportional sizes or "best fit" sizes), StackPanels simply put one control after the other, either horizontally or vertically and the Canvas allow you to specify the exact position of each control.

These 3 layout controls are based on the Panel class. You can see that they behave completely different from each other and if you stick to the idea that a sub-class can't change the behavior of a parent class, you may think that's a violation of the principle, but I should say that it is not. The Panel class does not give any guarantee on how items will be presented on the screen, which dependency properties must be filled for the controls to be correctly placed or the like. The only guarantee a panel gives is that it can contain multiple controls. We can say that if a method expects a Panel as a parameter, instead of expecting one of the more specific classes, that it expects only to access the already existing controls (for example, counting the controls, searching for a sub-control with a specific name or similar) and not that it is expecting to add a new control to be correctly visualized, as the Panel simply doesn't give a guarantee that the control will be presented at all.

But what if I decide to create a new control, based on a Grid or on a Canvas and I simply implement my own logic to display the inner controls, completely ignoring the already existing logic?

This happens a lot when developers don't understand the hierarchy of controls correctly, so instead of inheriting directly from the Panel class, they inherit from the Grid or Canvas class (I don't know why but these two are the most common base class that I see people using when generating their layout controls) and completely replace the layout logic.

So, a method expecting to fill a Canvas (adding many controls and setting their Canvas.Left and Canvas.Top properties) can receive an instance of that alternative layout control and the entire generated layout will be completely wrong because the child controls will not be positioned using the Canvas.Left and Canvas.Top properties (and may even be positioned using another attached Property that was simply not set).

That's a real violation of the Liskov Substitution. In this situation, we solve it by simply inheriting directly from Panel. So any method requiring a Canvas will not accept such new layout control (that's fine), any method that is only interested in listing the inner controls can still receive the new layout control as a Panel (that's fine too) and if there is a method that simply adds new childs to any Panel, well, we can say that such method must be corrected to expect a "minimum guarantee" (that is: a guarantee that all controls will be made visible accordingly by only adding them without setting any extra properties [that's the case of the StackPanel]).

Conclusion

My personal conclusion is that the principle is not hard to follow and that many programmers follow it naturally, until they see a bad example of the principle and get lost.

Most of the problems to understand it come from the fact that a sentence ("A Square is a Rectangle") is being translated directly as Square and Rectangle need to be classes and "is a" means inheritance when that's not the case. In fact, it is very rare to translate real life objects to the programming world as there are always mismatches and double interpretations and so it creates a problem in OOP and people usually try to solve the problem keeping the sentence, when to solve the problem it is easier to look at the problem differently and to understand the problem, it could be much simpler if real programming situations are presented and correctly solved.

A Final Note

People who already saw my advanced articles sometimes downvote my basic articles saying that I already did better, that I should talk about advanced topics. Well, I expect a basic article to be judged as a basic article as I am not saying it is an advanced article. The reality is that I write from inspiration and, as I think there is too much material giving false information about this specific topic, I really wanted to talk about it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions

 
GeneralRe: type morphing Pin
yetibrain10-Jun-13 4:20
yetibrain10-Jun-13 4:20 
GeneralRe: type morphing Pin
Paulo Zemek10-Jun-13 4:25
mvaPaulo Zemek10-Jun-13 4:25 
GeneralRe: type morphing Pin
yetibrain10-Jun-13 4:48
yetibrain10-Jun-13 4:48 
GeneralRe: type morphing Pin
Paulo Zemek10-Jun-13 5:16
mvaPaulo Zemek10-Jun-13 5:16 
BugA Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 2:25
professionalGustav Brock27-May-13 2:25 
GeneralRe: A Square is not a Rectangle in geometry. Pin
yetibrain27-May-13 3:20
yetibrain27-May-13 3:20 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 3:48
professionalGustav Brock27-May-13 3:48 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 4:00
mvaPaulo Zemek27-May-13 4:00 
I think that we can't use juridic documents as the base.
In brazilian laws, there are descriptions for computers that are like "a computer or a machine capable of executing binary instructions or a machine capable of executing high level algorithms".

So, is that or meaning a computer is not a machine capable of executing binary instructions? Of course not, but that or was put to make it clear that the law is applied to cellphones (for example) that aren't considered computers. If we remove the computers from the description the law will be the same, but not as "easy" to understand... so they first give one description and then they put other descriptions that are more generic.

So, when saying: be square or rectangular, they can say "be rectangular" but then somebody may say: Ah... but it was a square, not a rectangle (and the discussion about Is a Square a Rectangle will take place... so it is easier to put both in the description). Also, in the "or rectangular" they don't even say it must be a rectangle... it must be rectangular... this is already more generic than a rectangle.
GeneralRe: A Square is not a Rectangle in geometry. Pin
yetibrain9-Jun-13 23:59
yetibrain9-Jun-13 23:59 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek10-Jun-13 4:09
mvaPaulo Zemek10-Jun-13 4:09 
GeneralRe: A Square is not a Rectangle in geometry. Pin
yetibrain10-Jun-13 4:31
yetibrain10-Jun-13 4:31 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek10-Jun-13 4:51
mvaPaulo Zemek10-Jun-13 4:51 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 3:26
mvaPaulo Zemek27-May-13 3:26 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 3:55
professionalGustav Brock27-May-13 3:55 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 4:03
mvaPaulo Zemek27-May-13 4:03 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 4:28
professionalGustav Brock27-May-13 4:28 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 4:40
mvaPaulo Zemek27-May-13 4:40 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 5:05
professionalGustav Brock27-May-13 5:05 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 5:09
mvaPaulo Zemek27-May-13 5:09 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 6:16
professionalGustav Brock27-May-13 6:16 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 7:52
mvaPaulo Zemek27-May-13 7:52 
AnswerRe: A Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 8:34
professionalGustav Brock27-May-13 8:34 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 8:59
mvaPaulo Zemek27-May-13 8:59 
AnswerRe: A Square is not a Rectangle in geometry. Pin
Gustav Brock27-May-13 20:15
professionalGustav Brock27-May-13 20:15 
GeneralRe: A Square is not a Rectangle in geometry. Pin
Paulo Zemek27-May-13 23:02
mvaPaulo Zemek27-May-13 23:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.