Principle of Liskov’s replacement

Asked

Viewed 2,094 times

25

Liskov’s substitution principle says that if given types T and S being S subtype of T then S should be replaced by T. My understanding is that if I have an instance of S then I can use it in places where I would use instances of T.

But it seems to me that it is already naturally guaranteed by the language itself. In this case, I work with C# and if I have a type S who inherits from T then the following code is fully valid

S obj = new S();
T objRef = obj;

So what happens is basically

  • When using objRef in place of obj I only have access to T, whereas those of S are not accessible.

  • When calling a virtual method T from objRef whether it has a different implementation in S that’s what’s called.

Anyway, I at all times I can use objects like S in place of objects of the type T. Because of this, obeying Liskov’s substitution principle is no longer guaranteed by the language itself?

Why this principle is important if it seems to be a natural thing of language?

  • 1

    This is only true in terms of the verification that the compiler does. What this check does not guarantee is that the logic implemented in a subtype class breaks this rule. See this pole where has the well-known example of rectangle and squared.

  • 2

    Macoratti has a great series (videos) on The SOLID Principles In this video he explains the Principle of Liskov’s replacement with examples. Sources: https://www.youtube.com/channel/UCoqYHkQy8q5nEMv1gkcZgSw http://www.macoratti.net/11/05/pa_solid.htm

  • 1

    The LSP really is something very natural if we look at it from today’s point of view but it was not so obvious at the time (so much so that it ended up getting a special name). At the time they had just invented object orientation and it was quite common to use "implementation inheritance" (e.g., circles with ellipses or stacks with vectors) that did not respect the substitution rule.

2 answers

22


The the principle of Liskov’s substitution can be applied in languages that guarantee nothing. The principle uses a semantics and does not care about the implementation of this.

Even languages that guarantee something can only guarantee that the replacement mechanism will be correct and possibly the compiler will generate code needed to facilitate the work of the programmer who does not need to worry about all the details.

In fact, compilers can only identify if the structure of the code is in order, they can only understand what the code can tell them. Nothing prevents you from making a code in C# in which all contracts are agreed, the replacement can be done technically perfectly but that the actual code that implements the type of fact is not replacing operations in an equivalent way and compatible with the desired behavior.

In general only a human can tell if that code written in the subtype corresponds to an implementation different from the inherited type but producing the desired results in accordance with the original intention of the higher type. It is considered impossible to create a language that can guarantee this (it would be possible, but the cost is great and would require so much annotation on the issue that should not compensate for the effort).

Some languages, including C#, allow contracts to be verified not only in the signing of the methods but how the data will be communicated to their callers, but no more than this. In fact the more rigid these contracts are the more difficult it will be to create a compatible subtype.

Examples of rape

A clear example of violation of the principle that violates nothing in the language (no worry of correct code just showing the problem):

class Produto {
    ...
    public virtual void somaEstoque(int valor) { this.qtde += valor; }
}
class Metal : Produto {
    ....
    public override void somaEstoque(int valor) { this.qtde -= valor; }

The example is exaggerated but shows how easy it is to violate the principle. You can do the opposite of what is expected without the language complaining of anything.

Another form of violation is when you don’t use polymorphism. The language has no way of knowing whether you should use it or not.

class Arquivo {
    ...
    public virtual void Gerar() { ... }
}
class ArquivoWord : Arquivo {
    ...
    public void GerarDoc() { ... }
}
public class ArquivoPdf : Arquivo {
    public void GerarPdf() { ... }
}

Okay, you don’t have a replacement in there. But you should. The language does not require you to overwrite the virtual method as the problem requires and as it was thought.

Another example that hurts semantics without affecting the technique is when a method that is in the ascending class and it is invalidated in the descending class:

public override void Metodo() { throw NotImplemeted(); }

If the mother class defined this method in the contract she wanted something to be done there. This is a strong indicator that the daughter class is not a Mom, that’s exactly what this principle says. People tend to think that one thing is a when it really isn’t. And then they begin to discover that inheritance is very problematic and rarely useful. And puts into question the usefulness of OOP from a pragmatic point of view.

OOP

I’m not saying that OOP is garbage and it’s a paradigm that shouldn’t be used. I use it when it brings more benefits than harm but most of the time I use more classes to encapsulate than to inherit. Contrary to the belief of OOP lovers think, it ends up simplifying the design.

That is why multi-paradigm languages work and monoparadigm languages fail. The problem arises when the programmer thinks the former should be, and some even think that they use only one paradigm and force the use of one tool to solve all problems. I used to be this programmer when I had no experience, I fell in love with OOP in the 80’s and it took me more than a decade to realize that it wasn’t healthy. And it only happened when I started using OOP in practice and saw that it was just another tool with its advantages and defects and not to universal tool that some tried and still try to sell.

One of the criticisms made to OOP is that it cannot actually reproduce the real world as promised in some definitions and reuse of code is not always as simple as it is sold. If you set a type too narrowly to achieve a more precise definition you create problems for the subtype to be defined.

It is a problem similar to what occurs in a database where it is preached to restrict what the "users" (applications) of the database can do, that is, the database is self-sufficient in all rules of use. That’s beautiful in theory but in practice it makes many applications unfeasible. So the solution is not to define the database as well as to leave part of the definition of how the model should be validated for the application in a different way according to each context where the data is being used. Then you start to wonder if what’s in the database should be and even if, ultimately, it shouldn’t all be in the application.

Circle problem and ellipse

The difficulty generated the circle and ellipse problem where it is impossible to ensure that all criteria of type and subtype are met at the same time.

In general it is defined that a circle is a special case of an ellipse. Just as a square is a special case of a rectangle. So let’s look at a code using this hierarchy:

class Retangulo {
    public virtual double Altura { get; set; }
    public virtual double Comprimento { get; set; }
    public double Area { get { return Altura * Comprimento; } }
}
class Quadrado : Retangulo {
    public override double Altura { set { base.Altura = base.Comprimento = value; } }
    public override double Comprimento { set { base.Altura = base.Comprimento = value; } }
}
class Calculos {
    public void MudaArea(Retangulo retangulo) {
        retangulo.Altura *= 2;
        retangulo.Comprimento *= 4;
        //faz alguma coisa
    }
    public void FazCalculo() {
        MudaArea(new Quadrado() { Altura = 2.0, Comprimento = 2.0 };
    }
}

I put in the Github for future reference.

In the definition we already see a problem. If height and length should be the same, why should both exist? The language did not complain but semantically the definition is wrong and violates the LSP.

The thing gets worse when the classes are used and the code sends a square to a method that changes the values of height and length independently. So the method MudaArea() expects to make a change that does not occur as it should. In the background the area that should be multiplied by 8 ends up being multiplied by 64. Language can do nothing to prevent this discrepancy.

But we can say that the problem began when the subtype tried to establish a rule of its own. And it violated the rule of the base type. It gave Barbara the chills.

But if the definition did not guarantee anything about height and length this problem would not happen. But then it would allow the method to transform what would be a square into a rectangle from a semantic point of view, although technically the code would still treat it as if it were a square. Once again language has failed to do anything.

It has a huge amount of ways to show how these classes can violate the LSP. It could for example introduce a member Lado and ignore Altura and Comprimento. It is that examples like this become more obvious - for some - that violates the is a.

It is possible to solve the problem as seen in the Wikipedia article linked above but will generally hurt the LSP. Some solutions presented there may cause further problems. Others make it very difficult to think of everything that can happen, even being impossible to handle everything. It is one thing to find a solution to a simple and well studied case, another is to do in general complex problems from day to day.

Completion

In the end we see that one of the problems of OOP is that it is difficult to define the objects, transpose the real world for code. And from the point of view of code we ended up discovering that the circle is not an ellipse and almost nothing is a in fact since almost anything can include a detail incompatible with its ancestor.

OOP is cool when it simulates invented objects, which only exist in the same code. That’s why it works best in GUI or games we know about - but not in the smart games we want. Even in these cases it can be difficult to give all semantic guarantees. The advantage is that we can make simplifications when we invent the object.

  • Who is Barbara?

  • @LINQ Liskov :D

  • I tried to fit into every piece of text except the most obvious. Thank you.

6

   The Liskov substitution principle (The Liskov Substitution Principle), corresponds to the letter L of the acronym SOLID (Abbreviation of the first five principles of object-oriented programming and code design identified by Robert C. Martin (or Uncle Bob) around 2000.)

   Anyway, I can always use objects from type S in place of type T objects. Therefore, obey the Liskov’s replacement principle is no longer guaranteed by language itself?
Why is this principle important if it seems to be something natural from language?

   With this statement you are treating the Principle of Liskov’s replacement as something separate from the principles SOLID, and so can hurt, for example, the Principle of Sole Responsibility (A class must have one, and only one, reason to change. ) or the Open-Closed Principle (You should be able to extend a class behavior without modifying it.).

   Remember that the Principle of Liskov’s replacement is only one of the five principles SOLID. The use of these principles alone in a separate way does not ensure extensible, cohesive and easy-to-maintain code.

   Follows a description of the principles SOLID:

  • S - Single Responsibility Principle (The Single Responsibility Principle)

    - Uma classe deve ter um, e somente um, motivo para mudar.

  • The - Open-Closed Principle (The Open Closed Principle)

    - Você deve ser capaz de estender um comportamento de uma classe, sem modificá-lo.

  • L - Liskov Substitution Principle (The Liskov Substitution Principle)

    - As classes derivadas devem poder substituir suas classes bases.

  • I - Interface Segregation Principle (The Interface Segregation Principle)

    - Muitas interfaces específicas são melhores do que uma interface geral.

  • D - Dependency Inversion Principle (The Dependency Inversion Principle)

    - Dependa de uma abstração e não de uma implementação.


Macoratti has a great series (videos) on The SOLID Principles

In this video he explains the Principle of Liskov’s replacement with examples.

Sources:
https://www.youtube.com/channel/UCoqYHkQy8q5nEMv1gkcZgSw http://www.macoratti.net/11/05/pa_solid.htm
http://eduardopires.net.br/2013/04/orientacao-a-objeto-solid/

  • 1

    Do you think this answers what was asked? Give a rerun on the question, basically what you posted is what the AP already knew about the subject.

  • 2

    I think it can contribute to understanding mainly for those who do not adhere to these principles or are learning (like me). If you don’t think this way you can delete @bigown :)

  • 3

    It’s not a question of exclusion, negativity or anything, the information is interesting and correct but it doesn’t answer the focus of his question. He knows what SOLID is and he knows what LSP is, he just doesn’t understand why he has to follow it if the C# language already "guarantees" it.

  • 2

    I agree with your point of view. My intention with this answer was to contribute to discussion and alert those who are learning the SOLID principles. @bigown

  • @rubStackOverflow, your answer is very good, but in the wrong context, I suggest you remove your answer and put it in another post that asks what is SOLID, I have not researched, but certainly there is here in SOP

Browser other questions tagged

You are not signed in. Login or sign up in order to post.