Skip to content(if available)orjump to list(if available)

Inheritance and Subtyping

Inheritance and Subtyping

52 comments

·January 29, 2025

caspper69

This has been coming up a lot lately.

It seems to me that a new generation of developers are simply rejecting oo-paradigms because they are oo, without an in-depth analysis.

I agree with the general consensus that oo inheritance hierarchies of the past became too deep and were overwhelming, but subtyping has a place.

Because interfaces are stateless, you generally cannot completely implement required interface functionality without wiring them up to each individual object, resulting in a lot of boilerplate code repetition.

This is exacerbated in GUI programming, for example, where subtyping makes a lot of sense (and doesn't devolve into Animal -> Rabbit nonsense).

With subtyping, you can take an object that is 95% of another object and just override where necessary.

I also don't believe composition provides the same level of encapsulation that can be achieved through traditional class-based oo. There seems to be this prevailing view that oo is all about inheritance, when the truth is that oo was really about encapsulation and isolation. You only expose what you need to, and objects (whether they be classes or structs or whatever) can be built and tested without fear of code collisions or meddling from the outside view.

I will concede that interfaces can and do provide a level of encapsulation, but in practice, it's just clunky.

Just look at some Rust objects and the sheer number of traits that they must be aware of and implement by hand. It's piecemeal when it could be one and done.

And please understand, I'm not talking about "...in the beginning there was a GObject, and that GObject bequeathed to...and..."

int_19h

This boilerplate repetition that you refer to is only necessary when doing composition because the languages don't provide the requisite syntactic sugar for it, the way they do for inheritance. This is very unfortunate but totally fixable.

caspper69

I'd be interested to understand how / what types of proposals exist to solve this very real ergonomic issue.

As I stated in my original post, the implementations of interfaces/composition that I have used (primarily C# and Rust) are stateless, so you must always re-implement. And honestly, if Rust were to say, allow the specification of struct members in trait definitions, one could argue that their structs are really sealed/final classes, just using different keywords.

I am genuinely curious, because I do believe that people have a valid point that composition does tend to lead to less object spaghetti than inheritance, even if I can't quite pinpoint why from a purely theoretical standpoint (my belief is that they're "holding it wrong").

int_19h

For example, Kotlin has syntactic sugar specifically to implement interfaces by delegating to an inner object (with overrides etc): https://kotlinlang.org/docs/delegation.html

impulsivepuppet

There are more interpretations of "OO" than there are people, but the overall direction is that OO isn't a panacea for code maintainability, and some frameworks ahem Spring create a mental model at times so distant from OOP, that I might as well have written everything in Python. I gain nothing from having classes when one half are records and the other half are singletons.

vips7L

Inheritance is just a tool. It’s insane to reject it outright just because someone can use it incorrectly. It does some things extremely well and better than composition.

stephantul

I think this is interesting, but I think the way Python is described is slightly wrong? In Python, you don't need to define a protocol to support duck typing, you can just implement the same behavior. Protocols don't have a function outside of static typing (they don't influence any program behavior at run-time.)

I guess it depends on whether you consider duck typing to be "passes the static type checker" or "it works the same".

alfons_foobar

True.

Also this:

"Because Animal is a Protocol, any class that defines feed() becomes an Animal"

is slightly incorrect.

By default, protocols are only relevant for static type analysis, but you _can_ enable runtime instance checking using the @runtime_checkable decorator.

(So you can do "if isinstance(foo, Animal):...", but AFAIK this decorator comes with a performance penalty)

knome

Protocols allow you to explicitly declare a class implements them by making them a base of the class, but do not require it. Every class that implements the specification of the protocol is usable where the protocol is specified, not only those explicitly marked with it. Python didn't make c#'s doofy mistake of requiring the programmer to explicitly annotate everything. So, not every implementer of the protocol will have isinstance be true.

alfons_foobar

That is correct, but the @runtime_checkable decorator (which goes on the class that defines the protocol, e.g. "Animal" in the blog's example) makes it so that any class that implements the required methods (e.g. Rabbit) also passes the isinstance test (so isinstance(Rabbit, Animal) does actually return True, even though Rabbit is not defined as a subclass of Animal, i.e. NOT class Rabbit(Animal) !)

Whether it makes sense to use that is a different question, but it is possible :)

See https://docs.python.org/3/library/typing.html#typing.runtime...

kazinator

Maybe it's a new closed-closed principle. A class should be closed to modification and extension, F U.

neonsunset

[flagged]

kazinator

> described is slightly wrong?

Shhhh! You're spoiling how it's the most interesting language the author knows in terms of inheritance.

int_19h

Sather was a very interesting example of a language that is very much "OO is the way" design, but which cleanly separates composition and inheritance:

https://www.gnu.org/software/sather/docs-1.2/tutorial/inclus...

https://www.gnu.org/software/sather/docs-1.2/tutorial/abstra...

pfdietz

> Python is the most interesting language I know of regarding inheritance.

Someone doesn't know Common Lisp.

toolslive

you mean CLOS (Common Lisp Object System) ?

There are others that are also _very_ interesting (but also pre 90s). SELF fe, was object oriented, but did not have inheritance. It used prototypes. In essence, you would create something of the same "type" by cloning the prototype.

Anyway, subtyping and inheritance are similar, but not identical, concepts. Also, some languages provide traits iso protocols (and maybe even both ?). Some languages provide functors. The goal is always the same: abstract commonalities. Let's just keep it at: "it's complicated" ;)

I think it's better to study the design mistakes of programming languages in a historic context. For example: C++ offers multiple inheritance. This caused the diamond problem. Java tried to fix this via interfaces. This fixed the problem, but was also a mistake as interfaces cannot provide behaviours. So Multiple inheritance is not an issue if only 1 of the parties provides state; all others can provide signatures but also behaviours.

riffraff

> So Multiple inheritance is not an issue if only 1 of the parties provides state; all others can provide signatures but also behaviours.

IIRC that's what Bertrand Meyer advised in OOSC (I think he called it "marriage of convenience" between abstract and concrete superclasses). But he also claimed the diamond problem is overstated, and (paraphrasing) it's not a deep semantic issue, but a trivial syntactic one, "simply" solved with rename-on-inherit :)

kazinator

It's not easily solved with a rename on inherit, because the resulting object has to be substitutable in all the places where any one of the multiple base classes are expected. Those places expect the names to be what they are, they don't know about renamed names.

The diamond problem is actually about the situation when through at least two levels of inheritance, a class ends up inheriting the same base two or more times.

C++ has two choices for the diamond problem: virtual base inheritance results in one copy. Regular inheritance in multiple copies.

The clash problem is separate from the diamond problem. A clash occurs when you inherit from two bases that use the same names, but are usually separate bases. For instance a lottery game class inherits graphics and lottery; the former provides graphics::draw and the latter lottery::draw.

In C++ that is dealt with by leaving the name lookup be ambiguous. When the derived game object is used like this: game.draw(...), the name lookup is ambiguous. The program has to specify game.lottery::draw(...) or game.graphics::draw().

Places in the program that use the object through references to one of the bases do not face the ambiguity. Given a graphics &gobj, gobj.draw() is unambiguous, even if that object is really a game class instance that also has lottery::draw in it.

Scope resolution operator can resolve the ambiguity under the diamond problem, when inheritance is plain (not virtual). Say A inherits B and C. Both B and C inherit D. So now A has two D's. Say D has a member m. Given an A object aobj,I think we can separately reference aobj.B::D::m to get to the D::m that was inherited via B, and aobj.C::D::m to get to the copy inherited via C.

caspper69

Thank you.

I haven't really seen the diamond problem referenced lately as an objection to inheritance, but I honestly never understood what the big deal was. In the event of a collision, the compiler can ask for further disambiguation, which does in fact boil down to an issue of syntax, not one of a soundness or correctness violation.

pfdietz

CLOS is part of Common Lisp.

riffraff

Python is probably the most CLOS-like language in common use tho, isn't it? Multiple inheritance, metaclasses, the ability to decorate functions. No built-in multiple dispatch but I'm pretty sure I saw some implementations of that too over the years :)

kazinator

Someone also doesn't know Python?

Isn't it the case that if we have an obj and its class has a feed method, we can call obj.feed(...) regardless of what it inherits from?

agumonkey

Someones doesn't know Common Lisp.

that said most people don't know about things predating the 90s

grey-area

Go interfaces are explicit, not implicit, the difference is more where they are defined.

With subtypes the interfaces are defined with classes(an animal does this and that), with interfaces they are defined at the point of use (I accept a param that does this and that).

Interesting that gosling said at one point he felt including classes (i.e. inheritance) in Java was a mistake. I feel this is one thing Go got right compared to many other languages. Turns out inheritance just isn’t very helpful.

Jtsummers

> Go interfaces are explicit, not implicit, the difference is more where they are defined.

That's not what the blog author wrote so what are you responding to? What they actually wrote:

>> If a Go struct implements the same functions as an interface, it implicitly implements the interface.

And that's correct except they use the term "functions" instead of "methods". But most people won't get confused by that distinction.

https://go.dev/tour/methods/10 - "Interfaces are implemented implicitly"

grey-area

They are declared explicitly and implemented implicitly.

Thus the analogy the author is trying to make with magic methods in more dynamic languages like python or ruby is inaccurate and IMO misleading.

vishnuharidas

I used Java for a while and then switched to Kotlin as my daily tool, and one interesting thing that I found was that Kotlin prevents inheritance by default, and the developer has to explicitly mark a class as "open" to allow inheritance. Eventually I stopped using inheritance and now prefer to use composition over inheritance wherever possible.

AtlasBarfed

I haven't used kotlin. How does it help it?

So the kicker in imperative OO for composition is that the class you are composing has some state / instance vars.

How do you include methods that can view/update those instance vars?

Sure it's easy to compose with static/pure functions.

Do you enclose the instance vars in holder classes and pass those holders to the composition implementation class in it's method signature?

In the end, inheritance or composition is about constructing a class /struct/whatever with a given set of expected signatures with some ideally documented guidance on any intricacies.

sesm

As a Java developer the author should take a look at Clojure protocols. Those are even more flexible and can be implemented for 3rd party classes or even standard library classes without wrapping.

viccis

Python has far more options for this if you use some of the newer typing features. For example, it now (as of 3.12) automatically detects covariance and contravariance when you are dealing with generic types, which has made subclassing much nicer for me when it comes to what a static type checker can catch or autocomplete.

vips7L

Object orientism is not inheritance. It is simply encapsulation and having functions attached to the data.

mrkeen

From one point of view, yes.

Another point of view is that it's a new kind of de-encapsulation.

Apparently this is a 'global' variable:

    int variable = 0;
    void foo() {
      variable++;
    }
    void bar() {
      variable++;
    }
but this is an 'encapsulated' variable:

  class MyClass {

    int variable = 0;
    void foo() {
      variable++;
    }
    void bar() {
      variable++;
    }

  }
You can block the top one in a code-review because "global variable", and tell the developer to fix it by passing the variable in and out of functions when needed, but I have a feeling the bottom one's getting merged even if you object on the same grounds.

Tomte

The Common Lisp Object System disagrees with functions being attached to data.

kazinator

It's possible that what we can express with the Common Lisp Object System is a generalization that isn't OOP. It goes beyond OOP and includes it.

I.e. OOP is a style of programm organization (readily realizable under CLOS) where we associate operations with the leftmost argument and its type.

null

[deleted]

marginalia_nu

I don't think subtyping is something you should probably be using very often. I wouldn't say never, but you can go a very long time without finding a legitimate use for the feature, and your code will generally be better for it.

It's a bit weird, given how these elaborate class hierarchies were touted as such a big and important feature in Java originally, but when the dust settled it turned out more often than not to complicate the code.

cobbal

Personally I like to differentiate subclassing from conforming to an interface. Both are subtyping, but interfaces are much cleaner.

marginalia_nu

Interface inheritance is indeed much less of a foot-gun than class inheritance, but I'll argue even interfaces are generally fairly overused. The single-class interface pattern that exists in some parts of Java-land is just bizarre.

vips7L

I see that a lot more in C# than in Java tbh.

null

[deleted]