by josephg | 2025-05-09 00:05:56+00:00
> Human = torso, legs, arms. Three distinct objects combine into one thing. A human by definition is the union of these things. It's fundamental. It's just your bias is trying to see it as something else.

No, it’s not. If I put a torso, legs and arms (and perhaps a head) on a table, I don’t get a human being. I’d say a human composes all of those things (and more!). But a human doesn’t inherit from them. For example, each leg can kick(). But you can’t inherit from two legs! And if you did, which leg is the one that kicks when you call the function? Much better to have human class which contains two legs. Then human can have a kick(RIGHT_LEG) function which delegates its behaviour to right_leg.kick().

There’s lots more ways composition helps us model this. Let’s say we want to model blood temperature, which is tracked in every limb separately. Composition makes it more straightforward to have different behaviour (& state) in the Body class for blood temperature than in any of the limbs. (Eg maybe the blood temperature is the average of all limbs temperature. That is more straightforward to implement with composition.)

> inheritance minimizes code copying. It reuses code in the most efficient way possible.

On the surface, I agree with this claim. But it’s funny - programs which make heavy use of OO always seem unnecessary verbose. I wonder why that is? I’m thinking of Java where it’s common to see utility classes that just have 1 or 2 fields take up 100+ lines of code, due to class boilerplate, custom hashCode, toString and isEqual methods and all the rest.

But in any case, as you say, we aren’t just trying to optimise for the fewest lines of code. We’re trying to optimize for how easy something is to read, write and maintain. Adding code to increase simplicity is often worth it. Inheritance increases complexity because it adds a layer of hidden control flow. When I’m reading a program, I need to do a lot of work to figure out if foo.bar() is calling a function in one of the base classes or in the derived class. As you say, humans don’t deal with that kind of complexity well. In general, explicit is better than implicit - this.leg.kick() is more explicit than this.kick() when kick() exists somewhere in one (or more?) of the base classes.

Also let’s say I have 3 classes A, B extends A and C extends A. If there’s a bug in B that involves something in the base class A, fixing that bug may break implicit invariants in C. This kind of “spooky action at a distance” is horrible. Ironically, it violates the principle of encapsulation that OO claims as one of its core principles. I find this kind of problem is rarer in compositional systems. And when it happens, it’s usually much more straightforward to debug and fix. The reason is because classes are all self contained. You don’t have partially-specified base classes that only kinda sorta maintain their invariants. And derived classes don’t implicitly include their base class’s behaviour. As a result, there’s less implicit entanglement. B and C can much more easily change how they wrap A’s behaviour.

At the end of the day, I think we more or less agree that inheritance makes code harder to reason about. I don’t write code to express a pure conceptual representation of the world. (And neither should you!). I write code to get stuff done. If inheritance makes it harder for humans to be productive with our software, then that’s reason enough to abandon it.