6 ms·
I feel like inheritance is the actual problem with OOP. Trees of types with increasing specialisations just don't describe many real world problems that well.
by arc776 6y ago
I feel like inheritance is the actual problem with OOP. Trees of types with increasing specialisations just don't describe many real world problems that well.
When you do build programs like this, code gets more and more rigid until it becomes a maintenance nightmare, and extremely expensive to pivot functionality. Sometimes having a complex object hierarchy literally stops you making some new feature.
A lot of mental effort is sucked up in mashing problem spaces into hierarchies, and once there the program is constrained by them.
- zozbot234 6y agoIt has little to do with subtyping per se and everything with the way that implementation inheritance, specifically, breaks modularity. The whole idea of inheritance contra composition is that every call to a virtual method-- including calls internal to the class-- goes through a dispatch step so that the actual code that gets executed depends on whether the object is one of a "derived" class, where that method might have been changed in ways that might break any amount of expected invariants. This introduces brittleness both in base class code (which generally has to call methods that might get overridden in derived classes) and in the derived class itself (which has no way of knowing what invariants might be expected of it as part of base class code). Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
- dragonwriter 6y ago> the derived class itself (which has no way of knowing what invariants might be expected of it as part of base class code). In principle, with a sufficiently-robust type system, the invariants expected by the base class would be encoded in the typing, and the derived class would only be able to narrow them. (Of course, OOP languages tend to either be untyped or have insufficiently robust type systems for this; robust type systems are more frequently associated with functional languages.)
- temac 6y agoIf the type system already trivially enforces your custom invariants, aren't the types defined by your classes already in it?
- zokier 6y ago> every call to a virtual method-- including calls internal to the class-- goes through a dispatch step why would you make internal methods virtual though?
- bregma 6y agoVirtual methods should only be internal methods. There's no point in making the public API have virtual methods. That would be a bass-ackwards parody of proper OO design.
- kragen 6y agoIt's common to put virtual methods in a public API; indeed, in Java we have the "interface" construct consisting entirely of public virtual methods and, occasionally, named constants. (In more recent versions it can also include default implementations.) In Golang the only virtual-method mechanism works by means of a similar interface construct, which also only contains public virtual methods. So, for example, Java's Map is an interface type with, for example, .get() and .put() methods; TreeMap and HashMap implement those methods differently. There are similar things in Smalltalk-80 and the C++ STL. So clearly you think Java, Smalltalk, and C++ are "a bass-ackward parody of proper OO design." I'm interested to hear what systems you consider paragons of proper OO design.
- zozbot234 6y agoInterface inheritance is broadly fine though, because it specifically lacks the conflation of "base class" and "derived class" code. The interface can be assumed to directly imply some invariants, and it's easy to see of any implementation whether it respects them. Everything is nicely localized.
- kragen 6y agoI agree, but I didn't interpret the comment I was responding to as objecting to implementation inheritance as such, rather the contrary: they're objecting to an enormously wide category of OO designs which includes not only interface inheritance but also most, but not all, uses of implementation inheritance; for example, the Smalltalk-80 design that lumps inject and select into some abstract base class for containers.
- arc776 6y ago> every call to a virtual method-- including calls internal to the class-- goes through a dispatch step You've touched on a whole host of performance issues here too. Not only are we making indirection after indirection when accessing virtual methods and data (my poor cache), but invariably these are all stored in heap memory, so we get the worst possible performance profile baked in. > the actual code that gets executed depends on whether the object is one of a "derived" class, where that method might have been changed in ways that might break any amount of expected invariants. On top of that inheritance makes the concrete type unknowable at compile time just to add to the pain. > Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected. Yes this 100%. Inheritance actually stands in the way of code reuse. If I have a class that is kind of similar to another, but different enough to "require" being another class, oh dear they're now incompatible. So you try to kind of fit it into a subclass, but it makes more sense in another, and now you're spending mental effort on a problem created by architecture and not the actual thing you're trying to solve. This is really clear in the game development world, where game objects often just don't fit into the inheritance format, even with multiple inheritance. This is why most complex games are built with the entity component system pattern as it focuses entirely on composition. Perhaps the crux of the issue is that inheritance encourages design by "identity" (mental concept) rather than "attribute" (data). Thus to solve problems we must mentally classify them into some taxonomy as if they are animal species before we even write any code. This is just extra work in exchange for all the issues mentioned.
- temac 6y ago> where that method might have been changed in ways that might break any amount of expected invariants It's not supposed to, and if it does, the design is clearly broken. Now the (rhetorical) question is: does it happens often. The not so rhetorical question is: is it possible to make it happen rarely. The even more interesting question is: is it easy / more practical compared to alternative approaches, and if not then what is the point. My opinion on SOLID is that there is precisely one hard "principle", and it is worthwhile: the LSP (it derives directly from logic, that's why). I believe that Open-close is even borderline insane, and that if there is a crazy way to make it not insane, this way is probably applied by so few people that it may well be irrelevant -- most people will try to apply it in ways that will quickly put them at risk for LSP violation, but LSP is far more important (or they will try to apply it in bad places, but that is another story). Plus programs designed with complex hierarchies are often missing the point of execution contexts, and then understanding them is absolute hell -- their original authors sometimes do not understand them themselves. (The rest of SOLID are soft attempts at fixing self-inflicted wounds, sometimes even reasonable if you insist on doing Javaesque / old-C++-esque OOO, but I digress.) I'm not sure why anybody thought that kind of OOO was a good idea, or that the main characteristic in interesting big programs was the usage of "classes". I even find the suggestion of causation instead of mere correlation dubious; they were already quite a good number of big programs before, and what permitted the explosion of program size was more the ever growing capabilities of computers, that happened, in affordable versions, during the Java-like OOO hype. Besides the simplistic reductionism (we can start with: which classes model entities, which classes model values, which classes are controllers, etc.) which is not too much a big deal in practice, modeling with class diagrams is often missing the river in the middle of the forest for some groups of intertwined trees.
- kragen 6y agoWhat does "OOO" stand for? Usually it means "out-of-order [execution]" but that doesn't make sense here. The Smalltalk-80 container hierarchy demonstrates that you can get a lot of mileage out of simple single-dispatch virtual methods with inheritance. The Smalltalk-78 system, which you can try at https://lively-web.org/users/bert/Smalltalk-78.html https://lively-web.org/users/bert/Smalltalk-78.html (although it's not working for me at the moment), got a multiwindow GUI with an IDE running usably on an Intel 8086 with 256KiB of RAM, in only about 100 classes and 2000 methods totaling 200 KiB of code. This is not what I would describe as a "big program", but it is a fairly impressive program nonetheless. Seeing that kind of thing is what led people to adopt object-oriented programming.
- btilly 6y agoI feel like inheritance is the actual problem with OOP. Trees of types with increasing specialisations just don't describe many real world problems that well. I firmly agree. I wrote https://www.perlmonks.org/?node_id=318257 https://www.perlmonks.org/?node_id=318257 over 15 years ago. I still agree with the fundamental criticism of "OO everywhere" that I offered there.
- GlennS 6y agoThanks, enjoyed this. That SICP quote really nails it too.
- jholman 6y agoThank you for the excellent piece. Though w.r.t. the speculation in your final "Disclaimer", I'd expect the algebraically inclined to prefer either the elegant combinatorical explosion of a J, or the build-it-yourself language of a metaprogrammed Lisp.
- chrstphrhrt 6y agoAgree 110%, and I want to take your notion as a cue to expound a little more from personal trauma :) I find OOP useful for configuration and library-level APIs (e.g. for injecting dependencies and wrapping things), when there is really zero expectation that a user should have to understand the class definition entirely. But that's about it. Where it seems to break down for me is when classes are actively used at the application implementation level for general encapsulation. E.g. data access layers. This can be done perfectly fine with modules/packages. Adding inheritance is just asking for trouble in a team setting IMHO. Trouble comes when there is no clear peer review and style policy to avoid classes for anything other than config or distributing libraries (as separate projects). What ends up happening is a proliferation of subclasses or method overrides when developers are in a rush to ship features without understanding the whole codebase. This is a technical loan with very high interest. It makes sense rationally in the moment, as classes have an inviting feel to the user as a kind of grab bag of related functionalities that are easily introspected (at first). Compare it to searching through docs for all the different namespaces in a package and learning what types their various functions support, it requires more thought and grasping the concepts of the library. Alternatively, a class with a broadly defined purpose starts to look like a common utility to throw things at. It's a grab bag of stuff that is more amenable to hacking with blinders on, in a way. Next thing you know the chains of inheritance and method overrides have grown into a very hard to disentangle hydra. The accumulated overhead of maintaining it is compounded by the debugging challenge of knowing where exactly a given instance is coming from and which layers it has been through on its way to a given breakpoint.
- xt00 6y agoThe concept of "if you want to touch this code base you have to understand the entire thing" is pretty tough on basically everybody but the people who either wrote most of the code or who live in that code base all the time. It would seem like code-bases should (either because people architected it to be possible to work with without understanding the whole thing or the programming language helps facilitate it) be able to be worked with while only understanding a portion of the code base. In some cases classes and interfaces help with that, but in plenty of cases you end up in one of the sort of child classes trying to figure out how to get out of the hole you are in where you can't interact with anything anywhere in the code base and you have to basically plumb pipes through lots of places to allow moving data where you want.
- GlennS 6y agoAgree completely, that's a really nice summary of the key problems. The mental effort of building up taxonomies in particular is exactly what put me off inheritance (and fussy type systems in general). It's too easy to jump down that rabbit hole without considering whether it's a productive activity.
- im3w1l 6y agoThis is highly codebase dependant. I have seen code with crazy hierarchies like you say, and ones without it. It's possible to "just don't do that".
- aryehof 6y agoPerhaps an issue is that the well known saying "prefer object composition over class inheritance" isn't done, because a majority of programmers simply don't know how to apply it. Inheritance is used inappropriately (outside of frameworks) almost everywhere.
- deleted 6y ago[deleted]