Liskov Substitution: The real meaning of inheritance
14 comments
·January 21, 2025bts
And functional programmers would argue that contravariance is the real meaning of Liskov’s substitution principle: https://apocalisp.wordpress.com/2010/10/06/liskov-substituti...
gsf_emergency
>So LSP just says “predicates are contravariant”
Maybe just leave out the "just" for a pleasant journey?
Since the interesting part of Barbara's uh, guideline, as "almost" pointed out by your link, is "almost" the opposite of "almost" trivial..
Don't mind me, I'm imbecilic :)
(See your link's comments, at least those imbeciles "almost" get it)
sunshowers
Liskov substitution will not save you. One of the worst cases of inheritance I've ever seen was in a hierarchy that was a perfect Liskov fit -- an even better fit than traditional examples like "a JSON parser is a parser". See https://news.ycombinator.com/item?id=42512629.
The fundamental problem with inheritance, and one not shared by any other kind of polymorphism, is that you can make both upcalls and downcalls within the same hierarchy. No one should ever use inheritance in any long-term production use case without some way of enforcing strict discipline, ensuring that calls can only go one way -- up or down, but not both. I don't know to what extent tooling to enforce this discipline exists.
(Also I just realized I got punked by LLM slop.)
mont_tag
Better to think of LSP as more of a gray scale than all or nothing. The more the APIs match, the more substitutability you gain.
Switching to composition has its advantages but you do lose all substitutability and often need to write forwarding methods that have to be kept in sync as the code evolves over time.
warrenbuffering
Liskov Substitution is good sometimes actually
bedobi
Do yourself a favor and wear yourself off all this SOLID, Uncle Bob, Object Oriented, Clean Code crap.
Don't ever use inheritance. Instead of things inheriting from other things, flip the relationship and make things HAVE other things. This is called composition and it has all the positives of inheritance but none of the negatives.
Example: imagine you have a school system where there are student users and there are employee users, and some features like grading that should only be available for employees.
Instead of making Student and Employee inherit from User, just have a User class/record/object/whatever you want to call it that constitutes the account
data class User (id: Int, name: String, email: String)
and for those Users who are students, create a Student that points to the user data class Student (userId: Id, blabla student specific attributes)
and vice versa for the Employees data class Employee (userId: Id, blabla employee specific attributes)
then, your types can simply and strongly prevent Students from being sent into functions that are supposed to operate on Employees etc etc (but for those cases where you really want functions that operate on Users, just send in each of their Users! nothing's preventing you from that flexibility if that's what you want)and for those users who really are both (after all, students can graduate and become employees, and employees can enroll to study), THE SAME USER can be BOTH a Student and an Employee! (this is one of the biggest footguns with inheritance: in the inheritance world, a Student can never be an Employee, even though that's just an accident of using inheritance and in the real world there's actually nothing that calls for that kind of artificial, hard segregation)
Once you see it you can't unsee it. The emperor has no clothes. Type-wise functional programmers have had solutions for all these made up problems for decades. That's why these days even sane Object Oriented language designers like Josh Bloch, Brian Goetz, the Kotlin devs etc are taking their languages in that direction.
saidinesh5
Never using inheritance is pushing the dogma to the other extreme imo. The whole prefer composition over inheritance is meant to help people avoid the typical OO over use of inheritance. It doesn't mean Is-A relation doesn't/shouldn't exist. It just means when there is data, prefer using composition to give access to that data.
There will be times when you want to represent Is-A relationship - especially when you want to guarantee a specific interface/set of functions your object will have - irrespective of the under the hood details.
Notifier n = GetNotifier(backend_name);
Here what you care about is notifier providing a set of functions (sendNotification, updateNotification, removeNotification) - irrespective of what the implementation details are - whether you're using desktop notifications or SMS notifications.caspper69
Just because you see OO languages starting to favor composition over inheritance does not mean inheritance has no place, and indeed, interfaces as a form of composition have existed in many bog-standard OO languages for decades.
Your example dosn't compute, at least in most languages, because derived objects would not have the same shape as one another, only the shape of the base class. I.e. functions expecting a User object would of course accept either an Employee or a Student (both subclasses of a User), but functions expecting a Student object or an Employee object would not accept the other object type just because they share a base class. Indeed, that's the whole point. And as another poster mentioned, you are introducing a burden by now having no way to determine whether a User is an Employee or a Student without having to pass additional information.
Listen, I'll be the first to admit that the oo paradigm went overboard with inheritance and object classification to the n-th degree by inventing ridiculous object hierarchies etc, but inheritance (even multiple inheritance) has a place- not just when reasoning about and organizing code, but for programmer ergonomics. And with the trend for composition to disallow data members (like traits in Rust), it can seriously limit the expressiveness of code.
Sometimes inheritance is better, and if used properly, there's nothing wrong with that. The alternative is that you wind up implementing the same 5-10 interfaces repeatedly for every different object you create.
It should never be all or nothing. Inheritance has its place. Composition has its place.
And if you squint just right they're two sides of the same coin. "Is A" vs "Can Do" or "Has".
sunshowers
Inheritance has no place in production codebases—unless there is strict discipline, enforced by tooling, ensuring calls only go in one direction. This Liskov stuff has zero bearing.
mariodiana
It's sad that late-binding languages like Objective-C never got the love they should have, and instead people have favored strongly (or stronger) typed languages. In Objective-C, you could have your User class take a delegate. The delegate could either handle messages for students or employees. And you could code the base User object to ignore anything that couldn't be handled by its particular delegate.
This is a very flexible way of doing things. Sure, you'll have people complain that this is "slow." But it's only slow in computer standards. By human standards—meaning the person sitting at a desktop or phone UI—it's fast enough that they'll never notice.
incrudible
I will complain that it is hard to reason about. You better have a really good reason to do something like this, like third party extensibility as an absolute requirement. It should not be your go to approach for everything.
incrudible
I will agree that the problem that class hierarchies attempt to solve is a problem one usually does not really have, but in your example you have not solved it at all.
It matches a relational database well, but once you have a just a user reference, you can not narrow it down to an employee without out of band information. If a user should be just one type that can be multiple things at once, you can give them a set of roles.
warrenbuffering
[flagged]
I thought there were more but these are the only two interesting prior threads I could find. Others?
A better explanation of the Liskov Substitution Principle - https://news.ycombinator.com/item?id=38182278 - Nov 2023 (1 comment)
The Liskov Substitution Principle (2019) - https://news.ycombinator.com/item?id=23245125 - May 2020 (93 comments)