Hacker News new | ask | show | jobs
by colllectorof 1820 days ago
>In all my experience with OOP, it's always been inheritance that is the root of all evil.

It's not, though, and the fact that people keep repeating this meme shows that most developers don't even bother thinking about issues they face beyond superficial blamesplaining.

The reason inheritance causes so many issues in languages like Java is because they are statically typed and also use classes as types[1]. Classes must be somewhere in the inheritance tree, hence you are forced into some place of that tree. To make things worse, Java has many keywords that restrict what inheritor of a class can do (private, final, etc).

Inheritance is much less troublesome in, say, Smalltalk, since the language is dynamically typed. If someone expects you to implement Foo, you can (almost always) just implement its relevant methods without explicitly extending the class. Thus, a whole host of annoying scenarios simply does not occur.

--

[1] BTW, this breaks one of the fundamental commandments of classic OOP: you should not depend on implementation details of an object, only on its message protocol. Obviously, it's impossible to be independent of implementation details if some library forces you to use a particular class.

5 comments

You're describing interface-based polymorphism, which is what go and rust use. In go, I can have a struct with methods that implements a particular interface by implementing all the methods described in that interface, but I can't inhereit from another struct. The person you're replying to called this out as a better system too.
Polymorphism is good. You describe polymorphism.

Inheritance is bad. Inheritance is patching a class and overriding some of its methods, while leaving others intact. This brings all kinds of unexpected interplay between methods of different levels of overriding. A typical example is http://www.cse.psu.edu/~deh25/cmpsc473/jokes00/joke01.html

Ideally all "concrete classes" with method implementations should be final, and the polymorphism should be achieved via interfaces / typeclasses / traits, or purely abstract classes where these are not available. Reuse of implementation should be achieved via composition; there are several ergonomic ways to express it.

> Simulator supervisors report that pilots from that point onward have strictly avoided kangaroos, just as they were meant to.

Won't fix; working as intended.

> Polymorphism is good. You describe polymorphism.

It also seems like they're describing the nominal typing of Java versus a structural approach.

Haskell and, IIRC, Rust allow you to declare that a certain data type conforms to some interface, and describe how, by listing / adding the functions with necessary signatures.

This allows to have the upsides of structural polymorphism without losing static checks.

Go, OTOH, goes all the way structural.

> Haskell and, IIRC, Rust allow you to declare that a certain data type conforms to some interface ...

I believe at least in the case of Haskell, you are referring to type classes[0].

0 - https://wiki.haskell.org/OOP_vs_type_classes

I'm unfamiliar with anything like that in Rust, beyond Rust's structural approach to tuples. I'd love to hear more about it, though!
I think they're just talking about how you have to declare what trait a function implementation is for, rather than having it derived from the type signature alone. The `impl Trait`[0] syntax. In Go, you don't need to declare that the function implementations are being implemented for a particular interface, you just have to match the type signatures and function names.

Rust's way can help avoid some errors. You can't accidentally implement an interface, whereas in Go you can if you happen to implement a group of functions with appropriate names and type signatures. It's unlikely to cause actual bugs (you'd have to misuse the resulting implementation) but can be conceptually somewhat confusing.

[0] https://doc.rust-lang.org/book/ch10-02-traits.html#returning...

> It's not, though, and the fact that people keep repeating this meme shows that most developers don't even bother thinking about issues they face beyond superficial blamesplaining.

I don't know that I'd say "inheritance is the root of all evil" (there are lots of antipatterns in OOP that are unrelated to inheritance, like Joe Armstrong's banana-gorilla-jungle observation) but I will say that inheritance is pretty close to useless in the best case and harmful in most cases. And I say this as someone who learned to program and then became a professional programmer when OOP was all the rage. I was taught OOP without the previous bias of other paradigms; it was only after learning other paradigms that I was able to articulate frustrations I was having with OOP. The implication that people who criticize inheritance in this way "haven't bothered to think" is patently false in the best case, and laughably arrogant in the worst case.

> The reason inheritance causes so many issues in languages like Java is because they are statically typed and also use classes as types[1]. Classes must be somewhere in the inheritance tree, hence you are forced into some place of that tree. To make things worse, Java has many keywords that restrict what inheritor of a class can do (private, final, etc).

Fear not, Python is dynamically typed and inheritance is a mess there as well.

> If someone expects you to implement Foo, you can (almost always) just implement its relevant methods without explicitly extending the class.

This is just structural subtyping (see Go's interfaces for a statically typed example of structural subtyping) also known as "duck typing". It seems like you're positing that the problems with inheritance derive from nominal subtyping (e.g., Java's `implements` keyword), but these things are orthogonal. Python has duck typing ("structural subtyping") and its inheritance is no less painful than Java's. Similarly, Rust has nominal subtyping (a type must explicitly implement a trait) and it has none of the inheritance-related problems that Python and Java have.

I feel like OOP always had the nerd catnip problem. Since the very beginning the various programming tutorials would have the contrived examples of animals and canines and dogs, or geometric shapes and triangles etc. which just managed to ring a particular very satisfying bell in people's heads. It was just such a neat concept with those examples that just made sense. How it turned out in practice is a different story but I feel this had a lot to do with the enthusiastic uptake.
1983 Smlltalk-80: The Language and Its Implementation by Adele Goldberg and David Robson had pretty good example with none of this animal/mammal/dog crap. Not sure when the trend for giving awful examples like this really started, but I don't think it was "from the very beginning".
To me there are about three tiers of this basic insight:

1. Inheritance causes all kinds of issues so you shouldn’t use it.

2. Actually, inheritance is fine as long as you do it right (e.g. Liskov)

3. Actually, getting part 2 right is difficult, and the heavy risks of getting it wrong aren’t worth the minor benefits of inheritance.

> Inheritance is much less troublesome in, say, Smalltalk, since the language is dynamically typed. If someone expects you to implement Foo, you can (almost always) just implement its relevant methods without explicitly extending the class.

Sorry, I don't understand this sentence. Isn't inheritance simply a way to avoid writing duplicate code? If you write the code to implement methods, isn't that not inheritance anymore?

Inheritance conflates code reuse ("avoid writing duplicate code") with polymorphism (allowing for multiple different instances to implement the same interface). It also allows for trampolining method calls up and down a hierarchy (a method in a base class might call another method which might be overridden by another class in the hierarchy).

Outside of OOP, we use composition for reuse and interfaces for polymorphism, and we don't trampoline method calls up and down a hierarchy because it's (probably?) always a bad idea. When we really need reuse and polymorphism, we can use both composition and interfaces, since the two are correctly orthogonal.

> Inheritance conflates code reuse ("avoid writing duplicate code") with polymorphism (allowing for multiple different instances to implement the same interface).

Note that languages like C++ allow for inheritance without polymorphism, i.e. pure implementation inheritance.

However, I also think that composition should be preferred whenever possible.

What the grandparent post means is that in dynamic languages you can just implement one of the "base" methods yourself instead of inheriting from a class that's bigger than you need, in order to avoid problems. I personally don't have an opinion on that, but it's not something I'd do myself.

Also, like the sibling said, inheritance is a tool that does multiple things: code reuse, which we call implementation inheritance, being the one everyone hates (the age-old advice is to use composition for code reuse instead), and interface inheritance being the one everyone loves.