
The Liskov Substitution Principle (2019) - juanorozcov
https://www.brainstobytes.com/the-liskov-substitution-principle/
======
sumanthvepa
BTW if anyone is wondering why deriving class Square from class Rectangle is
an error, it is primarily because the interface of the Rectangle class is
mutable. If instead the Rectangle class was not mutable then deriving a square
from it would be perfectly fine. Discussion on StackOverflow:
[https://stackoverflow.com/questions/1030521/is-deriving-
squa...](https://stackoverflow.com/questions/1030521/is-deriving-square-from-
rectangle-a-violation-of-liskovs-substitution-principle)

~~~
mjburgess
I disagree with the mutability argument... the Square constructor is "not a
subset" of the Rectangle constructor,

Compare: new Square(side) new Rectangle(width, height)

In this way, a Rectangle is more like a child of a square: widening its
capabilities.

~~~
chrisoverzero
Subclasses get narrower (that is, more specific) than their parents. Consider
those constructors. In order to constructor-chain from a Square up to a
Rectangle, duplicate the single parameter:

    
    
        Square(side) : Rectangle(side, side)
    

There's not a well-behaved way to go in the other direction.

    
    
        Rectangle(width, height) : Square(width) // ???
        Rectangle(width, height) : Square(height) // ???

~~~
pdpi
> Subclasses get narrower (that is, more specific) than their parents.

That's precisely the problem. Subclasses have to less restrictive than their
parents. If a subclass adds observable restrictions, it fails the LSP.

~~~
Twisol
Conversely, if a subclass adds observable flexibility -- such as the ability
to stretch along a single axis -- it also fails the LSP.

Subclasses can really only add _orthogonal_ properties, such that influencing
one of these properties doesn't perturb properties observable from the super-
interface.

~~~
BeetleB
> Conversely, if a subclass adds observable flexibility -- such as the ability
> to stretch along a single axis -- it also fails the LSP.

That's not a given. If the new capability is only in new methods of the
subclass, then the program expecting the parent class will never be impacted
by it.

To be fair, I suppose you agree:

> such that influencing one of these properties doesn't perturb properties
> observable from the super-interface.

~~~
Twisol
Right, that's why I qualified both times with "observable". ~_^

It's worth noting that even if this capability can only _occur_ due to methods
on the subclass alone, it can still fail LSP if those manipulations are still
_observable_ to a client using only the superclass.

This is pretty much only possible because multiple clients can have a
reference to the same object, since it allows one client to observably
transition the object into a state that should be unreachable from the
perspective of the second client. (This is the root of the need to make
"defensive copies" [1].)

If one imposes a strict single-ownership discipline, then it doesn't matter if
super-interface properties can be violated by subclass-only methods, because
for the duration of its use via its super-interface, those violations are
impossible (and hence unobservable).

[1]
[http://www.javapractices.com/topic/TopicAction.do?Id=15](http://www.javapractices.com/topic/TopicAction.do?Id=15)

------
gregkerzhner
I feel like the real Liskov Subsitution Principle should be "if you have a
subtype P of type S, redesign your code so that there are no subtypes".

Seriously though, I have been a professional developer in a classical object
oriented language for the last 5 years and have written 0 class hiarchies.
Common functionality can be mixed in by composing "parent" objects around
common child objects. Passing multiple types of objects to a single function
is better handled via interfaces, not inheritance.

Is there a good reason to have a class hierarchy in modern programming?

~~~
mumblemumble
You're contradicting yourself a bit here. Implementing an interface is just a
special case of inheritance.

The fundamental reason why some languages make a formal distinction between
interfaces and purely abstract base classes is that using the "interface"
keyword is effectively signing a contract that says you will not do this one
thing (implementing virtual methods), and in return the compiler will allow
that base type to participate in multiple inheritance.

 _edit:_ I should also mention that it's entirely possible to violate LSP
using only interface inheritance. Java's collections do this:
Collections.unmodifiableList() returns a List<T> that can't be modified. But
List<T> is rotten with methods for mutating the list. So what happens if you
try to mutate one of these? You get a rather surprising exception.

I suppose you could argue that this isn't _technically_ a violation of LSP,
because the JavaDoc for List<T>::add says that it might throw
UnsupportedOperationException. But UnsupportedOperationException is a
RuntimeException, not a checked exception, and it's rare for Java developers
to make a habit of handling UnsupportedOperationException when mutating
collections. So I'd argue that it's only a part of the method's spec in the
most pedantic of ways. IMO, the more pragmatic perspective is that creating a
subtype that inherits a whole bunch of interface methods that it has no
intention of implementing is just about as clear-cut a violation of LSP as you
can get.

~~~
rovolo
Yep, the read-only List<T> implementations definitely feel like a violation.

For the benefit of people using older languages, I'd like to point out that
newer languages tend to split collection interfaces into ReadOnly and
ReadWrite variants, where ReadWrite subclasses ReadOnly. This means that a
mutable list can be assigned to a read-only list variable, but a read-only
list can't be assigned to a read-write list variable (because you can't write
to an unmodifiable collection).

~~~
nybble41
There is a third variant which should also be considered: Immutable. It offers
exactly the same interfaces as ReadOnly but with the additional guarantee that
not only can _you_ not mutate the object through that reference, no one else
can either. Enumerating an Immutable collection or getting the item at a
particular index multiple times is certain to always give you the same result,
which allows simpler code and (with compiler support) better optimization.

This would be superfluous in Rust since the borrow checker ensures that no one
else can mutate data that you have a reference to, but most other languages
with mutation would benefit. However, the only mutable-by-default language I
am aware of which directly distinguishes between read-only ("const")
references and immutable data is D.

------
pdpi
One apparently minor change in Kotlin coming from Java is that classes are
final by default, and can be made inheritable from by qualifying them with
`open`, instead of being open by default and requiring

This seems small, but is a major shift in mentality: Classes shouldn't be
inherited from unless they were designed to be inheritable from, and this
usually comes with non-trivial invariants you need to document and respect
(one aspect of this being the LSP). Therefore, inheritability should be opt-in
rather than opt-out.

~~~
gHosts
In fact, skip all the natter about subtypes and the like and think _purely_ in
terms of class invariants.

A class invariant holds at the end of the constructor and the start of the
destructor and at the start and end of every public method.

A class invariant check for the child class _must_ invoke the class invariant
check of the parent class up the class hierarchy.

And that is the point and the whole point of the exercise.

You can utterly rely every on instance of the parent class or any descendent
class obeying the class invariant for the parent class.

This allows you to reason about a broader group of types without getting
bogged down by concrete details.

~~~
Noumenon72
What's the invariant for the problem in the article, where set_height on a
square mutates @width? Seems like you don't want to prevent width from
mutating _everywhere_ , you just want to ensure it doesn't mutate during
set_height.

------
jrockway
I have always felt that people use OO exactly the opposite as Liskov suggests.
Liskov says that subclasses must be fully substitutable, and that implies that
any valid operation on the superclass must be valid on the subclass. So say
you have a Bag class. You can add elements to it at will. Then you create a
Set subclass, and now inserting a duplicate throws an exception. You just
broke substitutability. Even though the code is easy express with this
hierarchy (if exists(element) throw else super(element)), your program blows
up if you try to substitute. Doing the opposite is safe, of course, but the
code reuse doesn't work when you make your Bag a subtype of Set. From an
algorithm standpoint, a Bag isn't really implemented in terms of a Set, so
you're writing a new data structure from scratch (rather than just changing
one condition in insert() like the invalid example).

What I take away from this is that people are mostly using OOP to cut-n-paste
code when two things are similar. I see the value in not wanting to rewrite
code (or to create a third "common factor" that you will never use, to
implement Set and Bag), but I think this use of OOP basically precludes any
computer science reasoning when looking at your classes. Apparently that's not
a problem, as people are ignoring Liskov right and left while building good
stuff. But thinking about it deeper, I fear that we didn't find the right
model when we invented OOP, and that is why newer languages are ignoring OOP
in favor of composition.

~~~
narag
For some people, programming languages are much like math, so there's right
and wrong, and there's a right way to do OOP, or even OOP is or isn't right in
itself.

Maybe I'm wrong, but I tend to see language features as a usability question.
In different human languages (or even in the same language) you can say the
same thing using different syntax, but one way of saying it is more clear or
more inspiring. It's not what some object _is_ , it's how it works better what
makes sense to me. Again, that's just me, YMMV.

~~~
mumblemumble
I agree, it's better to be pragmatic. So it's not _super_ useful to say that
LSP is about whether you're doing it right or wrong.

I prefer to think of it in terms of making the codebase intuitive to work in.
So I like to formulate it as, "You should be able to safely and fearlessly
plug any subtype of a class into a method that accepts that class as an
argument."

This is a bit of a departure from how Liskov originally formulated things, but
I think it is fair to say that it's a refinement that better captures the
spirit of how she talks about it in more recent interviews. And it also
captures why it's such an important principle, especially if you're working on
a team: Nobody wants to get stuck working in a codebase where Widgets are
partitioned into two different categories that can only be distinguished by
carefully reading their implementations, according to criteria that can only
be understood by carefully reading the implementations of methods that operate
on Widgets. It's not _wrong_ , per se, it's just not a working environment
that any self-respecting person wants to be stuck in..

------
evdev
Meta comment, but I would love for there to be more insight into the sociology
of Hacker News. I feel like in the last couple of years we've seen a _stark_
decrease in the frequency of "What to expect when you're expecting (to be a
millionaire founder)" articles and a stark increase in "Enterprise Patterns"
discourse. The latter is oddly at pretty serious tension with PG's ideas about
development and Scheme.

I find the "Liskov Substitution Principle" is an awkward way of pointing at
the idea that your usage of subtyping should not render the type variance in
your system nonsensical.

~~~
Jtsummers
It's cyclic. When I first joined over a decade ago there seemed to be a lot of
technical posts and discussion at the time, which gave way to a lot of SEO
discussions, which gave way to technical discussions, then shit tons of Erlang
(not that I minded), then other stuff for founders, etc. People tend to post
interesting things which leads to other interesting and related things. So
you'll see a lot of a certain topic or field, maybe a surge in new users or
user activity because it interests them, and then some new topic or field
becomes dominant for a while and appeals to a different subset of users who
become a bit more active with submitting, posting, and voting.

------
loopz
If you absolutely must use subclassing, remember that whatever code is
operating on your base class, would need to be able to operate all your
subclasses in identical fashion without introducing accidental surprises and
unanticipated side-effects. If this seems restrictive, please consider that
changes to one unit of code should ideally be unrelated to other separate
units of code, and how to better accomplish this. Later usage of subclassing
can at any time introduce similar leakages of encapsulation and temptations
around that theme.

------
Fatalist_ma
I don't think this is a good or even valid example. Where do we see that the
method that depends on the Rectangle class no longer works correctly for
instances of Square? Why does it depend on the exact value returned by the
object?

The example from the Stackoverflow link makes more sense: " ssert.AreEqual(20,
rect.Height); ... because changing a rectangle's width does not affect its
height. " Well, we may choose to not follow that assumption in our project.
Let's say we're making a graphical editor, our rectangle could have various
restrictions - like fixed proportions, max/min area, etc; so I can imagine how
we may not have a guarantee that changing 1 side does not change another side.
If it's just for general-purpose geometric calculations - then sure, but then
it better be an immutable object.

I mean, you should not use inheritance for things like these but I don't think
this always violates LSP.

------
memco
I think the concept of the article is great but the explanation could use some
work. Given the code it does not seem clear to me why the test case of an
explicitly requested rectangle now returns functions from a square that was
not initialised or requested in the setup. Maybe this is specific to the
language the author is using?

~~~
juanorozcov
Thanks!

I will try to make the explanations clearer, I'm still trying to improve my
writing and this is very much appreciated, oh, and the code is Ruby.

I forgot to add the source code for the article, I just pushed it to a git
repo and added a link to it at the article's footer.

Thanks again!

~~~
TheTaytay
Thanks for the article!

In school, I believe I heard this summarized as something along the lines of:
"a derived class should require no more and deliver no less than its base
class". I have always liked that succinct phrasing of the principle.

~~~
juanorozcov
"a derived class should require no more and deliver no less than its base
class"

Uh, very nice, I need to write that down, it's by far the most concise version
of the principle I've heard!

Thank you

------
toolslive
In golang, X is a subtype of interface{}. But []X isn't a subtype of
[]interface{} (according to the compiler). Doesn't this violate the above
subsitution principle ?

~~~
zozbot234
In golang, a white horse is a horse: If you ask me for a horse, I can give you
a white horse. But if I want a stable for _n_ horses, it's not correct to give
me a stable for _n_ white horses, because that doesn't account for yellow or
black horses. Does this violate the substitution principle?

(inb4: "It depends on what the meaning of 'is', is!")

~~~
toolslive
To continue your analogy, I want to be able to see the stable of white horses
([]X) as a stable of horses ([]interface{}), since a white horse is a horse
after all. In java (or C++ or most other languages), you can do the following

    
    
            String[] strings = {"x0","x1"};
            Object[] things = strings;
    
    

They can't be both right.

~~~
zozbot234
But a stable of horses, can accept yellow or black horses. A stable of white
horses, cannot accept yellow or black horses. If what one can accept in both
cases was the same, it is evident that "white horse" would not differ from
"horse". But acceptable and unacceptable horses are mutually contrary. Thus it
is clear that a stable of white horses is not a stable of horses.

~~~
toolslive
> Thus it is clear that a stable of white horses is not a stable of horses.

You're not making sense any more.

~~~
nybble41
A stable that can _only_ contain white horses is not a stable that can contain
_any_ kind of horse. Whether you can ignore that distinction depends on
whether the stable is in a covariant or contravariant position, i.e. whether
you want to get horses _from_ the stable or put horses _into_ the stable.

These would be fine:

    
    
        Horse[] genericStable = …;
        WhiteHorse[] whiteHorseStable = …;
    
        genericStable.insert(new WhiteHorse());
        Horse someHorse = whiteHorseStable.first();
    

However, these can fail due to type errors:

    
    
        whiteHorseStable.insert(new BrownHorse());
        WhiteHorse horse = genericStable.first();
    

Because the ability to substitute a subtype for a supertype (or vice-versa)
depends on whether the use is covariant or contravariant (or invariant) it
becomes difficult to have proper subtype or supertype relationships in object-
oriented languages where this distinction is not factored in to the type
system. Simplifying a bit, to apply the LSP the language needs to distinguish
between getters and setters at the type level and can't treat all methods
equally.

~~~
toolslive
Yes! it's about (co|contra)-variance and mutability, and your type system
should understand this.

