Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to comply with Liskov's Substitution Principle (LSP) and still benefit from polymorphism?

The LSP says "The derived types must not change the behavior of the base types", in other words "Derived types must be completely replaceable for their base types."

This means that if we define virtual methods in our base classes, we have violated this principle.

Also if we hide a method in the drive method by using new keyword then again we have violated this principle.

In other words, if we use polymorphism we have violated LSP!

In many applications I've used Virtual methods in the base classes and now I realize it violates LSP. Also if you use Template Method pattern you have violated this principle that I've used it a lot.

So, how to design your application that complies with this principle when you'd need inheritance and you'd like to benefit also from polymorphism? I'm confused!

See the example from here: http://www.oodesign.com/liskov-s-substitution-principle.html

like image 302
The Light Avatar asked Jan 18 '13 16:01

The Light


People also ask

Is polymorphism related to liskov substitution?

The Liskov Substitution Principle (LSP) is strongly related to subtyping polymorphism. Based on subtyping polymorphism in an object-oriented language, a derived object can be substituted with its parent type. For example, if we have a Car object, it can be used in the code as a Vehicle .

How do you not violate the Liskov Substitution Principle?

There are several possible ways: Returning an object that's incompatible with the object returned by the superclass method. Throwing a new exception that's not thrown by the superclass method. Changing the semantics or introducing side effects that are not part of the superclass's contract.

How do you apply the Liskov Substitution Principle?

Barbara Liskov, defining it in 1988, provided a more mathematical definition: 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.

What is the best example of Liskov Substitution Principle?

The classic example of the inheritance technique causing problems is the circle-elipse problem (a.k.a the rectangle-square problem) which is a is a violation of the Liskov substitution principle. A good example here is that of a bird and a penguin; I will call this dove-penguin problem.


3 Answers

LSP says that you must be able to use a derived class in the same way you use it's superclass: "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program". A classic inheritance that breaks that rule is deriving Square class from Rectangle class since the former must have Height = Width, while the latter can have Height != Width.

public class Rectangle
{
    public virtual Int32 Height { get; set; }
    public virtual Int32 Width { get; set; }
}

public class Square : Rectangle
{
    public override Int32 Height
    {
        get { return base.Height; }
        set { SetDimensions(value); }
    }

    public override Int32 Width
    {
        get { return base.Width; }
        set { SetDimensions(value); }
    }

    private void SetDimensions(Int32 value)
    {
        base.Height = value;
        base.Width = value;
    }
}

In this case, the behavior of Width and Height properties changed and this is a violation of that rule. Let's take the output to see WHY the behavior changed:

private static void Main()
{
    Rectangle rectangle = new Square();
    rectangle.Height = 2;
    rectangle.Width = 3;

    Console.WriteLine("{0} x {1}", rectangle.Width, rectangle.Height);
}

// Output: 3 x 2
like image 144
Tommaso Belluzzo Avatar answered Nov 02 '22 23:11

Tommaso Belluzzo


Barbara Liskov has a very good article Data Abstraction and Hierarchy where she specifically touches polymorphic behavior and virtual software constructions. After reading this article you can see, that she describes in deep how software component can achieve flexibility and modularity from simple polymorphic calls.

LSP states about implementation details, not abstractions. Specifically, if you consume some interface or abstraction of type T, you should expect to pass all subtypes of T and not to observe unexpected behavior or program crash.

The keyword here is unexpected, because it can describe any of the properties of your program (correctness, task performed, returned semantics, temporarily and so on). So making you methods virtual does not mean by itself violating LSP

like image 28
Ilya Ivanov Avatar answered Nov 03 '22 00:11

Ilya Ivanov


"The derived types must not change the behavior of the base types" means that it must be possible to use a derived type as if you were using the base type. For instance, if you are able to call x = baseObj.DoSomeThing(123) you also must be able to call x = derivedObj.DoSomeThing(123). The derived method should not throw an exception if the base method didn't. A code using the base class should be able to work well with the derived class as well. It should not "see" that it is using another type. This does not mean that the derived class has to do exactly the same thing; that would be pointless. In other words using a derived type should not break the code that was running smoothly using the base type.

As an example let's assume that you declared a logger enabling you to log a message to the console

logger.WriteLine("hello");

You could use constructor injection in a class needing to produce logs. Now instead of passing it the console logger you pass it a file logger derived from the console logger. If the file logger throws an exception saying "You must include a line number in the message string", this would break LSP. However, it is not a problem that the logging goes to a file instead of the console. I.e. if the logger shows the same behavior to the caller, everything is okay.


If you need to write a code like the following one, then LSP would be violated:

if (logger is FileLogger) {
    logger.Write("10 hello"); // FileLogger requires a line number

    // This throws an exception!
    logger.Write("hello");
} else {
    logger.Write("hello");
}

By the way: The new keyword does not influence polymorphism, instead it declares a completely new method that happens to have the same name as a method in the base type but is not related to it. In particular, it is not possible to call it through a base type. For polymorphism to work, you must use the override keyword and the method must be virtual (unless you are implementing an interface).

like image 3
Olivier Jacot-Descombes Avatar answered Nov 02 '22 23:11

Olivier Jacot-Descombes