Composition
If you can describe it with “has a”, use composition; only use inheritance for “is a”.
The way I read that is, if there’s no real inheritance or dependency relationship between them and you’re only after code reuse, composition is the best way to go. “Is a” itself means there’s a trait that covers you, you have to satisfy that trait, so you have to inherit. The subclass has to be “type-equivalent” to the parent in every situation, and that’s the Liskov Substitution Principle. But “has a” has no such covering, so there’s no need to force inheritance. If I inherit just to borrow some ability from a class, that inheritance is unnecessary, and it gives me coupling I didn’t need. Later, if the parent has to change, every subclass can get dragged in. You have no idea which subclass inherited which parent feature that you just changed.
Concretely, composition is a delegation pattern. We talked about DI before, and dependency injection is the perfect match for composition. Delegation in composition is what “only caring about structure” looks like. I need your feature, but I don’t care how you implement it. All I know is that you have this structure (or the methods and types I can work with). I called your method (which necessarily fixes the input and output), I used your types, and that’s where it ends. How you do it is none of my business, that part I “delegated” to you, and I only accept an implementation that DI hands me from outside.
This idea is Inversion of Control (IoC). I don’t care about the things I depend on, I leave them to the outside to implement. I don’t have to build the concrete implementation inside, I only care about the part I’m going to use. It’s a contract at the structural level, and I’m only answerable to the interface. So you could say IoC is interface-oriented programming taken as far as it goes, and its abstraction sits higher than object orientation. An object can be a concrete implementing class, but an interface is only structure, and can have no implementation at all.
The extreme shows up in the literal meaning of “inversion of control”. The actual business classes don’t care at all when something gets built or what gets used, the DI container handles that. A class cares only about its own scope and answers only for itself, everything else it ignores, or rather, hands off to the outside. It’s like building blocks. A block isn’t responsible for where it ends up, and a pile of Lego can be built into anything. The person building the Lego is the DI container.
Design ideas in software are obviously influenced by industry, or rather, they have something in common with it. Standardization, replaceability, decoupling, all of them package concrete functionality into parts you can swap and assemble at any time. It’s more like two different fields converging on the same answer. To solve complicated real production problems, where countless people cooperate without knowing each other and business situations and requirements shift at any moment, modularity looks like the best workable direction for a system that has to hold up.
There’s a more general principle behind this. For a complex system to stay controllable, it has to hide information at some level of granularity. A part doesn’t need to know the design of the whole machine, a function doesn’t need to know the architecture of the whole system, a cell doesn’t need to know the intent of the whole organism. Complexity can only be managed by shielding it in layers, and that’s close to a universal rule. by claude
I agree with that. In a complex system each actor answers only for itself, or rather, it can only answer for itself, because nobody has global information. Look at society and it’s a nested modular structure. People make up organizations, countless organizations make up a country, countless countries make up human society. Every layer is a module, but the rules a module runs by can be completely different at different layers. The biggest difference between society and software engineering is that software is designed top-down while society emerges. But not entirely. We talk about sh*t-mountain code, and what that says is that nobody can really design a perfect architecture. A system that keeps running always drifts toward chaos, and in the end it becomes something grown rather than designed.
I doubt even Microsoft could claim it understands one hundred percent of how its own huge system works in detail. What they do is patch problems as problems show up and keep things consistent overall. Who knows whether the bottom layer now runs on bugs, with five hundred chained bugs happening to make everything fine. That’s the complexity of large systems. In other words, shifting situations make every design fail, because you have no global information and you can’t predict requirements. So there’s no best design, only a good-enough design that lowers the odds of trouble but can’t promise there won’t be any. As for the rest, wait until something actually breaks and then patch it. Since everyone gets things wrong, design matters for this: with a design you may still get it wrong, but at least it’s fixable and locatable. Without one, when it goes wrong you probably can’t even find where the problem is.
Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: 组合.