Inheritance and Subtyping
blog.frankel.ch
blog.frankel.ch
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..."
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").
I guess it depends on whether you consider duck typing to be "passes the static type checker" or "it works the same".
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)
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...
it looks rather gross to me, and more dangerous than useful. why bother with a half check that can only see if the methods/attributes are present, but that can't guarantee they do what you need? better to not offer such a check at all, I should think.
but I'm not a fan of a great number of things python has been doing over the last some years.
if you were going to do such a thing (perhaps to allow type-safe callbacks from untyped code), I would think a wrapper would be the proper method, that checks the incoming parameters and the outgoing return value, raising an error if those are of the wrong sorts, and possibly further wrapping if something was declared to fit a protocol.
Shhhh! You're spoiling how it's the most interesting language the author knows in terms of inheritance.
https://www.gnu.org/software/sather/docs-1.2/tutorial/inclus...
https://www.gnu.org/software/sather/docs-1.2/tutorial/abstra...
Someone doesn't know Common Lisp.
that said most people don't know about things predating the 90s
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?
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.
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 :)
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.
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.
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.
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"
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.
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.
I.e. OOP is a style of programm organization (readily realizable under CLOS) where we associate operations with the leftmost argument and its type.
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.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.
You cannot eliminate complexity (although you can minimize it, to be clear). But for most things, you can shuffle the semantics around, you can paper over them in a new way, but at the end of the day, someone has to pay the piper.
In my experience, complexity eliminated from one spot will invariably bubble up somewhere else in an unexpected way.
It's still possible, mind you. TypeMock has been offering this exact ability for C# for many years now. But the free TDD frameworks generally didn't have this.
This is not necessarily zero-cost however - if the compiler cannot prove specific type members being invoked, it has to construct an execution profile and then apply it to subsequent compilations, and also emit a guard when doing dispatch on those.
[0]: https://github.com/dotnet/runtime/blob/main/docs/design/core... (note - subject to change)
The Go approach is a breath of fresh air when it comes to mocking and testing.
The goal of the tests is to answer the question "if the pipeline is green, does it give us confidence it will work exactly as intended in production?". Mocks work against this goal.
Especially nowadays when writing tests is dirt cheap because LLMs can often get them right at the first try, there is little reason to avoid approaching this problem without prioritizing sanity over following stupid cargo cult that should have died a decade ago.