Why declare an instance as a supertype but instantiate it as a subtype, plus Liskov Substitution Principle

1.2k views Asked by At

I've been trying to understand the Liskov Substitution Principle for a couple of days now, and while doing some code tests with the very typical Rectangle/Square example, I created the code below, and came up with 2 questions about it.

Question 1: If we have a superclass/subclass relationship, why would we want to declare an instance as the supertype but instantiate it (new it up) as the subtype?

I understand why, if we're doing polymorphism through interfaces, we would want to declare and instantiate variables this way:

IAnimal dog = new Dog();

However, now that I recall about it in old programming classes and some blog examples, when using polymorphism through inheritance, i'd still see some examples where some code would declare a variable this way

Animal dog = new Dog();

In my code below, Square inherits from Rectangle, so when I create a new Square instance this way:

Square sq = new Square();

it still can be treated as a Rectangle, or added to a generic List of Rectangles, so why would someone want to still declare it as Rectangle = new Square() ? Is there a benefit I'm not seeing, or a scenario where this would be required? Like I said, my code below works just fine.

namespace ConsoleApp
{
class Program
{
    static void Main(string[] args)
    {
        var rect = new Rectangle(300, 150);
        var sq = new Square(100);
        Rectangle liskov = new Square(50);

        var list = new List<Rectangle> {rect, sq, liskov};

        foreach(Rectangle r in list)
        {
            r.SetWidth(90);
            r.SetHeight(80);

            r.PrintSize();
            r.PrintMyType();

            Console.WriteLine("-----");
        }


        Console.ReadLine();
    }

    public class Rectangle
    {
        protected int _width;
        protected int _height;

        public Rectangle(int width, int height)
        {
            _width = width;
            _height = height;
        }

        public void PrintMyType()
        {
            Console.WriteLine(this.GetType());
        }

        public void PrintSize()
        {
            Console.WriteLine(string.Format("Width: {0}, Height: {1}", _width, _height));
        }

        public virtual void SetWidth(int value)
        {
            _width = value;
        }

        public virtual void SetHeight(int value)
        {
            _height = value;
        }

        public int Width { get { return _width; } }
        public int Height { get { return _height; } }
    }

    public class Square : Rectangle
    {
        public Square(int size) : base(size, size) {}

        public override void SetWidth(int value)
        {
            base.SetWidth(value);
            base.SetHeight(value);
        }

        public override void SetHeight(int value)
        {
            base.SetHeight(value);
            base.SetWidth(value);
        }
    }
}

}

Even though this should be breaking the Liskov Substitution Principle, I get the following output:

"Width: 90, Height: 80

ConsoleApp.Program+Rectangle

Width: 80, Height: 80

ConsoleApp.Program+Square

Width: 80, Height: 80 ConsoleApp.Program+Square

Question 2: So, why or how would this code sample be breaking the LSP? Is it only because of the Square invariant of all sides being equal breaks the Rectangle invariant that sides can be modified independently? If that's the reason, then the LSP violation would be theoretical only? Or how, in code, could I see this code breaking the principle?

Edit: Came up with a third question which came up in one of the LSP blog articles I was reading, and there was no answer so it's this

Question 3: The Open-Closed principle states that we should introduce new behavior/functionality through new classes (inheritance or interfaces). So if for example, I have a WriteLog method in the base class, which has no preconditions, but I introduce a new subclass which overrides the method but ONLY actually writes to the log if the event is highly critical....if this is new intended functionality (precondition being hardened on the subtype), would that still be breaking the LSP? The two principles would appear to contradict one another in this case.

thanks in advance.

2

There are 2 answers

8
Bert F On BEST ANSWER

Question 1: If we have a superclass/subclass relationship, why would we want to declare an instance as the supertype but instantiate it (new it up) as the subtype?

The reasons you would do this with a supertype are the same reasons you would do for with an interface. All the reasons you list for why declaring the variable as its specific subtype rather than the supertype apply equally as well for why declaring the variable as its specific subtype rather than an interface that the subtype implements.

abstract class Car { ... }
public abstract class ToyotaCamery2011 extends Car ( ... )

class Garage {
    private Car car = new ToyotaCamery2011();
    public Car getCar() { return car; }
    ....
}

class Garage {
    private ToyotaCamery2011 toyotaCamery2011 = new ToyotaCamery2011();
    public Car getCar() { return toyotaCamery2011; }
    ....
}

As long as all the methods of Garage only use methods of Car, and the public interface of Garage only shows Car and nothing specific to Prius2011, the 2 classes are effectively equivalent. Which is more readily understandable, e.g. which one models the real world more closely? Which ensures I don't accidentally use a Prius-specific method, i.e. built a Prius-specific garage? Which is just the slightest bit more maintainable when if I decide to get a new car? Is the code improved in any way using the specific subtype?


Question 2: So, why or how would this code sample be breaking the LSP? Is it only because of the Square invariant of all sides being equal breaks the Rectangle invariant that sides can be modified independently? If that's the reason, then the LSP violation would be theoretical only? Or how, in code, could I see this code breaking the principle?

Its difficult to talk about LSP without talking about promises/contracts. But Yes, if Rectangle promises that sides can be modified independently (more formally, if a postcondition for calling Rectangle.setWidth() includes that Rectangle.getHeight() should be unaffected), then Square deriving from Rectangle breaks LSP.

Your program does not depend on this property, so its fine. However take a program that is trying to satisfy a perimeter value or area value. Such a program may rely on the idea that Rectangle has independent sides.

Any class that accepts a Rectangle as input and depends on this property/behavior of Rectangle will likely break when given a Square as input. Programs like this can either jump through hoops to look for and disallow a Square (which is knowledge of a subclass) or it can change the contract of Rectangle with respect to independent sizes. Then all the programs that use Rectangle can check after every call to setWidth() or setLength()to see whether the adjacent side also changed and react accordingly. If it does the latter, thanSquarederiving frmoRectangle` is no longer a violation of LSP.

Its not just theoretical, it can have real impact on software, but it is often compromised upon in practice. You see this in Java often unfortunately. Java's Iterator class provides a remove() method that is optional. Classes that use iterator must have knowledge about the implementing class and/or its subclasses to know whether its safe to use Iterator.remove(). This violates LSP, but its accepted practice in Java. It makes writing and maintaining software more complex and more susceptible to bugs.


Question 3: The Open-Closed principle states that we should introduce new behavior/functionality through new classes (inheritance or interfaces). So if for example, I have a WriteLog method in the base class, which has no preconditions, but I introduce a new subclass which overrides the method but ONLY actually writes to the log if the event is highly critical....if this is new intended functionality (precondition being hardened on the subtype), would that still be breaking the LSP? The two principles would appear to contradict one another in this case.

I think you mean postconditions when you say preconditions - you're describing about the what the method promises to fulfill. If so, then I see no LSP violation - if the method superclass promises nothing, then the subclass can do what it likes and still be perfectly substitutable. The fact that the subclass is more selective ("ONLY actually writes") about what it writes is new functionality, especially in light of the fact that the superclass promises nothing.

2
btilly On

Why do you think that this should break the Liskov Substitution Principle?

What the LSP is really about is that methods should Do The Right Thing if they are passed objects that are of a subtype rather than the original type. Meaning that if you can prove that the method "does the right thing" if you pass it objects of the type, then it will "do the right thing" if you replace those objects with objects of a subtype.

But in your case you have no method calls, and so the LSP is somewhat irrelevant.