10 ms·
Liskov Substitution: The real meaning of inheritance
- mont_tag 2y agoBetter 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.
- bts 2y agoAnd functional programmers would argue that contravariance is the real meaning of Liskov’s substitution principle: https://apocalisp.wordpress.com/2010/10/06/liskov-substitution-principle-is-contravariance/ https://apocalisp.wordpress.com/2010/10/06/liskov-substituti...
- gsf_emergency 2y ago>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)
- zbyforgotp 2y agoA practical rephrasing of LSP: subclasses should be subtypes.
- bedobi 2y agoDo 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.
- warrenbuffering 2y ago[flagged]
- mariodiana 2y agoIt'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 2y agoI 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 2y agoI 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.
- bedobi 2y agoI agree that there are better ways to model roles and FGA the point of the example wasn't to be an idiomatic solution to that problem, but to illustrate the pointlessness of inheritance in general, and User Student Employee was the first thing that came to mind that was more "real" than the usual Animal examples in any case, as for only having a User reference, you don't ever have only a User reference - overwhelmingly you would usually load a Student or Employee, and each of them would have those attributes on them that characterize each of them respectively (and those attributes would not be shared - if they were, such shared attributes would go on the User object) only those functions that truly are operating only on User would you send in each of their User objects the problem with what you're advocating is you end up with functions like this fun doSomething(user: User) { if (user is Student) { do this } else { do that } } which is insane
- sunshowers 2y agoLiskov 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 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.)
- rramadass 2y ago> 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. Disagree with your first part. Inheritance used to express Subtyping is different from that used for Code-reuse and yet again different from that used for implementing Framework skeleton structure. You have to disambiguate them carefully when using it. See the article linked to by user "Fannon" here - https://news.ycombinator.com/item?id=42789466 https://news.ycombinator.com/item?id=42789466 As for tooling, you have to enforce the contract using pre/post/inv clauses following Meyer's DbC and also explicit documentation.
- sunshowers 2y agoThanks for that article -- I have to agree with Jacob Zimmerman in the comments to the article: > I don’t get it. I read one part of the article, think I get it, then I read a different part and what I read there doesn’t jive with what I thought I understood. And I can’t figure out how to reconcile them. --- > As for tooling, you have to enforce the contract using pre/post/inv clauses following Meyer's DbC and also explicit documentation. I think we call them asserts and type-level state machines :) I don't really believe in documentation as enough of a barrier to doing worse things. It must, at a structural level, be easier to do better things.
- warrenbuffering 2y agoLiskov Substitution is good sometimes actually
- dang 2y agoI 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 https://news.ycombinator.com/item?id=38182278 - Nov 2023 (1 comment) The Liskov Substitution Principle (2019) - https://news.ycombinator.com/item?id=23245125 https://news.ycombinator.com/item?id=23245125 - May 2020 (93 comments)
- adelmotsjr 2y agoFrom the article: Again, kudos to Uncle Bob for reminding me about the importance of good software architecture in his classic Clean Architecture! That book is my primary inspiration for this series. Without clean architecture, we’ll all be building firmware (my paraphrased summary). What does clean architecture have to do with building firmware or not? Plenty of programmers make a living building firmware. Just because they don't need/can't/want to apply clean architecture in their code, doesn't mean they are inferior to those who do. Furthermore, after a snippet which I suppose it is in Kotlin, there is this: While mathematically a square is a rectangle, in terms of behavior substitutability, it isn’t. The Square class violates LSP because it changes the behavior that clients of Rectangle expect. Instead of inheritance, we can use composition and interfaces The Liskov principle is about one of the three types of polymorphism (so far): subtyping polymorphism. Which is about inheritance. Composition is _not_ subtyping. And interfaces (be it Java's or Kotlin's) are another type of polymorphism: ad-hoc. Even Wikipedia[1] has the correct definition: Ad hoc polymorphism: defines a common interface for an arbitrary set of individually specified types. Therefore, the examples of interfaces aren't compliant with LSP as well. I understand the good intentions behind the article, but it left much to be desired. A proper research to at least fix the glaring errors should have been made beforehand. [1]: https://en.wikipedia.org/wiki/Polymorphism_%28computer_science%29?wprov=sfla1 https://en.wikipedia.org/wiki/Polymorphism_%28computer_scien...
- Kerrick 2y agoI’m in the middle of reading Clean Architecture right now. The square/rectangle example is directly from the book. The firmware statement is an argument made (differently) in the book that software is called soft because it’s easy to change. Firmware is harder to change because of its tight coupling and dependencies (to the hardware). Software that is hard to change due to tight coupling and dependencies could almost be considered firmware—like brand new code without tests can almost be considered legacy.
- moi2388 2y agoYou shouldn’t believe what you read, especially from the book clean code. The principles are somewhat okay, the examples are terrible
- zwieback 2y agoIf I remember correctly, Liskov didn't talk about inheritance but subtyping in a more general way. Java, C++ and other, especially statically typed, compiled languages often use inheritance to model subtyping but Liskov/Wing weren't making any statements about inheritance specifically.
- cratermoon 2y ago>subtyping in a more general way This is correct. I read her paper closely. One example I give is how SICP provides two implementations for complex numbers[1], the rectangular for and polar form. (make-rectangular (real-part z) (imag-part z)) and (make-polar (magnitude z) (angle z)) then on page 138 provides this interface that both satisfy (define (real-part obj) (operate 'real-part obj)) (define (imag-part obj) (operate 'imag-part obj)) (define (magnitude obj) (operate 'magnitude obj)) (define (angle obj) (operate 'angle obj)) 1 https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/full-text/book/book-Z-H-17.html#%_sec_2.4.1 https://mitp-content-server.mit.edu/books/content/sectbyfn/b...
- rramadass 2y ago> use inheritance to model subtyping but Liskov/Wing weren't making any statements about inheritance specifically. Right. Inheritance is just one mechanism to realize Subtyping. When done with proper contract guarantees (i.e. pre/post/inv clauses) it is a very powerful way to express semantic relationships through code reuse.
- foobarkey 2y agoSOLID and clean code are not some universal bible that is followed everywhere, I spend a considerable amount of effort reasoning juniors and mid levels out of some of the bad habits they get from following these principles blindly. For example the only reason DI became so popular is that you could not mock static in Java at the time. In FB codebase DI was also used in PHP until they found a way to mock static, after which the DI framework was deprecated and codemods started coming in removing DI. There is literally nothing wrong in using a factory method or constructing what you need on demand. These days static can also be mocked in Java and if you really think about it you see Spring Boot adds a lot of accidental complexity (but sure its convenient and well tested so its ok to use), concepts like beans and beanfactories are not essential for solving any business problem Which brings me to S in SOLID, which I think is probably top 2 worst principles in software engineering (the no 1 spot goes to DRY). Somehow it came from some early 2000-s TDD crowd and the test pyramid, it makes sense if you embrace TDD, mocking, test pyramid and unit tests as a good thing. In reality that style of software is really hard to understand, every problem is split into 1000 small pieces invoking each other usually in some undefined ways, no flow can be understood without understanding and building a mental model of the entire 1000 object spaghetti. The tests themselves mostly just end up setting a bunch of mocks and then pretty much coupling the impl and the test on method call level, any change to the impl will cause the tests to break for only the reason that the new method call was not mocked. After going through all this ceremony the tests are not even guaranteeing the thing will work during runtime since the db, kafka or http was mocked out and all the filters, listeners, db validations were skipped. In these days so called integration tests with docker compose are a lot better (use actual db or kafka, wiremock the http level), that way your have a reasonble chance to catch things like did this mysql jdbc driver upgrade broke anything I have to mention DRY also, the amount of sins caused in name of DRY by juniors is crazy, similar looking lines get moved into a common function/method/util all the time and coupling is introduced between 2 previously independant parts of the system. As the code involves and morphs into something different the original function starts getting more args to behave differently in one case and differently in another case, if it had been left as separate files each could evolve separately. I dont really know how to explain this better than coupling should not be introduced to save few lines of typing or boilerplate, in fact any abstraction or indirection should only be introduced when its really needed, the default mode should be copy/paste and no coupling (the person adding a cross cutting PR will likely not be a jr and has enough experience to know how and when to use grep). Anyhow I have enough experience to know people are usually too convinced that all this solid, clean code stuff is peak software so I wont expect to change anyones thinking with 1 HN post, it usually takes me 2 years or so to train a person out of this and back to just putting the damn json in db without ceremony. Also need to make sure LLM-s have some good data that is based on experience and not dogmas to learn from :) As for L, no strong beef with L it’s OK
- deleted 2y ago[deleted]
- rramadass 2y agoLike most articles on "Inheritance" this is clueless about providing any "real meaning/understanding". People always take the soundbites (eg. Uncle Bob SOLID) provided as a mnemonic as being the end-all, don't fully understand the nuances and then usually arrive at a wrong conclusion. LSP (https://en.wikipedia.org/wiki/Liskov_substitution_principle https://en.wikipedia.org/wiki/Liskov_substitution_principle) has to do with behavioural subtyping guaranteeing semantic interoperability between types in a hierarchy. It involves not just the syntax of function signatures but their semantic meaning involving Variance/Invariance/Covariance/Contravariance and their guarantees using an extension to Hoare Logic i.e. Preconditions/Postconditions/Invariants (derived from Meyer's DbC). Thus without enforcing the latter (which is generally done via documentation since there is no syntax for expressing pre/post/inv directly in most languages) the former is incomplete and thus the complete contract is easily missed/forgotten leading to the mistaken belief "Inheritance is bad". The LSP wikipedia page links to all the concepts, the original papers and more for further clarification. See also Bertrand Meyer's Using Inheritance Well from his book Object Oriented Software Construction, second edition book - https://archive.eiffel.com/doc/manuals/technology/oosc/inheritance-design/page.html https://archive.eiffel.com/doc/manuals/technology/oosc/inher... Finally see Barbara Liskov's own book (with John Guttag) Program Development in Java: Abstraction, Specification, and Object-Oriented Design for a "correct approach" to OOP. Note that Java is just used as a example language while the principles are language independent.
- Nezghul 2y ago> class Square : Rectangle() { ... What if instead of Rectangle class we would have ReadonlyRectangle and Rectangle? Square could then inherit from ReadonlyRectangle, so code expecting only to read some properties and not write them could accept Square objects as ReadonlyRectangle. Alternatively if we really want to have only Square and Rectangle classes, there could be some language feature that whenever you want to cast Square to Rectangle it must be "const Rectangle" (const as in C++), so again we would be allowed to only use the "safe" subset of object methods.
- cryptonector 2y agoI think what you mean is that if a Square that is also a Rectangle can't be made to be non-square, then inheritance works. Which, fair enough, but I think there's still other good reasons that inheritance is a bad approach. Interfaces (and traits) are still way better.
- Viliam1234 2y agoWhat is "ReadonlyRectangle"? Is it just an interface that only exposes read-only methods; or is it an explicit promise that the rectangle is immutable? Perhaps we could go with even more classes. "Rectangle" and "Square" for the read-only methods, without any implications about mutability. "MutableRectangle" and "MutableSquare" for mutable implementations; "ImmutableRectangle" and "ImmutableSquare" for immutable implementations. - "Rectangle" has methods "getWidth" and "getHeight". - "Square" has a method "getSide". - "ImmutableRectangle" implements "Rectangle". - "ImmutableSquare" implements "Rectangle" and "ImmutableRectangle" and "Square". - "MutableRectangle" implements "Rectangle"; has extra methods "setWidth" and "setHeight". - "MutableSquare" implements "Rectangle" and "Square"; has an extra method "setSide". ...or you could just give up, and declare two classes "Square" and "Rectangle" (mutable) that either have nothing in common, or they just both extend some "Shape" class that can paint them and calculate the area.
- Fannon 2y agoHere to recommend this article, really helped me to understand inheritance better. Liskov Substitution is just one aspect / type of it and may conflict with others. https://www.sicpers.info/2018/03/why-inheritance-never-made-any-sense/ https://www.sicpers.info/2018/03/why-inheritance-never-made-...
- rramadass 2y agoVery Good; Gives the "Correct" overview of different usages (i.e. policy) of Inheritance (i.e. mechanism). Quote: Inheritance was never a problem: trying to use the same tree for three different concepts was the problem.
- stmw 2y agoBTW, if not everyone knows who MIT's Liskov is - Turing award winner - https://en.wikipedia.org/wiki/Barbara_Liskov https://en.wikipedia.org/wiki/Barbara_Liskov
- sirwhinesalot 2y agoA problem that only exists in OOP codebases. Just don't do it and avoid the issue entirely.
- whattheheckheck 2y agoWhat do you work on that avoids oop code bases?
- sunshowers 2y agoI've written Rust full time for the last 8 years, being part of teams that have shipped several large, transformative, and basically correct projects. No OOP in sight. It's wonderful!
- sirwhinesalot 2y agoAt work we sadly have to implement very OOP-y standards with all the bullshit that entails. 11 levels of inheritance with overrides all over the place sure isn't fun to deal with. But for things I do myself I use objects and interfaces strictly as a tool to solve specific problems, not as the overall program structure. Most of the time you just need to turn some bits into other bits with a function, no need to overcomplicate things. The question of if a square is a rectangle or a rectangle is a square is the sort of thing that comes from OOP brain-rot. They're just data, and their "isa" relationship is likely not even relevant to the problem you're actually trying to solve, like displaying them onscreen. A "square" could be a function that makes a rectangle out of a single float. A "rectangle" could be a function that produces a polygon. The concepts need not be modeled as types or objects at all. It depends on the actual use case.
- taylodl 2y agoThat's an interesting perspective on inheritance. The problem I see inheritance solving is not having to distribute subtype-specific logic throughout your code base - functions can interact with objects in a generic manner and you get to keep subtype-specific code all in one spot. That's a win. Inheritance isn't the only means for achieving this capability, though. You can also use interfaces and protocols. I prefer to use interfaces. If my class needs to implement an interface than that's explicit: it implements the interface. I can use inheritance if the class really IS-A variant of another class and it can use that base class in fulfilling its obligation in implementing that interface. That's an implementation detail. But the fact it has responsibility for implementing that interface is made explicit.
- rucamzu 2y agoI don't feel the rectangle/square example is valid, given that both alternatives follow different designs - there's no Shape base class in the inheritance example. Moreover, I don't think that switching from a base (abstract) class to an interface is enough on itself to call it composition. The two issues the article mentions have imho less to do with the LSP itself, and more with the limitations that different programming languages have when it comes to define contracts through interfaces (not the same thing), like the lack of exception specs or non-nullability enforcement.
- mega-tux 2y agoSh*t, just realized that Liskov SP collides with LSP from Language Server Protocol :(