Live data from Hacker News

Liskov Substitution: The real meaning of inheritance

cekrem.github.io

31–40 of 92 posts

Re: Liskov Substitution: The real meaning of inheritance

#31
post #24
post #16

Earlier quoted context omitted.

> 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. Bah. There are completely legitimate uses of inheritance where it's a really great fit. I think you'll find that being dogmatic about avoiding a programming pattern will eventually get yo…

I happily admit it's more than possible to come up with examples that make inheritance shine. After all, that's what the authors of these books and articles do. But most of them put the cart before the horse (deliberately design a "problem" that inheritance "solves") and don't seriously evaluate pros and cons or even consider alternatives. Even then, some of the examples might be legitimate, and what you're referring…

I agree that examples matter a lot, and for some reason a lot of introductory OO stuff has really bad examples. Like the whole Person/Employee/Employer/Manager dark pattern. In no sane world would a person's current role be tied to their identity--how do you model a person being promoted? They suddenly move from being an employee to a manager...or maybe they start their own business and lose their job? And who's modeling these people and what for? That's never shown. Are we the bank? The IRS? An insurance company? Because all of these have a lot of other data modeling to do, and how you represent the identities of people will be wrapped up in that. E.g.--maybe a person is both an employee and a client at the same time? It's all bonkers to try to use inheritance and subtyping and interfaces for that.

Algebraic data types excel at data modeling. It's like their killer app. And then OO people trot out these atrocious data modeling examples which functional languages can do way better. It's a lot of confusion all around.

You gotta program in a lot of different paradigms to see this.

Re: Liskov Substitution: The real meaning of inheritance

#32

From 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 beca…

I’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.

Re: Liskov Substitution: The real meaning of inheritance

#33

SOLID 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…

The rationale for Dependency Injection was never _just_ about "making testing static methods" easier. In fact, Dependency injection was never about static methods at all. No DI advocate — not even the radical Uncle Bob — will tell you to stop using Math.round() or Math.sqrt(), even though they are static methods.

The main driver for dependency injection was always to avoid strong coupling of unrelated classes. Strong coupling can be introduced by cases like Class A always instantiating a class B which is a particular subtype of class S (i.e. giving up the Liskov substitution principle), Class A initializing class B with particular parameters that cannot be extended or overridden, Class A calling a static method or a singleton method which modifies or reads a global value.

Strong coupling makes you lose on flexibility, reusability and code readability. If you need to modify how either class A or class B behave later, you may now need to painstakingly scan all the places BOTH classes are used (and all the places other classes touching them are used) and modify the way they are constructed. If you want to enable OrderProcessor to accept bank transfers, but it was built to always call "new CreditCardProcessor()" internally inside its constructor, you will now have to find every place CreditCardProcessor is constructed and modify it. The worst offenders I've seen are pure logic classes that have no business having side side effects, but still end up opening multiple files, or doing a bunch of HTTP requests that you cannot avoid, because their authors just thought: "Cool, I can mock all this stuff with PowerMock while testing!"

The other issue I mentioned is code readability. This is especially an issue with singletons or static methods that mutate global state. You basically get the dreaded action-at-a-distance[1]. You might initially write a class that is using a singleton UserSessionManager object to keep track of the current user session. The class only operates on simple single-threaded scenarios, but at one point some other developer decides to use your class in a multi-threaded context. And Boom. Since the singleton UserSessionManager wasn't a part of the interface of your class, the developer wasn't aware that it's being used and that the class is not ready for multi-threaded contexts[2]. But if you've used DI, the dependencies of the classes would have been explicit.

That's the true gist of DI really. DI is not about one heavyweight framework or another (in most cases you could do it quite easily without any framework). It's also not a pure OOP technique (it is common used in functional languages too, e.g. with Reader Monad). Dependency injection is really just about making your dependencies explicit and configurable rather than implicit and fixed.

As a tangent, mocking static methods was possible for a rather long time. PowerMock (which allows mocking statics with EasyMock and Mockito) was available at least since 2008, and JMockit is even earlier, available at least in 2006[3]. So mocking static methods in Java has been possible for a very long time, probably before even 5% of the Java programmers have even started using mock objects.

But it's not always ideal. Unfortunately, tools like PowerMock or JMockit static /final mocking are working by messing with the JVM internals. These libraries often broke down when a new version of Java was released and you had to wait until the compatibility issue was fixed. These libraries also relied on tricks like custom classloaders, Java Instrumentation Agents and bytecode manipulation. These low-level tricks don't play way with many other things. For instance, if you are using a framework which needs its own custom class loader, or when you're using another tool which needs bytecode manipulation. I was personally bitten by this when I wanted to implement mutation testing[4] in Java, and I couldn't get it to work with static mocking. Since I believe mutation testing carries more value than the convenience of being able to mock statics for testing, it was an easy choice to dump Powermock.

[1] https://en.wikipedia.org/wiki/Action_at_a_distance_(computer...

[2] https://testing.googleblog.com/2008/08/by-miko-hevery-so-you...

[3] http://butunclebob.com/ArticleS.MichaelFeathers.ItsTimeToDep...

[4] https://en.wikipedia.org/wiki/Mutation_testing

Re: Liskov Substitution: The real meaning of inheritance

#34
post #31
post #24

Earlier quoted context omitted.

I happily admit it's more than possible to come up with examples that make inheritance shine. After all, that's what the authors of these books and articles do. But most of them put the cart before the horse (deliberately design a "problem" that inheritance "solves") and don't seriously evaluate pros and cons or even consider alternatives. Even then, some of the examples might be legitimate, and what you're referring…

I agree that examples matter a lot, and for some reason a lot of introductory OO stuff has really bad examples. Like the whole Person/Employee/Employer/Manager dark pattern. In no sane world would a person's current role be tied to their identity--how do you model a person being promoted? They suddenly move from being an employee to a manager...or maybe they start their own business and lose their job? And who's mode…

I love your concrete examples!

Thanks for sharing the pointer to your wasm engine. Is that part of a course you teach, or something born out of an auto-didactic pursuit?

Re: Liskov Substitution: The real meaning of inheritance

#35
post #34
post #31

Earlier quoted context omitted.

I agree that examples matter a lot, and for some reason a lot of introductory OO stuff has really bad examples. Like the whole Person/Employee/Employer/Manager dark pattern. In no sane world would a person's current role be tied to their identity--how do you model a person being promoted? They suddenly move from being an employee to a manager...or maybe they start their own business and lose their job? And who's mode…

I love your concrete examples! Thanks for sharing the pointer to your wasm engine. Is that part of a course you teach, or something born out of an auto-didactic pursuit?

User "titzer" is Ben Titzer; co-founder of WebAssembly - https://s3d.cmu.edu/people/core-faculty/titzer-ben.html

Re: Liskov Substitution: The real meaning of inheritance

#36
> 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.

Re: Liskov Substitution: The real meaning of inheritance

#37

If 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.

> 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.

Re: Liskov Substitution: The real meaning of inheritance

#38
post #4

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…

Inheritance is nothing more or less than composition + implementing the same interface + saving a bit of boilerplate.

When class A inherits from class B, that is equivalent to class A containing an object of class B, and recoding the same interface as class B, and automatically generating method stubs for every method of the interface that call that same method on your B.

That is, these two are perfectly equivalent:

  class B {
    public void foo() {
    } 
  } 

  class A_inheritance extends B {
  }

  class A_composition implements B {
    private B super;

    public void foo() {
      super.foo();
  }
This is pseudo-code because Java distinguishes between Interfaces and Classes, and doesn't have a way to refer to the Interface of a Class. In C++, which doesn't make this distinction, it's even more equivalent. Especially since C++ allows multiple inheritance, so it can even model a class composing multiple objects.

The problem with inheritance is that people get taught to use it for modeling purposes, instead of using it for what it actually does - when you need polymorphism with virtual dispatch, which isn't all that common in practice. That is, inheritance should only be used if you need to have somewhere in you code base a collection of different objects that get called in the same way but have to execute different code, and you can only find out at runtime which is which.

For your students and employees and users example, the only reason to make Student and Employee inherit from User would be if there is a reason to have a collection of Users that you need to interact with, and you expect that different things will happen if a User is an Employee than if that User is a Student. This also implies that a Student can't be an Employee and vice versa (but you could have a third type, StudentEmployee, which may need to do something different from either a Student or an Employee). This is pretty unlikely for this scenario, so inheritance is unlikely to be a good idea.

Note also that deep class hierarchies are also extremely unlikely to be a good idea by this standard: if classes A and B derive from C, that doesn't mean class D can derive from A - that is only useful if you have a collection of As that can be either A or D, and you actually need them to do different things. If you simply need a third type of behavior for Cs, D should be a subclass of C instead.

I'd also note that my definition applies for inheritance in the general case - whether the parent is an interface-only class or a concrete class with fields and methods and everything. I don't thing the distinction between "interface inheritance" and "implementation inheritance" is meaningful.

Re: Liskov Substitution: The real meaning of inheritance

#40
post #21

Earlier quoted context omitted.

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 wan…

I see no need for inheritance there, that can and should be done using interfaces eg in the contrived example I gave, any of all three of the User, Student and Employee can implement the interface (and if needed Student could simply delegate to its internal User while Employee could "override" by providing its own, different implementation)

What difference do you see between implementing an interface and inheriting from a class, that makes one good and the other bad?

I'm asking beyond the arbitrary distinctions that some languages like Java or C# bake in.

Post reply on HN