Hacker News new | past | comments | ask | show | jobs | submit login

The key is "prefer composition to inheritance" and dates back to Gang of Four.

The word "prefer" is critical to understand. It just means "usually choose A over B" not "B is never the right answer."

Unfortunately, since we - as an industry - like hard and fast rules, we move towards that second explanation and act like inheritance never makes sense. Like any tool, there's a time and place where it is the best tool, other times where it's a reasonable tool, and other times it's a terrible tool.

Therefore "everyone uses it" because either a) it's a reasonable approach or b) the developer doesn't know of or can't use a better one.

We should work on fixing b) instead of denying a) exists.




I agree with this sentiment but I also must say that the time's I've needed inheritance have been few and far between. I have seen really good examples where it works really well (UX is pretty common, but I've also seen really clean cases like data structures with complex interfaces and a simple abstract class).

Where I think inheritance works best is when the state in base classes is limited and the interface is quiet clear. Ideally where you are meant to override is also well defined.

Where it's the worst is when someone tries to use inheritance because they notice coincidentally duplicate code. The worst hell for this is in application configuration. I've seen 5 layer deep inheritance trees to handle really basic things like "what port should this app bind to". It saved no code and introduced a bunch of complication around the transitive dependency baggage it brought on board.

IMO, configuration should always be done via composition. It's more than fine to have a bunch of smaller composable config pieces just so long as you can easily jettison the broken parts.


> I agree with this sentiment but I also must say that the time’s I’ve needed inheritance have been few and far between.

You never strictly need inheritance, but strict need isn’t the criteria for whether something is a good solution (otherwise the fact that most things in programming can be done multiple ways would mean that almost nothing is ever a good solution, since there is almost always an alternative route to the same result.)


I assume the person didn't mean "need" as in "can't do without it", but instead as in "I can make clearer code with it than with the alternative approaches".


> Where I think inheritance works best is when the state in base classes is limited and the interface is quiet clear. Ideally where you are meant to override is also well defined.

What benefit is inheritance providing here? What you described sounds mostly like a struct, at which point the only value the interface provides is possibly some computed fields.


When you scratch deep enough at programming, everything is structs and interfaces defining how you interact with them and how they interact with the world.

The best example of this (IMO) is how `AbstractMap` works in Java. [1]

In order to make a new object which implements the `Map` interface, you just have to inherit from the `AbstractMap` base class and implement 1 method, `entrySet`. This allows for you to have a fully compliant `Map` Object with very little involved work which can be progressively enhanced to provide the capabilities you want from implementing said map.

This comes in handy with stuff we've done when you can take advantage of the structure of an object to get a more optimal map. For example, a `Map<LocalDate, T>` can be represented in a 3 node structure, with the first node representing the year, the second the month, and the final the day. That can give you a particularly compact and fairly fast representation.

The value add here is you can start by implementing almost nothing and work your way up.

[1] https://docs.oracle.com/javase/8/docs/api/java/util/Abstract...


>In order to make a new object which implements the `Map` interface, you just have to inherit from the `AbstractMap` base class and implement 1 method, `entrySet`

Used to think that way, but I now prefer the alternative - passing function(s)/lambda(s) for the necessary functionality of the dependent class.

This way is actually more flexible, as you can change behavior without modifying the override or having a bunch of switches/if-else in your required function.

So instead of 'entrySet' being defined inside MyClass, you would define it outside it, or possible as a static method, and pass it to AbstractMap when you create it.

So you don't need to have every class implement a bunch of interfaces like or Hashable, Orderable, etc. in order get the desired behavior.

Now I guess you would come back about you shouldn't be able to able to do that outside the class, but I also think those are also bad ideas. Python famous gets away with not having private/protected (although there is a way you can kinda of get something similar).


It is just a matter of how flexibility you want to expose through your API. Sometimes a rigid and stricter API is the right choice where you want the API itself as the guardrail against non-standard patterns.


Scopes and closures Give you guard rails and don’t require gluing functions to state unnecessary.


I understand, and what you shared is a perfect example of what I said- but I fundamentally disagree with the notion that it's the same between the two.

I think that in effect, as you associate more behavior with a particular struct(as opposed to what you're attempting to do with said struct), the greater expectation it presents that the struct is what you code around. More and more gets added to state over time, and more expectations about behavior get added that don't need to exist.

Sure, you could say "Well, then just be strict about what behavior is expected in the interface"- but that effort wouldn't be necessary if we didn't make the struct the center of the behavior in the first place.


This works with Rust's traits as well, for example, Iterator. Or Ruby's mixins (which are inheritance, I guess, heh). It is super useful, but doesn't actually require inheritance, even if you can use inheritance to do it.


> The best example of this (IMO) is how `AbstractMap` works in Java.

I think it is fair to say that this is idiomatic Java, and for that reason it is a great example within the context of Java.

But does it translate to the abstract? Given your hypothetical ideal language that allows you to do anything you can imagine as you can best imagine it, is this still a good example, or would you reach for something else (e.g. composition)? Why/why not?


“Prefer A over B” rules usually are problematic because what they usually mean is:

(1) There is a concrete set of cases where A is better than B. (2) But, there is also a concrete set of cases where B is better than A. (3) And, either (a) the cases where A is better than B are more common than the reverse or at least (b) people in the environment where the advice is developed are currently choosing B in cases where A is significantly better.

But, critically, “prefer A over B” does not communicate the set of cases where A is better than B or vice versa, so it tends to lead to the conditions where “prefer B over A” becomes the advice based on (1) and (2) being unchanged but (3)(b) working in the opposite direction.


IMO it's also important to understand the context of when GoF said that - Stroustrup's C++ book was advocating for things like "class WindowWithButtonAndScrollbar inherits from Window, Button, and Scrollbar" at the time!

The GoF book itself is full of examples that use inheritance, but somehow the bumper-sticker-sized advice has taken on a life of its own.


Any abstract advice that's like "do this instead of something else" looks to me like the person had a bad experience and is generalizing from it. This goes both ways, applying to both the original hype about OOP, and this sort of modern anti-OOP hype. It turns out bad code is possible regardless of which design pattern is used.


Prefer IS-A relationships where they make sense, i.e. where you expect the Liskov Substitution Principle to apply. Otherwise use composition. It's really that simple.


Yes and:

I vaguely recall Riel's Object-Oriented Design Heuristics [1996] shared the same advice. https://www.amazon.com/Object-Oriented-Design-Heuristics-Art... https://www.oreilly.com/library/view/object-oriented-design-...

Aside: Young me obsessed over design patterns, methodologies, software engineering, SQA, etc. I had all the books. I started a design patterns study group and hosted it for a while. Now all that "wisdom" is just a bunch of old books. Somehow craftsmanship, me, or both became irrelevant.

Or maybe it was never important.

Like Alistair Cockburn opined about the failure of CASE tools, people somehow manage to ship software successful, without the benefit of all that smart stuff.


I'd be ready to agree if I could be pointed at a time that inheritance actually carries a real benefit- a time you would choose it over composition, if composition is available.


There is no benefit to the "tree of life" single inheritance [1].

What you want to reach for to achieve compositional behavior or polymorphism are type classes, traits, and interfaces. They attach methods to data without the silly "Cat is an Animal, Dog is an Animal" cladistic design buffoonery.

There's nothing wrong with OO, except for inheritance-based OO.

[1] Don't get me started on multiple inheritance. Instead of solving problems, they invented them.


What about a situation where you have an ECS, and if it has say, LifeComponent(), ReproductionComponent(), it can be identified as an Organism (as opposed to say a Crate). Now, you can have inheritance off of that, a composition can be identified as an Orangutan, or a Human, but only if it was an Organism. The Organism is basically a memoization of Life and Reproduction components on the Orangutan.

Now, A human can create a child, but only if the parent object was a "Human." Am I thinking about this in the wrong way?


Yes. I hadn't heard the term tree of life inheritance, but that is the heart of the problem. I think this could be somewhat mitigated by banning access to non-abstract parent methods and all grandparent methods (as well as all relations that aren't directly reachable by going up the tree). But at that point you might as well just use composition anyway.


GreatGrandparent->setCancerChancePercent(10);

Grandparent->mutateCancerChancePercent("+|-", 3);

Parent

Child

By banning access to GreatGrandparent's getCancerChancePercent(); method, I won't know what the starting chance was, which will make it harder to determine nature vs. nurture. Isn't that what ancestry and genome mapping is doing? Going back up the tree?


Inheritance was a premature optimization for computers with small memory. It did its job. However, once memory got big, people forgot to throw it out.

Composition uses a lot more indirection. That's bad on modern CPUs. Pointer chasing throws out performance, so composition is not always preferred, either.


Simple composition, where an entity just has some components on it, can be completely "unrolled" at compile time, essentially removing all indirection as if the entity had all of the data to begin with. You don't need pointers.

For more complex composition, an ECS runtime would likely group all components of the same kind into the same place so that systems that ran with those components get even better data locality.


Composition doesn't imply pointer chasing.

In C++ there is extremely little difference between the code granted for inheritance or composition (unless you use virtual inheritance), so I have no idea what overhead you are talking about.


At least in the case of C#, composition tends to be done via interfaces, which has more indirection than if they were to use an abstract class.

This has been somewhat mitigated in newer versions of runtime.

Not sure what it's like in JVM world, I know they had a better dervirt for a while.


Yeah, but that is a decade later, and also ignores the JIT optimizations like devirtualization, or just like C++ templates, using generics for composition.


> JIT optimizations like devirtualization

JIT devirt can be nitpicky, PGO has improved the situation greatly but until around 6.0 or 7.0 devirt was easy to 'break' in a lot of common scenarios.

Heck, tying into the generic composition bit, ironically static generics still have issues right around devirts. Go figure.


Would you happen to know any good literature with clear composition examples vs inheritance?

The C++ and Python code I see daily is fully inheritance focused, and I would like to understand how it could be done differently (or better) starting from a perspective I understand.



Got a simpler example?


Well in my game everything inherits from GameObject. GameObject has a draw method. Enemies, coins, the player, doors, they're all GameObjects. But when it comes to drawing things, the logic doesn't vary by those subtypes. Rather some things are pixel art with a collection of frame by frame animations. Other things are just a static image. Some things I render as text. Some things are really complex, I render them as composable components with different frames of animation/keyframes, created in a custom character editor program. Each of those render modes I described is a different DrawObject. So instead of having an insanely complex inheritance hierarchy of all permutations of things and how they are drawn (or trying multiple inheritance), I have GameObjects and they contain an property/object of type DrawObject. I have a shallow inheritance hierarchy of GameObjects and a shallow inheritance hierarchy of DrawObjects. And I can compose them together as I see fit.

I even have an AggregateDrawObject class for cases where I need to run two animations at once for a single object.

You can do something similar with UpdateObjects too and have composable behavior.

Note that I still use inheritance, but just don't let it get out of control. Anyone saying "inheritance is bad" is either a moron or really means "deep inheritance hierarchies are bad" or maybe "bad designs are bad".




Consider applying for YC's W25 batch! Applications are open till Nov 12.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: