3 ms·
> One reason is that inheritance is strictly more powerful. When you delegate (compose), you can't modify the inner working of the object you delegated to, you
by lmm 3y ago
> One reason is that inheritance is strictly more powerful. When you delegate (compose), you can't modify the inner working of the object you delegated to, you can only wrap it.
It's more powerful, sure, but in the same way that goto is more powerful than if/for/while. In theory some languages give you the tools for a superclass to expose a stable interface to its subclasses (ish - even Java doesn't give you a way to specify "subclasses may read but not write this field", for example), but in practice they're rarely used (Wicket is literally the only library I've seen to offer an effective inheritance interface); the overwhelming majority of the time inheritance ends in fragile base classes and the resulting bugs.
Which is perhaps inevitable; the trouble with giving you the tools to change the superclass's behaviour in unintended ways is that it results in people changing the superclass's behaviour in unintended ways. If you have a locked-down inheritance model where classes and methods are final by default then maybe you avoid the fragility problem - but by the same token if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition.
> The final advantage is that in some languages the implementation is highly optimized. On HN you'd be forgiven for thinking nobody writes virtual method calls since 20 years ago but in reality virtual method dispatch is so common in real programs that it's heavily optimized by both CPUs (sophisticated indirect branch caching) and language runtimes (PICs in fast VMs like Hotspot or V8). Composition is less well optimized.
Shrug, as the language design consensus shifts, the implementation optimizations will follow. Composition should ultimately be easier to get better performance out of, because it's more constrained (in particular, knowing that a given class or method is final means that you can optimise that class or method much more aggressively).
> So people just aren't exposed to the situations where OOP is useful because browsers weren't designed for UI, and the memory of why it was originally developed atrophies.
AIUI the association between OOP and GUI is more of a historical coincidence than a deliberate design. Though I agree that they do seem to fit together well.
- mike_hearn 3y ago> even Java doesn't give you a way to specify "subclasses may read but not write this field", for example I think I know what you mean, but you can easily define a private field with a protected get method which amounts to the same thing. In Kotlin there's no distinction between properties and fields, so you can write: open class Foo { var thing: String private set protected get } to express that. > in practice they're rarely used I'm not sure I agree. Most Java libraries use it, and it seems to work fine. Mobile UI has used it for many years and that's an absolute ton of code. Game engines use it, games are a huge chunk of the software industry. People would use it a lot more if the web worked differently or if we had better non-web distribution systems on the desktop. > if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition. You can certainly do everything that inheritance allows without support for it in the language, but that doesn't say much. You can also do OOP in C, people do, but it's generally more convenient to have compiler support. Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse. As an example, one of the issues I have with Jetpack Compose is that if you want to customize the controls in any way you have to wrap them. There's no way to say "I want a MyButton that's the same as Button except in this one aspect", because OOP is gone. You have to define a totally new MyButton function/composable that delegates to Button. In JS it's just about tolerable due to the dynamic typing and related features (e.g. spread operator), but in Kotlin it doesn't work despite relying on compiler plugins that change the language's core semantics. You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another, and if you upgrade to a new Compose do you get the new features automatically? No! Due to its browser heritage React conflates widget libraries with theming, so you've had to wrap all the widgets to get anything beyond the basic Material Design look, and that's manually generated boilerplate so you'll have to go and plumb new properties through by hand. There's also no nice way for the authors of Button to define properties that are only intended for customizers vs users because there's no concrete notion of customization at all. Ugh. Now compare to an OOP toolkit: class MyButton extends Button { ... } You upgrade the base libraries and MyButton now has the new features automatically, no code changes required. > knowing that a given class or method is final means that you can optimise that class or method much more aggressively Virtual method calls are treated as final when possible in good language VMs anyway, so it makes no difference. They're only emitted as virtual when there are actually multiple receivers observed at that specific call site, which would have to be implemented as a switch{} in composition, so again, you don't gain anything but do lose language convenience.
- lmm 3y ago> you can easily define a private field with a protected get method which amounts to the same thing. It's not though, a method is significantly more cumbersome. People get lazy and make it a protected field, and probably they themselves remember that the subclass mustn't change it and it's only the poor next person taking over maintenance who gets bitten. > Most Java libraries use it, and it seems to work fine. I mean specifically offering a restricted interface for inheritance by making extensive use of "final", making unstable fields and methods inaccessible etc.. In my experience libraries are rarely careful enough about that, and upgrading a library that you've subclassed from is usually a fraught process. (Wicket is literally the only such library that I've ever been able to fearlessly upgrade) > Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse. I'm genuinely not convinced. I think if you have interfaces and delegation support, then a well designed non-inheritance API (which might involve e.g. splitting what were single classes in the inheritance version into two) can be just as convenient. > You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another Isn't the whole point of delegation as a language feature to avoid that? Can't you just say "delegating this interface to this member" and then all the methods you didn't specifically define get forwarded? > You upgrade the base libraries and MyButton now has the new features automatically, no code changes required. Or it breaks, because the base library changed in a way you didn't expect / didn't take into account the way you've subclassed it.
- _a_a_a_ 3y ago>> You end up with huge screenfuls of boilerplate that grossly exaggerates it. It's annoying but nothing like that.
- mike_hearn 3y ago> Isn't the whole point of delegation as a language feature to avoid that? Yes but Kotlin delegation is implemented for objects and interfaces, not functions. Compose is React inspired so it uses functions with keyword arguments for everything, so if a new keyword argument is used, you have to manually forward it. > Or it breaks It can happen for any kind of API upgrade, especially where you can customize large amounts of complex logic. I don't think it's unusually worse for inheritance based designs. Obviously that's predicated on there actually being a design, but that's not a problem unique to Java (e.g. C libs that export non-stable symbols is a perennial issue).