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.
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.
– ramaral
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
– rubStackOverflow
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.
– hugomg