I got to read Barbara Liskov original paper. You may get surprised by my findings.
Liskov Substitution Principle is considered important. Somebody called Robert C. Martin included this cornerstone axiom into “five basic principles of OOP”, and somebody called Michael Feathers named those principles S.O.L.I.D., where “L” stands for “Liskov”. It is being taught in the universities, so all the CS students are supposed to know it. Eventually everything escalated beyond the proportions, and you occasionally may get asked this question in your job interview when applying for software developer position.
So yes, it is fair to say that Liskov Substitution Principle is considered important.
With all this fame, Liskov’s principle get overused and abused. As any internet citizen, you must trust Wikipedia, and whenever Wikipedia says Circle is–not –an Ellipse because of Liskov, you should better trust this. Evil-minded people went as far as suggesting Object-Oriented Design is broken by design because of Circle-Ellipse problem. Liskov zealots go as far as declaring that overriding any method of any class violates Liskov principle, therefore method overriding is Evil.
Now that sounds bad. OO being broken is a scary thing indeed. But what is this mighty Liskov Substitution Principle then? Let’s read the original paper, as Barbara Liskov wrote it back in 1999.
Formal definition does look serious for uneducated eyes of a scared developer. This definition is written on the very first page of the paper. Quote:
Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T. A type’s specification determines what properties we can prove about objects.
“Type’s specification” is a key phrase here. Don’t overthink it just yet; Barbara Liskov explains what she means by this definition down the way. On the last page of her work, actually. Quote:
Our intention is to capture the intuition programmers apply when designing type hierarchies in object-oriented languages.
So far so good. Barbara observed good object-oriented programmers doing something intuitively, and decided to describe those best practices in a formal mathematical way. Nobody is hurt here.
Now, how do these good programmers perform these good practices? Read on. Quote:
Programmers […] use it primarily in an informal way. […] These mappings can be defined informally, in much the same way that abstraction functions and representation invariants are given in the comments in a program that implements an abstract type.
Translation to the uneducated eyes of a scared developer:
When writing public methods of superclass, the good developers write comments; and in those comments they give some certain promises (constraints and invariants) about what these methods promise to do and what not.
Nobody is hurt here either. Isn’t it true? It’s obvious and intuitively fair: if a supertype promises that method X does not throw any exception in any circumstances, then all users/clients trust this promise and do NOT expect any problem when invoking this method. If some method explicitly promises to throw an exception on some sort of input parameters, clients expect these exceptions. And eventually any new subtype that violates any documented promise will potentially disappoint all the existing users/clients that try to polymorphically use that method.
Sounds like a Captain Obvious, doesn’t it? One can formulate a million principles as harmless as LSP (and equally as useful).
“Documentation should not be misleading”
“Software should not contain bugs”
“Division by two should not cause division by zero exception”
“Not executing any method should not cause any method to be executed”.
Phew. What a relief. This Liskov thing is actually an easy thing to answer during your job interview.
If you promise that your methods do something, you should stick to this promise for all children. If some wishful client relies on promises that nobody gave him, he should blame himself for any damage.
Here is an example of unexpectedly naïve client that relied on constraints that nobody promised him:
void my_function(Supertype obj){ long started=time_now(); obj.methodX(); long ended=time_now(); if ((ended-started)<1000) ohMyGod(); } my_function(new Supertype()); // all is fine my_function(new SuperfastSupertypeImplementation()); // ohMyGod happened! //Please help us!
What happened? Some naïve client made an assumption (that nobody has ever guaranteed in no documentation whatsoever) that ALL subtypes will be as slow performing as a Supertype. Strictly mathematically speaking in Liskov terms, the client believed that property q(x) will be true for q(y) although nobody ever promised this. It’s a problem of excesively naïve client. What a sucker.
I’m still not convinced. Why would Barbara write so many smart words about such a trivial issue?
Quote:
However, intuition in the absence of precision can often go astray or lead to confusion.
Okay, that explains the whole issue. A researcher of Computer Science wants to write down her observation in a formal, mathematically-correct way because she can. Because some inconsiderate hypothetical person is not convinced by a saying “keep to the promises you made in your documentation” and is better convinced by a mathematical formula about “q(x) and q(y) where x is S and T is subtype of S”.
It’s not the world’s first Captain Obvious PhD, and it’s not the last. Let it be, let’s mind our own business.
But wait. How come circles and ellipses on Wikipedia violate Liskov, and why object oriented is broken by design because of Liskov and circles and ellipses?
After reading the famous paper by Barbara, and knowing her true intentions, you should be afraid no more. It’s all about keeping your promises given in your documentation. Here is Circle-is-an-Ellipse implementation that is okay for everyone, including Ms Barbara:
class Ellipse{ /** * sets width of an ellipse. */ public void modifyXaxis(int newLength){…} /** * sets height of an ellipse. */ public void modifyYaxis(int newLength){…} } class Circle extends Ellipse{ public void modifyXaxis(int newLength){ super.modifyXaxis(newLength); super.modifyYaxis(newLength); } public void modifyYaxis(int newLentgh){ this.modifyXaxis(newLength); } } void clientsUsage(Ellipse ellipse){ ellipse.modifyXaxis(30); ellipse.modifyYaxis(15); // show me who promised ellipse’s // X axis wouldn’t change by now }
See? Nobody got hurt. Ellipse never restricts itself to something that is not stated in its specification. Calling the method will result in at least the behavior described in a spec. What else happens within the method is purely an implementation detail of Ellipse (or its children), and it is NOT recommended to rely on undocumented functionality. Do NOT expect it will re-calculate ellipse’s area, and do NOT expect it will not recalculate it. Do NOT expect this method call will take exactly 10 CPU cycles to complete. However, DO expect this behavior will do what it promises. DO expect no exception is thrown because method declaration explicitly promises NOT to throw any exception.
Being totally fair, I must also show a Circle-is-not-an-Ellipse situation:
class Ellipse{ /** * this method modifies only width of an ellipse, * and does NOT modify its height. * Height remains unchanged until you call modifyYaxis(). * This method only takes 10 CPU cycles to perform, * and does not recalculate area. */ public void modifyXaxis(int newValue)[…} … }
In such a case, it is NOT a good idea to create a Circle by extending an Ellipse: some law-abiding client may rely on constraint documentation, and this client may be righteously surprised in finding some property modified without explicitly asking for it, or when method call takes longer than promised.
Frankly, I have no idea why the author would restrict Ellipse and all future Ellipse generations in that way. Perhaps – it’s my speculation – some of the clients asked for this particular (albeit strange) promise, and perhaps they had a good reason for it. These clients obviously heavily rely on Ellipse changing its width independently from its height, so these clients explicitly say they cannot work with circles – only with ellipses.
In such a case… well… no can do. We cannot extend the system by adding Circle subtype to the Ellipse, because, well, we must keep our promise to the existing clients.
But is it a problem with object oriented design in general? Does it mean that from OO perspective Circle is not an Ellipse because such saying violates Liskov?
No. Way.
It’s a problem of one occasional inconsiderate Ellipse developer who gave too many unnecessary promises long time ago, and now the clients depend on such illogical promises. Or alternatively it’s a problem of a legacy system that cannot work with Circles because they need specifically Ellipses. But such situation is fairly rare in real life. In general, Circle is an Ellipse.
In general, Liskov Substitution Principle is Captain Obvious, with way too much hype from the people who never read Barbara Liskov original paper.
I was almost convinced by your article, but eventually I realized that the LSP is really broken by the Circle->Ellipse. If your consider that Circle derives from Ellipse (en mathematically speaking, a Circle is an Ellipse indeed), then this is broken:
Ellipse ellipse = new Circle();
ellipse .modifyXAxis(30);
ellipse .modifyYAxis(10);
Because Java has virtual method, the ellipse will have X,Y= 10,10.
There was an implicit postcondition that X and Y can be modified independently in an ellipse, and your implementation breaks it.
Most often there are no explicit or implicit postconditions about setX() and setY() being independent.
Great blog you have got here.. It’s difficult to find quality writing like yours these days. I really appreciate people like you! Take care!!
This post is very interesting, but is hard to find in search engine.
I found it on 14 spot. You can reach google top ten easily using one useful wordpress plugin and increase targeted traffic many times.
Just search in google for:
Akalatoru’s Rank Plugin