The Liskov Substitution Principle (2019)
brainstobytes.com
brainstobytes.com
This is related to the PECS principle in generics:
class List<E> {
appendToMyself(List<? extends E> producer);
copyMyselfTo(List<? super E> consumer);
}If so , is that the same as if I suggested that calling the Rectangle method `setWidth` on a Square should return a Rectangle (that is _possibly_ a Square)?
i.e.
val mySquare: Square = new Square(20)
val myMutatedSquare: Rectangle = mySquare.setWidth(40) // 20 x 40Object of type (a) have only getters in their public API. Setters imply type (b).
And, while each square is a rectangle, the set of all possible square is smaller than the set of all possible rectangles.
To see this, a square can be described by a single number, putting the set of squares in 1 to 1 correspondence with the real number line. A rectangle can be described by two numbers, so the set of rectangles is in 1 to 1 correspondence with the plane.
The line and the plane have the same cardinality, so this means the sets have the same size. For example: https://math.stackexchange.com/questions/243590/bijection-fr...
For observers: fix the lower-left corner of your rectangles at the origin. Then a rectangle is determined by the position of its upper-right corner within the upper-right quadrant. The set of all rectangles is given by the entire quadrant. The set of all squares is the 45-degree line from the origin cutting the quadrant in half. The natural measure on a set of points in this space is "area". The entire quadrant has infinite area, but no matter how infinite in extent the line is, it has zero area. (In the context of probability, of which measure theory is an application, the likelihood of throwing a dart at the upper-right quadrant and hitting the line of squares is nil.)
(You might argue that my choice of origin is "not natural", but it gives the same scheme as the natural "rectangle is (width, length) pair" and is a little more graphically intuitive. Every choice of corner as origin would give an isomorphic setup, resulting in the same subset-zero argument.)
Of course, no such squares actually exist in the real world :) In computers, you're dealing with integers or floating point, and in them the set of rectangles / squares have the same measure.
You could convert any two arbitrary-length datatypes into one, though, with a suitable encoding.
Compare: new Square(side) new Rectangle(width, height)
In this way, a Rectangle is more like a child of a square: widening its capabilities.
This is not just abstract... I forget where in the docs TypeScript starts talking about certain things being incorrect because other things are not contravariant/covariant/invariant, but yeah: TypeScript is trying to implement a practical subtyping system and technically subclasses are not subtypes of their parent classes in TypeScript’s original vision, so pragmatism forces you to say “just pretend they are!” and technically the subtype system is unsound.
The problem is that in TypeScript’s theory a type is just the full interface of object properties on a value, including all their methods. Methods always look like functions with an implicit "this" argument type. So this becomes a problem precisely because the type of Rectangle looks like
type Rectangle = {
height: number,
width: number,
someMethod: (this: Rectangle) => something
}
And that recursive definition in terms of having those methods makes a type technically terminal (no subtypes can exist).So it is precisely that we can overload methods with implementations more specific to the subtype, which stops the subclass from being a subtype of the superclass.
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) // ???That's precisely the problem. Subclasses have to less restrictive than their parents. If a subclass adds observable restrictions, it 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.
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.
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).
Yes, but this will violate the Liskov Substitution Principle, which (effectively) states that any program that is correct for a class (Square) should be correct for the subclass (Rectangle).
So a program that takes in a parent class (Square) can rely on the sides being equal. Now pass in a child class (Rectangle) and your program may break. Hence the violation.
If you know you are constructing a Rectangle, then you use it's constructor. If you create a Square, you use that constructor.
But they are not inherited, and can have completely different semantics - accept different arguments or throw for for new reasons.
That having said, the Rectangle / Square example doesn't really work: Is `new Rectangle(42, 42)` a Square? (This brings us to factory methods...)
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?
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.
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).
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.
I sat down and did the math once while contemplating writing my own collections API, and found a much smaller representation based on the observation that the read-only parts of some collections are equivalent, and hence don't deserve a separate type. This got me down to about 12% more interfaces, which I thought was probably worth it for essentially splitting everything in two, especially since it reduces the amount of Type Variance the user has to deal with (especially before they added inference to type declarations).
For collections, most of the substitution problem is around shared state, and the differences between readers and writers.
So now I'm thinking about borrow semantics in for instance Rust. When you are handed an object reference, you are only getting part of the interface for that object, unless you have made arrangements for the caller to give you control of the internal state of that object. Most, but certainly not 100%, of the covariance that I see in interfaces revolves around updating state. For instance you don't have to make a contains() function covariant to fulfill LSP. Because obviously the answer to "does this set of integers contain the string 'foo'" is no, it doesn't. Meanwhile put() or update() do care a great deal about the inputs.
I'm wondering now if you could completely (or at least by default) fold borrow semantics straight into your type system and dispense with most or all of the out of band syntax that Rust requires...
IIUC, that's what "affine types" are about. See https://en.wikipedia.org/wiki/Substructural_type_system
Granted, that idea itself gets a bit dodgy. In the case of Collection<T>, for example, an isReadOnly method was not supplied, so we haven't really been given any good way to tell ahead of time if a particular collection instance supports mutation.
Some programming languages do this. IIRC XML Schemas allow constraints of derived "types". Prolog, obv. Cyc, for sure.
In UML we talk about generalizations and specializations, a superclass is more general, a derived class is more specific. But specialized means "I do X in a specific way", as opposed to a new constraint like "You can't do X with me".
And this is where we get into the immutable object discussion, and note that if we had a Square class, then you couldn't change its height but you could say square.with_height(10) and it would return a rectangle.
Then, one wonders, if you had rectangle(10,5).with_height(10) would it return a square?! And suddenly we are in the world of "type" being utterly ephemeral and dependent only on the state of an object, and then we would only have free objects, while "Interfaces" or "Types" would actually be constraints, and a Square would be "a rectangle with two equal dimensions" and now we are writing Cyc Theorems.
OTOH, it's probably easier to avoid LSP violations with interfaces than concrete supertypes.
> Is there a good reason to have a class hierarchy in modern programming?
Well, implementation inheritance facilitates code reuse; there's some ways to do that that aren't traditional class heirarchies, but they mostly seem morally equivalent to multiple inheritance with partially-abstract base classes. Ergonomically, though, I think some of them don't do as much to encourage inappropriate subytyping or implementation sharing compared to classical purely-class-based inheritance.
Java interfaces are nice because they can be used as types while one or many classes can implement them.
Redesign your code so that the calling code does not (need to) know about subtypes.
I have a bad reason :D
I like it when objects are simple to instantiate, but sometimes I'm forced to use Spring(Boot) with beans and auto-wiring and such. In src/main, my FooController is autowired up by Spring, but in src/test, I want to instantiate FooController for use in unit tests.
In order to have my cake and eat it too, I extract out all the spring dependencies into a very small shim subclass SpringFooController, which extends FooController. They have all the same methods and types etc, but all of the SpringFooController methods' implementations are just one-liner calls into superclass methods.
What does this get me? SpringFooController is picked up by Spring at runtime, but contains no logic needing to be tested. FooController contains all the logic, but no spring dependencies, so it can be instantiated in unit tests as an ordinary object.
With all the functionality Spring gives you out of the box, it's probably still worth it. But barely.
Is Spring worth it? The only reason I'd reach for it over something like http://sparkjava.com/ is because of its good integration with https://swagger.io/tools/swagger-ui/. It's the easiest way I know to help out your front-end devs.
Whether you like the workaround really depends on how much you dislike Spring, and how much you buy into that whole frameworks-bad-libraries-good argument.
As an example of a feature, here's how Spring might validate receiving a book as a POST body. It's as simple as adding @Valid:
@PostMapping("/books")
Book newBook(@Valid @RequestBody Book newBook) {
return repository.save(newBook);
}
My thoughts on the above:1) Did i import the right @Valid. There's no compile-time checking, I'll only know it works by trying it. 2) How can I test it? Not in a unit test. Calling newBook and expecting @Valid to fire is a rookie error. I need to spin up some Spring machinery to be able to test anything. 3) Is validation enabled? How do I know? 4) OK, the validator appeared to work in test. Do I have the same Spring config in the test and prod profiles (check yaml, environment variables, CLI args, and @Config objects)? Does validation also work in prod? 5) What does it look like when it rejects input? I assume Spring will return a 400, but what's the error message. Can I set it? Is it plaintext or json? Is it logged? Is it metred? How? 6) Its return type is Book. It always returns a Book? Not an Either<Error, Book>? Not a CompletableFuture<Book>? 7) It accepts a Book. This means that an INVALID BOOK IS INSTANTIATED, and then checked (I think?) after instantiation. This means when you write your Book class, you need to deliberately make invalid state representable, so that Spring has something to validate!
Here's the gist of what I'd write instead. (Not a direct comparison, I added features I wouldn't know how to integrate with @Valid)
@PostMapping("/books")
Response<Book> newBook(@RequestBody String strBook) {
return Book.parse(strBook)
.handle( (Book book) -> {
metrics.something();
logger.something();
repository.save(book)
.map(Response::new);
},
(Error err) -> {
metrics.registerError(err);
logger.logError(err);
return Response.of(400, "Invalid book submitted: " + err.msg);
});
}
The method that I wrote can be 'just run' and tested - no need to fire up Spring. You only have a Book immutable and type-safe again.I didn't detail the workaround, but it's essentially this:
//SpringBookController extends BookController
@PostMapping("/books")
Response<Book> newBook(@RequestBody String strBook) {
super.newbook(strBook);
}
// BookController
Response<Book> newBook(String strBook) {
... // basically the big example from above minus the @RequestBody and @PostMapping
}I also think class hierarchies are simply the natural way humans think of concepts, and inheritance seems to work well if you're inheriting from more abstract classes to more concrete implementations.
Inheritance (a construct to allow you to create class hierarchies) is not the same thing as subtyping.
For instance, an integer is a subtype of a real number (assuming both, in some hypothetical language, are distinct primitive types).
In fact, LSP will work perfectly fine even in languages that don't support inheritance.
LSP decries that that the behaviour of a particular operation matches the expressed intent of said operation.
Going back to our integer being a subtype of real number example, the "<" operator will work perfectly fine whether any of the operands are either an integer or a real number.
So, I definitely agree with the sentiment "class hierarchies are useless", but I can't say the same about LSP.
In fact, if someone is passing me an object, I expect it to behave the way it does, as is decried by LSP.
Also, you may argue that I am being pedantic, however I fear some people may take what you are implying a bit literally (e.g. never subtype anything), so I thought I'd clarify.
If we're talking about theoretical "real numbers", then in constructive mathematics I believe "<" isn't decidable on reals. Whether that matters for the point you're trying to make, I have no idea.
If we're talking about floats, we have a concrete example of weird behavior of "<" leading to programming error: in Haskell, you can make a Set of any type that supports "<" (by virtue of being an instance of Ord - things that can be ordered). However, following ieee754, NaN is not equal to NaN. Which means that you can wind up accumulating arbitrarily many copies of NaN in your Set. (You don't usually want to be comparing floats for equality anyway, but it's still not good behavior.)
I'm naive, and I'm curious to know where I can read more about that.
Edit: OK, now I get it. Equality/inequality verification is undecidable for irrational numbers.
Second, this is independent of structural vs nominal typing - implementation of an interface can be thought of as inheritance of a class whose methods all have noop implementations, and in fact this is (almost) how it's done in languages that don't have interfaces like C++
That said, you don't need an OOP language or "classes" to write an object-oriented program (with subtyping etc.); you can do it in C with message passing, callbacks, etc... Windows basically forces you to do this when using the Win32 GUI API. It's not really a counterexample though. It's not 100% exactly the same thing (and you might have some interesting discussions around this) but for the most part it can be quite similar in spirit and mostly just a matter of doing everything as classes would do, except manually.
If you haven't had a need for serious OOP then either your job involves writing a different type of code (e.g. data crunching might be better with just functions everywhere) or you need to invest more time learning object-oriented design. If it's just the usual "a Dog is an Animal" thing to you then you're missing out on an entire world. A huge slice of programmers either don't know to take it seriously, don't want to take it seriously, or don't have to take it seriously, and end up either hating OO design or finding it pointless or trivial. But it's quite a deep subject that is generally taught rather poorly and superficially (and possibly far, far too early) in courses; you're lucky if they even mention LSP, and there's so much beyond that that they likely won't touch at all. There's probably enough to it to talk about it in a graduate course if not more. And in practice, coming up with a good OO design can be quite challenging (and I'm talking about cases where OO is the solution); languages that force you to do everything as classes and objects often end up making a complete mockery out of it (like Java), but if you ever have to do things that are very dynamic (making a plugin architecture is often like this) it can be almost impossible to avoid.
P.S. Inheritance (which results in class hiearchies) is just a mechanism that's used for more than just OOP. It's also used for mixin purposes, which aren't really about OOP at all, but just about code reuse, especially in a language where there's no better solution. I haven't mentioned things like these above, but they come up a lot too. (e.g. the HTTP server in Python has a ThreadingMixin base class, though you can probably have a discussion about whether it's the best solution or not.)
Also, calling a visitor a glorified switch() is technically correct but makes a mockery out of it. You generally wouldn't want to (well, shouldn't want to) implement it as a switch(), though in simpler situations there might not be much difference. It can have implications on extensibility, modularity, etc... which is kind of obvious, otherwise everyone would be doing that and not going through the trouble of making a visitor.
rarely, but perhaps not "never".
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.
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.
This still makes inheritance more 'opt-in' than 'opt-out', but still facilitates the use of 'container' classes. It's still open for expansion but you can't -really- change the implementation.
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.
> Doing the opposite is safe, of course, but the code reuse doesn't work when you make your Bag a subtype of Set
I disagree. suppose the Bag class only allows unique entries, then if you create a subclass that overrides this behavior to allow duplicates you are breaking an expectation the system may have about Bags, and possibly relies on, provoking unexpected consequences. For instance, allowing duplicates in Bags may trickle down to an inventory shortage issue since the system plans at most one item of each type per user.
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.
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..
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.
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.
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!
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.
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
(inb4: "It depends on what the meaning of 'is', is!")
String[] strings = {"x0","x1"};
Object[] things = strings;
They can't be both right.You're not making sense any more.
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.Otherwise you get an exception.
String[] strings = {"x0","x1"};
strings[0] = "something else"; // mutable.
Object[] things = strings;
things[1] = null;// mutable.
all works.(in java), but I understand what you mean.