Why general inheritance is flawed and how to finally fix it
minborgsjavapot.blogspot.comI think that composition is absolutely better than inheritance except for one thing: boilerplate. The issue is that boilerplate is kind of important.
You don't want to litter your code with "f150.ford.car.vehicle.object.move(50, 50)". You can and should re-implement "move" so that you only have to call "f150.move(50, 50)", but that still requires boilerplate, just in the "F150" class.
Often you have class containing all of the functionality of another class, except a bit more functionality. You can always use composition but this happens so often you're creating a lot of boilerplate.
You could develop some other "syntax sugar" to replace inheritance. Maybe Haskell's type-classes are better (although they also kind of use inheritance, since there are subclasses). But chances are you'll go back to something like inheritance, because it's very useful very often.
COM solves this with delegation, where objects can only implement the methods that they care about and delegate everything else to the aggregated type, which provided the full interface.
However, depending on which stack one is using (VB 6, .NET, MFC, ATL, WRL, WinRT), the amount of boilerplate to deal with the runtime differs.
I wish more languages (in fact, any popular languages!) had convenient syntax for this.
Dynamic languages have it, via "doesNotUnderstand" and similar.
Kotlin also provides a way similar to those COM variants,
https://kotlinlang.org/docs/delegation.html#overriding-a-mem...
Best support is probably MOP in Common Lisp, I guess.
The Kotlin version is pretty much what I had in mind, that's pretty nice.
Any time one dares to defend Pascal's "with" statement, a shitstorm starts...
This doesn't inhherently have anything to do with inheritance. Delegation is the compositional solution to this problem and some languages do have built in sugar for that. It usually looks something like:
class F150(@delegate private val underlying: Car) { ... } class F150(private val underlying: Car) : Car by Underlying { ... } // etcIn kotlin you can delegate the implementation of an interface to another object. Basically syntatic sugar for the delegation pattern.
https://kotlinlang.org/docs/delegation.html
With it, you F150 can say it implements the "movable" interface, just buy stating which field it contains that implements it, and the you can run "f150.move"
I'd like languages to have some kind of "delegate" functionality, where you can just delegate names to point to nested names without screwing around with ownership - it would just act like a symlink. The scope of that action is limited and clear (and easy for your IDE to understand), and it's explicit that the subclass is still the "owner" of that property, which makes the whole thing a lot easier to navigate.
E.g. something like:
Then:class MyClass: def __init__(self, member_class): self.member_class = member_class # Delegate one member delegate move member_class.position.move # Delegate all members delegate * subclass.position.*
etc.a.move == a.member_class.position.moveC++ can do something something like this (at compile time) in its -> operator (ancient feature, long before C++98 was standardized).
will expand into enough -> dereferences until a foo is found. For instance suppose the object returned by obj's operator ->() function doesn't have a foo member, but itself overloads ->. Then that overload will be used, and so on.obj->foo()In Python you could do something like:
class Base: def func(self): print("In Base.func:", self.name) class Child: def __init__(self, name): self.name = name func = Base.func c = Child("Foo") c.func() #=> In Base.func: FooThe reason I'd like the construct is because it's explicit - intent (and the scope/limit of your intent) is encoded in what you create. It's clear you intend to do nothing with that name except symlink to the nested member, so the reader doesn't have to anticipate other behaviour (and can't accidentally do something else with it). Generic assignment doesn't convey the same restricted intent, and it doesn't carry those guard rails.
Really though it's a structure that only makes sense in strongly typed languages, so I probably shouldn't have used Python to illustrate the idea.
I'd like to point out that the article isn't disagreeing with you. It's saying inheritance is a dangerous interface for other users of your code (across packages is their terminology). So, if you write a library, maybe don't design it around extending classes. This is a much milder stance than the title implies, and seems pretty reasonable to me.
Edit: Totally with you on boilerplate though. +1.
That's not general [implementation] inheritance, it's just delegation. The problematic, non-compositional feature of implementation inheritance is something known as open recursion, viz. the fact that every call to an overridden method like .move(...) - importantly, even a call that's merely a private implementation detail of some code in the base class - goes through an implied dispatch step that introduces a dependency on the actual derived class instance that the base-class method is being called on. This creates the well-known "fragile base class" problem since method calls to these possibly-overridden methods are relying on fragile, unstated invariants that might be broken in the derived classes, or altered in future versions of the base class.
Boilerplate is a solved[0] problem since a _very_ long time and is orthogonal to the inheritance vs composition problem.
For the same reason, I'm not so absolutist about DRY. Having the most elegant codebase also often means the codebase that's hardest to work on, and it's often better to clean things up afterwards once you know how things will be structured.
This question determines if you need to be DRY or not:
"If [some fact] in the code base needs to change, how many places would we have to change it in?"
If the answer is > 1, you have a very good DRY case. Otherwise, when [some fact] changes, it will probably not be changed in one of the places, and the system will be broken.
This often coincides with having an "elegant codebase", but that's not the most important part.
A younger self used to be very strict about DRY.
For a living codebase, nowadays my general rule of thumb is to consciusly duplicate code until it covers 3 different cases, and only then refactor (unless the DRY way is as fast and obvious).
It takes more than that to yield spaghetti and a lot of time is saved on premature generalization. Plus the generalization is often way more straightforward once the explicit cases are already implemented.
https://en.wikipedia.org/wiki/Rule_of_three_(computer_progra...
I follow this too for "style" refactorings.
The original formulation of DRY was "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system," which in import winds up pretty close to what you have here.
Recently (last month basically), i rewrote my code (i'm leaving soon and i want my coworkers/successors to have success improving on what i've done).
I followed every principle of good code, except one, DRY. I tried to make generic parts to connectors, because they do have similarities. But this is a work of at least a year, and the price to make it generic was increasingly more complex configuration files (Just the pagination alone added 3 variables for two different APIs, and the number of app i am supposed to interact with should grow to ~40). I decided after a few days of reflexion that the idea was not that dumb in principle, but unworkable in my case, and decided that one connector for one API, even with a lot of repetition.
Yeah, I had a few connectors (API clients to different APIs with some business logic wrappers to handle low level stuff) and decided that they should share code with a generic connector interface, and then when one of them changed it was pretty painful untangling it. There is a tradeoff between getting stuff done and abstraction engineering, and there are costs to premature abstraction engineering.
See also: the wrong abstraction is worse than no abstraction at all.
Check out Scala 3's 'export' feature.
https://docs.scala-lang.org/scala3/reference/other-new-featu...
Go solves this with problem with embedding. If a type is imbedded inside of a struct and has its own methods, those methods are implicitly available on the new struct.
https://lwn.net/Articles/548560/
I really enjoyed the article above, which I read many years ago (before Rust 1.0!) which discusses how Golang and Rust handle polymorphism and code-reuse without classic object inheritance. My current thinking is that software objects are a general-purpose tool, but classic object inheritance should rarely be used as it is a solution to a narrow problem—classes should be "final" by default, and if not the inheritance pattern should be completely designed up front.
Java had the misfortune to be designed at a time when OOP was the new craze and the design decision to force all code into an object hierarchy has not held up well. I'd rather use languages designed either before or after Java, where you can use objects when they are appropriate and ignore them when they aren't.
I mostly agree, but there's one place where it does make a lot of sense to keep the hierarchy open: exceptions. Ability raise a specific error and catch it in a generic handler is very useful.
Interfaces would work fine here as well.
As someone who is greatly in favor of composition over inheritance, I don't agree - or at least my experiences don't point that way.
Both Rust and Go have had medium sized warts and/or boilerplate around errors, especially if you need control flow to depend on the error. In Python I've never felt that way.
Not sure I can put my finger on it, because any trivial example would be fine in either paradigm. I think it has to do with the forced/unnatural upfront decision "is this error a type or an interface" that may change later on, as a type might need to be refactored to an interface. There's probably one or two other reasons I can't think of right now.
Standard ML and OCaml have both sum types (like in Rust) and exceptions. I feel like both have their value, and both are important. A nice thing is that you can match on exceptions:
This makes it really easy to create alternative versions of functions, using exceptions or option types:match number_of_lines_of_a_file() with | x -> x [ exception File_not_found -> 0match number_of_lines_of_a_file_opt() with | Some x -> x | None -> raise File_not_found match number_of_lines_of_a_file_exc() with | x -> Some x | exception File_not_found -> Noneflow control is always impacted by errors. rust and go just don't hide that on you with a giant 'go to random location in the stack' capability.
Agreed. Exceptions tend to trivially respect the Liskov substitution principle and generally hold little state besides a debugging string. It's when you subclass an object as a method of code reuse that you start running into problems.
Arguably, subtyping in am OO language should either be signatures/interfaces only, or you should go full blown multiple inheritance for everything, as with the Fortress language.
Golang ide struggle with answering what implements this interface. The compiler obviously handles that fine
It makes it difficult to jump into an unfamiliar project
Assuming that’s what you mean by signature/ interfaces
C++ is much more complicated than go in that regards and my IDE generally does not have trouble with it
But this is an issue with tooling.
IntelliJ with Java/Kotlin does a great job here.
Unsolved AFAIK, Reducing the ergonomics of the language which is an important point
Is Fortress the same language that required exponential time constraint solving for its type system?
Type checking is not exponential last I checked. However, languages with global type inference have exponential behaviour when inferring types for some pathological programs.
I (happily) write a lot of OOP code, "inheritance is bad, use composition" is such a trite and unhelpful dogma that gets in the way of any actual discussion about where inheritance is useful.
IMO, the case where inheritance makes the most sense is when you have a set of objects polymorphically answering some question, usually with a simple answer.
class Subset
class Whole < Subset
def of(items)
items
end
end
class Range < Subset
def initialize(from:, to:)
@from = from
@to = to
end
def of(items)
items[@from:@to]
end
end
end
which is used as such: subset = Subset::Whole.new
puts subset.of(["a", "b", "c"]) # => ["a", "b", "c"]
subset = Subset::Range.new(from: 0, to: 1)
puts subset.of(["a", "b", "c"]) # => ["a"]
You can then pass around a Subset object anywhere (aka dependency injection) and push conditionals up the stack as far as possible.Simply saying "inheritance is bad" gets nobody anywhere.
In most languages Subset would be called a trait or interface, rather than general inheritance. You've picked an example with no fields or overriden methods, so it's impossible for it to demonstrate the shortcomings of inheritance.
What is the practical difference between inheritance, and a trait or interface with a default implementation? It seems like both risk the addAll() bug.
Not all languages allow for an interface to have a default implementation though. Delphi for example does not.
This leads the programmer towards composition and delegation.
To aid with this, Delphi even has some sugar for delegation[1].
[1]: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Usin...
Multiple inheritance is basically strictly more powerful than either traits or interfaces with default implementations.
I don't think traits or interfaces with default implementations prevent the bug, as I can imagine a Rust implementation that would do something similar.
OP title said "general inheritance is bad", not " Generally, inheritance is bad".
And the text support that. The "general inheritance" the author describe is not the one you've just used.
And i'm hijacking your post, sorry, but i really agree with the author with the "incidental inheritance" point. This is the worst. I lost a month to a bug caused by this kind of inheritance (Jenkins package that tried to be cute and interfered with a cloudbees class). I won't take a java gig ever again. Not worth the brain damage.
The thing I don’t like about passing objects around is that the state inside the object is opaque, and debugging it can be extremely frustrating, especially in something like Ruby some people are way too liberal with magic for my taste. My personal preference is to see immutable data structures being passed around through reasonably named functions, and that the is usually good enough for me.
The thing I like about passing objects around is that the state inside the object is opaque :). Thus when changes to the internal details of Person happen, the behavior of which is depended on by Inbox and Message, as long as I have properly depended on its public behavior, I don't need to change anywhere else. If I was just using plain data values as is common in e.g. Clojure, every change to something's internal representation would require changes to places which depend on it.
In a language where most errors will be runtime errors (Ruby), the inheritace problem described in the article is much less of a problem.
In such a language (e.g. Ruby), you will need test suites where languages with (strong) types use the type system to prove some level of correctness.
I used to be a fan of dyn typed langs (Ruby), but I've changed, I prefer strongly typed langs now for anything more than quick throw away scripts.
that approach gives me headaches to think about. Why not just have polymorphic functions?
with uniform function call syntax:fn subset(superset, start, end){ // superset is type inferred as long as it supports the [] operator // logic to collect superset[start] to superset[end] into an array and return it }
If you really want to reuse a subset range, you can use lambdas/closures, or in this case a simple wrapper[1,2,3,4,5,6].subset(1,4) == [2,3,4,5]// in some code fn subset1to4(superset){ return subset(superset,1,4) } array.subset1to4() anotherArray.subset1to4()Sure, that works for some specific problems where you're computing a value from a defined set of data types. "Subset of this data" was an example I've encountered in the past and used here because it has clearly distinct cases—give me the whole thing, give me some index-delimited range, possibly others—but there are plenty of other examples that don't fit a polymorphic function model (and let's forget that I've never even used a language with polymorphic functions).
As another example I've encountered in the past, let's say you have some object that can dynamically define fields. Once you define a field, you can retrieve its value or maybe some default value e.g.
Let's say doing anything with an undefined field is invalid. Here's my first pass at an implementation:model = Model.new model.define("points", default: 1) model.store("points", 10) points = model.retrieve("points") puts points # => 10
Works great! One day a requirement comes along that default values need to be lambdas, too, which are called every time the value is retrieved. How do we implement that? One way is to add a conditional to the Field class:class Model def initialize @fields = {} end def define(name, default: nil) @fields[name] = Field.new(name, default) end def retrieve(name) @fields[name].value end def store(name, value) @fields[name].value = value end end class Field attr_reader :name attr_accessor :value def initialize(name, value) @name = name @value = value end end
But now Field knows that it can be passed a lambda, so testing it needs to account for that case (among many other considerations, probably, in a real-world system). And any time we add more cases for default values, let alone changes to regular values like type casting or something, the Field class becomes more complicated. I'd probably reach for a new object instead:class Field attr_reader :name attr_writer :value def initialize(name, value) @name = name @value = value end def value if value.is_a?(Proc) @value.call else @value end end end
Now we've changed the conditional in the Field class to one that's actually relevant to it (do I have a value yet?) and won't change when the kinds of default values that it can accept change. Because we dependency-injected the Default object into the Field object, testing that conditional becomes a binary of retrieving the default value when no value is set, and retrieving the value once it's set. We can then test each kind of Default on its own, and changes to Default don't impact Field. If we really, really wanted to we could even eliminate the conditional in Field alltogether by unifying the interface for @default and @value such that they're both objects with a #value method (or maybe rename it to something else so we don't write @value.value). In either case we've made each piece simpler to reason about and pushed conditionals up the call stack so the resulting code is more straightforward.class Model def initialize @fields = {} end def define(name, default: nil) @fields[name] = Field.new(name, nil, Default.for(default)) end def retrieve(name) @fields[name].value end def store(name, value) @fields[name].value = value end end class Field def initialize(name, value, default) @name = name @value = value @default = default end def value if @value.nil? @default.value else @value end end end class Default def self.for(indicator) if indicator.is_a?(Proc) Default::Dynamic.new(indicator) elsif indicator.nil? Default::None.new else Default::Static.new(indicator) end end class Static < Default def initialize(value) @value = value end def value @value end end class Dynamic < Default def initialize(callable) @callable = callable end def value @callable.call end end class None < Default def value nil end end endI can probably recall more examples of simplifications like this, but this is where I find inheritance the most useful: a known set of things that each polymorphically conform to some interface. In these examples I don't actually use the superclass for any shared behavior, but you can imagine a case where I might.
One other benefit that I really like from the inheritance-object-modeling-as-pushing-up-conditionals perspective is that it makes you define what the different cases of something are as distinct objects, and give names to them. It's a similar benefit that falls out of using named sum types instead of signal values or tagged unions or something, but has the opposite effect (overall reduction of conditionals rather than proliferation).
The term "general inheritance" was not familiar to me as "inheritance across package structures". However, my OOP design intuition feels pretty good about that idea.
This quickly devolves into the inheritance vs. composition argument which isn't where I thought the Author wanted to go (but then sort of ended up going there). I agree with other commenters that it's an overstated idea. Inheritance is ridiculously useful in the right design structure, as is Composition. They both have a place. (Incidentally, bad Inheritance design usually looks very ugly very fast - bad Composition is often less glaring).
I find that years of designing in OOP has led me to build designs that have a goal of preventing me from making future mistakes and correctly consider implications of my code.
I find that my most immediate designs tend me towards Abstract Classes and Interfaces. While I usually get credit for "programming to the Interface" for this, that's not what usually led me there.
I like abstract methods. They (i.e. the compiler will) FORCE me to think about something if I ever decide to create another subclass of the Abstract class. The Author points out the "forget to call super" bug which is particularly nefarious and I avoid it at all costs. I can do that by providing a final concrete method which calls the abstract method. Let the subclasses implement that and never worry about super.
Anyway - governing inheritance across package hierarchies seems like a reasonable guideline. As for Inheritance vs. Composition, I don't favor either. When designing a class structure, I just make my best guess (as we'd all do) and find the structure quickly evolves on it's own. Usually, this ends up in a blend of shallow Inheritance trees with logical composition. There's always multiple Class Structures that will work - my goal is to find a reasonable one of those.
> Inheritance is ridiculously useful in the right design structure
I’ve made very little use of inheritance since I turned my back on C++/Java a decade and change ago. Can you give some examples where you feel inheritance wins out over composition?
Inheritance is flawed, in Java, mainly because it is the only organizing principle offered, so gets shoehorned into all kinds of problems where it is a poor fit.
Inheritance is just the right thing once in a while, but Java coders are obliged to apply it well beyond its useful range.
Just because it exists doesn’t mean it has to be used beyond its intended domain. One is entirely free to create flat “hierarchies” in Java. But I agree that in hindsight, final classes as a default would be better.
Fortunately nowadays, records and sealed classes remedy this for the most part in java.
What do you mean by "obliged to apply it"? For example, I very very rarely apply it and nobody is forcing me to create inheritance hierarchy.
The argument is "because it's possible to do misuse with inheritance, you should never use it".
By extension then, because it's possible to misuse Java/any programming language/computers/electricity/etc., you should never use it.
Or you could learn to use it properly.
Make no mistake, designing classes to support inheritance is much harder than just declaring everything final, and in many scenarios there is no good reason to do so
Isn't the "use it properly"-argument pretty much the same arguments as those saying that real C developers don't need the safeties offered by rust, they just need to use C or C++ properly?
The whole idea of language design (in my opinion) is to reduce the opportunities for mistakes, without getting in the way (thus reducing productivity). The biggest problem with Java and C# is that they are deceiptively simple. Anyone can get off the ground and the path of least resistance initially is the path of maximum pain in the end. That's the path of making large classes, lots of mutable state, long inheritance chains and so on. The languages aren't forcing anyone to use these antipatterns, but neither are they guiding the hand of the newcomer not to do that.
I _hate_ smart asses that do "everything final by default". It all fun and giggles until I can't mock some stupid class in some stupid library that I have no choice but to use just because someone is high on "inheritance is bad" hype. Instead of normal mocking/stubbing I now have to use stuff like PowerMock which does byte code hacking just so I can have a test.
How about you stop making decisions for me and let _me_ decide whether I want to inherit your class or not.
yea, at the very least, classes public members should be more like interfaces, that way mocking can be done easily in test mode, then in prod build lots of optimizations could "dissolve" the interface and be statically dispatched etc... hmmm....> I can't mock some stupid class in some stupid library that I have no choice but to use just because someone is high on "inheritance is bad" hype.
The final keyword is one of those places where Java shows its age to me. I agree with the overall point that inheritance is flawed, but I cannot bring myself to conclude that the use of final is the answer to the problem.
Simple example, String is final in Java. It is also immutable, and that is (mostly) irrelevant. Lots of string fields on inbound requests have validations, a simple one would be a field that contains a fixed length string. So obviously you validate that at the ingress before passing it down. Now, the question arises, should the core library be defensive and re-validate the string? Why not simply capture the subtype, TenCharacterString and parameterize methods with that?
Modern languages get this right. Subtyping is not inheritance. Inheritance is not subtyping. I should be able to subtype at zero cost, I don't need inheritance to do that, [and encapsulation is definitely not subtyping].
But Java doesn't have that. You mark something as final and you lose the ability to subtype just to eliminate the possibility of inheritance. On the other hand, to be fair to the argument against final, the real answer to my complaint is a proper type aliasing support.
The "extends" keyword gives it away that inheritance in Java was not positioned to support the restrictive cases you have in mind (Square : Rectangle, NegativeNumber : Number, TenCharacterString : String).
What issues do you see with wrapping? TenCharacterString eg. could use char[] as its backing store and implement CharSequence if you want to get it to speak a common language with String.
The author is onto something but I’m afraid it’s not explained very well. I think he’s mostly right though.
While reading it I was reminded of a design/implementation style I’ve run across several times over the years which is to find an existing class that does something similar to what you want. Then, subclass it and override methods until you get the behavior you want. And you’re done!
This leads directly to the Fragile Base Class problem. I think it also violates the Open/Closed principle. When subclassing occurs across components that are released independently (e.g., a library and an application), it either leads to continual breakage at each release, or ossification of the library. The latter happened to Java’s Swing. It got to the point where it was difficult to fix any bugs, because any “fix” would end up breaking some subclass that relied on the old behavior.
(See also Hyrum’s Law, which is more general than subclassing & inheritance.)
I have to agree with you. On the other hand, closed hierarchies can be an elegant solution to certain problems. Eg. sealed classes (and basically their single-class counterparts, final classes) avoid the mentioned problems in Java’s parlance.
One exception comes to mind though — Java’s SAMs, or in the general case, classes that more or less only wrap around a few methods intended to be overridden/implemented with clear requirements (but maybe this use case also should be restricted to interfaces?) But the default should be to add an explicit open instead of defaulting to non-final.
I feel the flaw is in building ontologies. Satic ones, at that.
There is great value in reducing type errors at runtime. Is hilariously ironic that one of the main tools we reach for seems intent on just moving them to design time.
Notably, not compile time. Design. Most failures from mistakes in ontology stall the problem out before release.
(Obviously, ymmv.)
The real world rarely sticks with nice hierarchies. Variations of set theory is more powerful, but would generally require merging RDBMS with IDE's, which does deserve more R&D. Using code to manage complex sets is limiting; query languages do it smoother because that's what they were intended for.
I've experimented myself with "table oriented programming", but don't have time to explore all the leads I uncover and rework the problem areas. Maybe when I retire?
For example, modern CRUD stacks are really just "event handling databases" done poorly. An RDBMS would be better at managing the gazillion event snippets, if it could "talk to" the compiler properly.
The "do everything in code" mantra of the web era is a mistake. Databases are better at managing complex relationships and masses of field/UI attributes, code better at non-collection-oriented algorithms. We should use the right tool for the job. "Data annotations" in Java and C# look like JCL's mutant stepdaughter. If that's the pinnacle of CRUD, then slap me silly.
> I've experimented myself with "table oriented programming"
Is that you, Bryce?
I got the same feeling reading this article that I got from reading _Design Patterns_: that the author(s) present some useful techniques to deal with the shortcomings of Java, but that most of their recommendations seem like kludgy fixes to problems that are absent or at least much less severe in better-designed languages.
I also note that, while the author does make some useful points about how to program more defensively, especially in the face of unexpected modifications to super/sub classes written by other programmers, one is inevitably beholden to at least a certain extent on the trustworthiness of code that one depends upon. (Even languages like LambdaMoo that start from the assumption that a program consists of code written by multiple mutually-untrusting programmers cannot entirely protect each against malicious subterfuge by the others.) I therefore question the value of the kind of 'hardening' the author recommends, especially when it might have unfortunate consequences on extensibility and testability.
On the "how to program more defensively" point, the author techniques are not meant to protect against malice (you are right that in such case of malice there is nothing that can be done), but instead to protect against foot guns, where innocent and reasonable changes in a module internals might unknowingly break another module.
I believe the author's arguments are quite valid, inheritance breaks the concept of a "black box" in Object Oriented Design. Once you inherit from a class, all that class internals become an "unadvertised signature", nothing is a black box that can be transparently changed anymore, any internal change may break a subclass.
Spaghetti code, spaghetti objects.
Same stuff in a new way.
I am always astonished/shocked/surprised how many articles are written trying to solve obscure problems/work arounds resulting from using OOP.
How about considering if OOP might be a stupid idea at the first place?
There are also problems with the combination of inheritance and concurrency. Google for "inheritance anomaly".
Thank you, interesting read, haven’t heard of it before.
Though I feel it would be dishonest to “blame it” on inheritance over on concurrency itself, when we don’t have any good solution to general concurrency as far as I know. We can only deal with it reasonably well by heavily restricting the domain-space to begin with (eg. immutables, no globals/sharing).
Re: using composition
In the past, it was feared that this would lead to reduced
performance but this is simply not the case.
Great to see the strong evidence here /sIt might be more productive if you were to outright disagree with the statement instead of simply noting a claim that isn't strongly substantiated. If you have experiences that demonstrate to you that this claim is weak I'm sure everyone else would be interested in them (I know I would be!).
To add onto that, I tried several different hand rolled alternatives to a c++ vtable and the builtin functionality outperformed them all which was surprising. It’s entirely possible I didn’t know what I was doing but I think my point is that vtables are a language feature that compilers know how to reason about and apply optimizations to. Other concepts not so much.
Something to keep in mind when microbenchmarking virtual calls is that compilers will happily devirtualize them whenever possible.
Yeah I’m aware but I made sure the same devirtualization would apply in production too so the compiler doing any devirtualization was good. It was a bit surprising that vtables outperformed std::variant (and I had tried implementing my own hand rolled equivalent of that too).
"what may be asserted without evidence, may be dismissed without evidence"
Where is your evidence for that claim? Seems like I should dismiss it out of hand.
QED.
I mean, I'm not sure where this 'fear' even came from?
At its core, inheritence is a special case of composition anyways (looked at from the other perspective it's syntactic sugar over either static or dynamic delegation), so it can't really be "faster".
At any rate, there's no abstraction so powerful it can prevent a programmer from making it slow.
This is true for languages where most objects are not garbage-collected. In Java (modern days) this would add a level of indirection.
Which may be in certain cases trivially optimizable. Eg. an object available only inside a class can have its method calls inlined into the containing class’s methods.
(But I’m no JVM developer (unfortunately) so I’m not sure whether this exact optimization exists or not)
Language Design: Let's start with the admission that there's a lot of cargo culting in language design. Evidence is hard to come by and the best evidence is other languages that succeed with different choices. I remember the flame wars about how multiple inheritance would cause a language to fall apart. When will languages adopt the idea that encapsulation is not sacrosant, as the theoretical issue about the backdooring like _method in python or package level public in Go^ is, someone might abuse it? Howabout make testing a first class concept and lump it in there or just use the shortcuts that have been working.
What is inheritance? Inheritance (for any given language) is a language supported type of class composition (https://youtu.be/eEBOvqMfPoI?t=2874), as a closure. A class is a function and once you understand this, it opens up possibilities in how you design and test. This has nothing to do with performance, which is a nonsequitor. Is Rust less performant than Java because of how it does composition? No. Perhaps there's something in the JVM that makes mixins difficult to optimize for, but that would require some evidence (there's no general branch prediction in the JVM, last I checked) and is, ultimately, at the feet of the JVM implementation. Have a look at Go and Rust.
Naturally, because a specific kind of inheritance is a language feature, it gets overused and a language, (like Java) for backward compatibility's sake, overuses it in designing new features. Looking at other languages like Javascript, Lua, Erlang, PHP, Ruby, Rust, etc. saner heads have prevailed and even Java has resorted to using "Aspects" ...which are runtime traits for additional types of composition^^.
Regarding the rest of the article... His arguments for using final include: 1. Someone may forget to use super() and that's bad because what I want to happen trumps what they want to happen. 2. People can't subclass my class across package boundaries, because I don't know why handwaved JPMS (then covered in Should Inheritance Across Package Boundaries Ever be Used?). His reasoning is not compelling, in the least. I can say, without hesitation, that 'final' is harmful. Adding final to a class is such a violation of the concept of reusable software, I'm surprised the FSF doesn't boycott languages. In C++ there are performance benefits. In Java it's just to put up a roadblock. This was never a good idea and makes testing impossible in some cases (where final classes are injected into final classes). This is purely because of Java's design as a language, not because of some demonstrably helpful concept, implemented poorly^^.
^If you are a language designer, always allow backdooring of accessors for testing, at the very least. This conviction that they must always apply to protect developers from each other is misplaced and has hurt the reliability of software, badly.
^^Spring has a form tacked-on composition, which is both ugly from a conceptual standpoint and problematic from a testing standpoint. Java always seems 20+ years behind.
> Language Design: Let's start with the admission that there's a lot of cargo culting in language design. Evidence is hard to come by and the best evidence is other languages that succeed with different choices.
You are going to get a lot of false positives using success as a metric for good language design.
I cannot think of a single popular language, other than Python, in the last 40 years that did not have huge companies with sizable marketing budgets behind them.
Programming languages do not succeed based on merit. They may fail based on lack of it.
> Programming languages do not succeed based on merit. They may fail based on lack of it.
Merit is what I'm talking about, in a way. The ability to use more robust forms of composition is a positive quality for a language. So much so, even if not explicitly stated, most languages have adopted new mechanisms over time beyond simple rigid inheritance trees.
> Merit is what I'm talking about, in a way. The ability to use more robust forms of composition is a positive quality for a language.
Maybe so, but what I am saying is that merit cannot make a language popular, but marketing can.
People keep telling this about Python, always ignoring who was paying Guido's salary.
> People keep telling this about Python, always ignoring who was paying Guido's salary.
Well, reveal the spoilers and don't keep us all in suspense :-)
Who was paying GvR a salary[1] when Python took off circa 2001/2002?
[1] I don't think paying a single person a salary is the same as the millions of dollars poured into marketing and development as done with all the other popular languages.
Zope was, which was one of the best ways to do CMS back in those days.
Spring does (and perhaps even prefers where possible) constructor-based inheritance though — which is as pluggable as it gets, making testing easy.
> Spring does (and perhaps even prefers where possible) constructor-based inheritance though
OFC Spring does lots of things (too many things), but that varies project to project based on what series of Spring-related libraries are being used.
More likely I'll see non Spring-core annotations like @RestController + @RequestMapping-attributes and have to figure a standard way to mock up some of what Spring does just to assert the outputs. Perhaps there will be a series of full functional tests which requires a setup/teardown. Maybe the project just ignores endpoint tests and focus on the less Spring annotated business logic claiming "it's simple enough".
Spring has resulted in slow, complex, and incomplete (coverage) testing almost anywhere it's used because it's literally hiding functionality behind a runtime composition that you can't access casually by calling a routine.
It's problematic because it's complex and the versions
Testing REST-endpoints should be the job of integration tests, which are by definition more involved. Also, spring has really great test suits for these use cases.
Other classes/components should in the general case be written as POJOs. The dependent components can be mocked/injected simply by using the constructor.
> Testing REST-endpoints should be the job of integration tests, which are by definition more involved.
I would confidently say, this philosophy is dead wrong. Integration test are useful, but you still want the unit tests to ensure that the code paths and side effects are maintained. With runtime composition, this is much harder. Java sacrificed what we do know for a grand experiment of doing as much as possible in pre-processing, breaking (sacrificing) the known concept of code reliability in the hopes that someone else would figure out a way to handle the testing implications down the line. Java Testing went from a gold standard to an afterthought with annotations. This is how important getting additional composition turned out to be, but at what cost?
> spring has really great test suits for these use cases.
@SpringBootTest requires booting up Spring just to add in the runtime composition. It's both unnecessarily time consuming and problematic to have to predict composition rather than observing it directly. Now you have to memorize what Spring might do, given annotations that can be anywhere AND the code you are trying to test. Nightmare stuff.
How would you test a REST endpoint, if I may ask? Because in the end it will somehow reply to a request. But that response has quite a few things going on — if you give back the url of a templates string as a constant, is that meaningful to unit test that? For anything more complex you should be writing a service which can and should be unit tested. But I believe that the set of headers, security! and the like is not in the realm of the quite complex job of endpoints. By that you would be testing the Spring library, which presumably happens on spring’s side.
> How would you test a REST endpoint, if I may ask?
Functional test (or integration test if you like). That wasn't the point.
> For anything more complex you should be writing a service which can and should be unit tested.
You're special casing a function based on how it's placed in the flow of the project. Unit (fn being a unit) tests are necessarily agnostic as to the overall functionality. If you want to special case how you are handling functions in a project, good luck. You'll continue to have failures that you'll handwave away as "not following the patterns" or "simple mistakes" rather than recognizing that you should have had useful unit tests to prevent it.
People often conflate the reasons that unit testing doesn't prevent bugs. The primary weakness of unit testing is that you cannot assert "no additional functionality" in the function implementation (code). When languages adopt testing as a first class concern and provide function hashes as a validation, we will see it adopted en-mass and these runtime compositions strategies will be left out, and rightly so.
Fantastic article!
Object oriented programming gets a horrible wrap on the basis of inheritance alone, and it's no wonder. Outside of limited domains, such as GUI programming, object inheritance makes little sense. Computer science students are right to question their introductory classes on inheritance when they teach contrived examples of dogs barking and cats meowing as an example of Mammal.makeSound() inheritance.
It's almost as if we're shoehorning in a code dispatch framework as a major language feature, except that framework sucks and we're stuck building with it. The best strategy working in languages with inheritance is to avoid it.
Duck typing or traits are better ways to represent polymorphic behaviors. We've known this for over a decade now.
Here's hoping that no new languages come with object inheritance as a concept. It's deader than NULL and shouldn't be resurrected.
> Duck typing or traits are better ways to represent polymorphic behaviors. We've known this for over a decade now.
Any objective measure of that? Because there is a catch, we can’t do what doctors can. There are no double-blind tests for language design. All we have is empirical studies and based on that, OOP languages do objectively much better. So if anything, your exceptional claim require exceptional evidence.
But otherwise I agree that these Animal hierarchies are just dumb and many definitely overuse inheritance.
Duck typing is terrible if you want understandable code. The world has largely moved away from this for good reason.
> Duck typing or traits are better ways to represent polymorphic behaviors. We've known this for over a decade now.
The trend is in the opposite direction.
TypeScript enables JavaScript programmers to benefit from static typing, and is seeing widespread use. Python now has type-hints.