Hacker News new | ask | show | jobs
by sqeaky 695 days ago
This is a strong argument against inheritance, but that isn't everything about OOP. Just one well supported advanced abstraction (That I would also argue should be rarely used.)

I would argue that just having strong type system and bundling methods with data gets you the vast majority of the usefulness of OOP. Liskov, Open/Closed, Message Passing, and other theoretical abstractions be damned.

EDIT - Where are the good places to use inheritance?

There are only a few I can think.

One is when you are trying to create a system that inverts dependencies by allowing a plugin system or follows some sort of nuanced workflow that others might want to "hook into". But that isn't the only way to do that, maybe other ways would be better like passing in functors.

Another situation I have seen recently is when creating a kind of data or messages that differ only by type and maybe a few small pieces of behavior and they are all known up front.

4 comments

> I would argue that just having strong type system and bundling methods with data gets you the vast majority of the usefulness of OOP.

Yes, a module system brings almost all of the advantages of OOP. The one remaining is structure abstraction (things like interfaces on Java derived languages, or type classes on Haskell derived ones).

But well, none of those are even typically associated with OOP. The OOP languages just have those features, like they have variables too.

Yep. Rust has all of these features (modules, structs with associated methods and type classes (traits)). But nobody thinks of it as an OO language. In fact, I’ve heard that many people struggle with rust if they’ve come from a heavily OO language like Java. You have to structure your code a little differently if you don’t have classes.

Modula apparently had many of these features too - and that predated what we now think of as object oriented programming. The good parts of OOP aren’t OOP.

> One is when you are trying to create a system that inverts dependencies by allowing a plugin system or follows some sort of nuanced workflow that others might want to "hook into".

I’m fairly certain that’s the use case of inheritance - at least in the Simula tradition. Classes as a means of lifetime management, moving parts that have well defined steps of operation (methods), and interchangeable parts (subtypes) which you can more or less slot into the larger system (polymorphism).

It’s easier to think about classes not as nouns, but as verbs over time (or rather, bounded by time): at a specific moment in the assembly line, call this particular method, at another moment, call that other method…

Object oriented programming in the Simula tradition I would even go as far as to say is just best practices in structured/procedural programming taken to their logical extremes.

> Classes as a means of lifetime management

Wrt plugin systems: at least as the class level, are classes really a means of lifetime management in practice?

IIRC, audio plugin APIs follow the shell command pattern of memory management for loading new classes-- the user dynamically loads a library into a running instance of an application, and there it stays until the application exits.

And even if plugin systems as implemented are actually unloading classes, the user is almost always just restarting the app to make sure it took. :)

Edit: clarification

Agree 100%: static typing (for code completion) + method/data bundling is the major win in OO, and it rarely gets talked about for whatever reason.

It's unfortunate that inheritance became such a major focus of practical OO languages. Would love to see a composition-first OO language. Might have its own problems, but would at least be interesting.

Go, Rust, Zig, etc all support static typing and method/data bundling without any explicit language support for implementation inheritance (interface inheritance in general and especially when structural rather than nominal is not nearly as much of an issue and doesn't create strict tree hierarchies).

Rust has support for variance and subtupint so perhaps it's not as pure of an example, but it's pretty heavily restricted.

Zig's support for method/data bundling being used for "objects" isn't even first class so I wouldn't call it OO (object-oriented) so much as object-orientation-capable with less fuss than if one wanted to build their own objects system in C.

Even in C++ the last time I thought I might need inheritance I made a simple class/struct with a few members that were `std::function` instances. Instead of needing inheritance this worked and I managed to keep type safety checks on all function return and parameter types. Once upon a time this would have been weird function pointers and `void*` with dangerous casts. Last month when I did it, there were just lambdas passed to typesafe constructors.
Go's first class support for typed return tuples and Interfaces is a lovely replacement for inheritance (E.G. an Interface of type blah supports this signature). They function as an API contract, if a given class implements the requirements of the Interface, it can be cast to and used as that anywhere which accepts that interface.
It's not unfortunate happenstance, it's by definition.

Dynamic dispatch is the defining feature of object-oriented programming. In dynamically typed languages such as Smalltalk, you can get there with duck typing. But a statically typed language needs a statically typed mechanism for dynamic dispatch, and that requires some way of saying, "Y is a particular kind of X, so all members of X are also in Y." Which is - again by definition - inheritance.

You could remove - or refuse to use - the inheritance (or, equivalently for some purposes, duck typing). But that would also prevent the use of dynamic dispatch, so what you're doing would bee be procedural programming, not OOP, even if you're using an object-oriented language to do it.

> Dynamic dispatch is the defining feature of object-oriented programming.

Message passing is the defining feature of object-oriented programming. Dynamic dispatch can be achieved using message passing, but message passing is more than dynamic dispatch.

Ultimately, static typing is incongruent with object-oriented programming. Messages are able to be invented at runtime, so it is impossible to apply types statically. At best you can have an Objective-C-like situation where you can statically type the non-OO parts of the language, while still allowing the messages to evade the type system.

Whether you'd call "composition-first" is probably asking for a big argument about what "composition first" really means, but Go is certainly a language that syntactically privileges a particular type of composition over inheritance. It doesn't even have syntax for inheritance, and frankly even manually implementing it is rather a pain (best I've ever done requires you to pass the "object" as a separate parameter to every method call... and, yes, I said that correctly, to every method call).

I'm not ready to try to stake a position on the top of some "composition first" hill because the syntactic composition it supports is not something I use all the time. It's an occasional convenience more than a fundamental primitive in the language, the way inheritance is in inheritance-based languages. Most of the composition is just done through methods that happen to use in composed-in values, but it is generally not particularly supported by syntax.

Typescript and it's structural typing my be what you're looking for.
Inheritance is just plain a great way to model a lot of relationships, in my experience, because a lot of things are most easily thought of as "x is a kind of y". I am perennially baffled that people shit on inheritance so much, because I think it's incredibly useful. I find myself often missing inheritance when working in Rust, for example.
Implementation inheritance often leads to code that is just awful to read. If class C extends class B and class B extends class A, then to find out what `new C().foo()` actually does, you need to read through the whole C-B-A hierarchy, bottom to top. If `A.foo()` calls `this.bar()`, you have to start again, from the bottom of the hierarchy. With an inheritance hierarchy of depth n, every method call could be going any of n different places. With an interface, there's a single level of indirection. With composition, the code simply tells you what happens next.

If class A and class B both implement interface X, and B wants to borrow code from A, it should just call A's methods—ideally, static methods, but B can keep an instance of A if it wants. Explicit is better than implicit.

Also, I dislike ontological statements like "x is a kind of y." What does that mean? Typically, it's a claim about behaviour: "x offers method w and satisfies invariant v". But the actual blueprint here is an interface, (w,v)—not another object y. The waters get even muddier when we start talking about "is-a" vs "has-a" relationships. It feels like OOP is trying to unhelpfully distance us from what's actually going on with our code. Under the hood, inheritance is no more than syntactic sugar for composition. I think that OOP's focus on the ontological philosophy of inheritance is the reason why it led to so much bad AbstractObserverStrategyFactory-style code.