An important point not mentioned by the article is that of "co-recursion" with inheritance (of implementation).
That is: an instance of a subclass calls a method defined on a parent class, which in turn may call a method that's been overridden by the subclass (or even another sub-subclass in the hierarchy) and that one in turn may call another parent method, and so on. It can easily become a pinball of calls around the hierarchy.
Add to that the fact that "objects" have state, and each class in the hierarchy may add more state, and modify state declared on parents. Perfect combinatory explosion of state and control-flow complexity.
I've seen this scenario way too many times in projects, and worse thing is: many developers think it's fine... and are even proud of navigating such a mess. Heck, many popular "frameworks" encourage this.
Basically: every time you modify a class, you must review the inner implementation of all other classes in the hierarchy, and call paths to ensure your change is safe. That's a horrendous way to write software, against the most basic principles of modularity and low coupling.
This is only the case when the language does not distinguish between methods that can be overridden versus those that cannot. C++ gives you the keyword "virtual" to put in front of each member function that you want to opt into this behavior, and in my experience people tend to give it some thought on which should be virtual. So I rarely have this issue in C++. But in languages like Python where everything is overridable, the issue you mention is very real.
Good point. In Java and many other languages you can opt out instead... which might make a big difference. Is it more of a "cultural" thing?... again, many frameworks encourage it by design, and so do many courses/tutorials... so those devs would be happy to put "virtual" everywhere in C++
Arguably the answer is “When Barbara Liskov invented CLU”. It literally didn’t support inheritance, just implementation of interface and here we have her explaining 15 odd years later why she was right the first time.
I used to do a talk about Liskov that included the joke “CLU didn’t support object inheritance. The reason for this is that Barbara Liskov was smarter than Bjarne Stroustrup.”
Diamond inheritance is its own special kind of hell, but “protected virtual” members of java and c# are the “evil at scale” that’s still with us today. An easy pattern that leads to combinatorial explosion beyond the atoms in the universe. Trivially.
People need to look at a playing deck. 52 cards, and you get 8×10^67 possible orders of the deck. Don’t replicate this in code.
Aside from game dev, Rust is being used in quite a lot of green field work where C++ would have otherwise been used.
Game dev world still has tons of C++, but also plenty of C#, I guess.
Agreed that it’s not really behind us though. Even if Rust gets used for 100% of C++’s typical domains going forward (and it’s a bit more complicated than that), there’s tens? hundreds? of millions (or maybe billions?) of lines of working C++ code out there in the wild that’ll need maintained for quite a long time - likely order decades.
struct A {
name: String,
owned: B
}
struct B {
name: String,
}
you can't have a writeable reference to both A and B at the same time.
This is alien to the way C/C++ programmers think. Yes, there are ways around it,
but you spend a lot of time in Rust getting the ownership plumbing right to make this work.
And python didn't get it right the first time either. It wasn't until python 2.3 when method resolution order was decided by C3 linearization that the inheritance in python became sane.
Inheritance being "sane" in Python is a red herring for which many smart people have fallen (e.g. https://www.youtube.com/watch?v=EiOglTERPEo). It's like saying that building a castle with sand is not a very good idea because first, it's going to be very difficult to extract pebbles (the technical difficulty) and also, it's generally been found to be a complicated and tedious material to work with and maintain. Then someone discovers a way to extract the pebbles. Now we have a whole bunch of castles sprouting that are really difficult to maintain.
I'm always surprised by how arrogant and unaware Python developers are. JavaScript/C++/etc developers are quite honest about the flaws in their language. Python developers will stare a horrible flaw in their language and say "I see nothing... BTW JS sucks so hard.".
Let me give you just one example of Python's stupid implementation of inheritance.
In Python you can initialize a class with a constructor that's not even in the inheritance chain (sorry, inheritance tree because Python developers think multiple inheritance is a good idea).
class A:
def __init__(self):
self.prop = 1
class B:
def __init__(self):
self.prop = 2
class C(A):
def __init__(self):
B.__init__(self)
c = C()
print(c.prop) # 2, no problem boss
And before you say "but no one does that", no, I've see that myself. Imagine you have a class that inherits from SteelMan but calls StealMan in it's constructor and Python's like "looks good to me".
I've seen horrors you people can't imagine.
* I've seen superclass constructors called multiple times.
* I've seen constructors called out of order.
* I've seen intentional skipping of constructors (with comments saying "we have to do this because blah blah blah)
* I've seen intentional skipping of your parent's constructor and instead calling your grandparent's constructor.
* And worst of all, calling constructors which aren't even in your inheritance chain.
And before you say "but that's just a dumb thing to do", that's the exact criticism of JS/C++. If you don't use any of the footguns of JS/C++, then they're flawless too.
Python developers would say "Hurr durr, did you know that if you add a object and an array in JS you get a boolean?", completely ignoring that that's a dumb thing to do, but Python developers will call superclass constructors that don't even belong to them and think nothing of it.
------------------------------
Oh, bonus point. I've see people creating a second constructor by calling `object.__new__(C)` instead of `C()` to avoid calling `C.__init__`. I didn't even know it was possible to construct an object while skipping its constructor, but dumb people know this and they use it.
Yes, instead of putting an if condition in the constructor Python developers in the wild, people who walk among us, who put their pants on one leg at a time like the rest of us, will call `object.__new__(C)` to construct a `C` object.
> In Python you can initialize a class with a constructor that's not even in the inheritance chain
No, you can't. Or, at least, if you can, that’s not what you’ve shown. You’ve shown calling the initializer of an unrelated class as a cross-applied method within the initializer. Initializers and constructors are different things.
> Oh, bonus point. I've see people creating a second constructor by calling `object.__new__(C)` instead of `C()` to avoid calling `C.__init__`.
Knowing that there are two constructors that exist for normal, non-native, Python classes, and that the basic constructoe Class.__new__, and that the constructor Class() itself calls Class.__new__() and then, if Class.__new__() returns an instance i of Class, also calls Class.__init__(i) before returning i, is pretty basic Python knowledge.
> I didn't even know it was possible to construct an object while skipping its constructor, but dumb people know this and they use it.
I wouldn’t use the term “dumb people” to distinguish those who—unlike you, apparently—understand the normal Python constructors and the difference between a constructor and an initializer.
For what it's worth, pyright highlights the problem in your first example:
t.py:11:20 - error: Argument of type "Self@C" cannot be assigned to parameter "self" of type "B" in function "__init__"
"C*" is not assignable to "B" (reportArgumentType)
1 error, 0 warnings, 0 information
ty and pyrefly give similar results. Unfortunately, mypy doesn't see a problem by default; you need to enable strict mode.
Oh I've seen one team constructing an object while skipping the constructor for a class owned by another team. The second team responded by rewriting the class in C. It turns out you cannot call `object.__new__` if the class is written in native code. At least Python doesn't allow you to mess around when memory safety is at stake.
1. Your first example is very much expected, so I don't know what's wrong here.
2. Your examples / post in general seems to be "people can break semantics and get to the internals just to do anything" which I agree is bad, but python works of the principle of "we're all consenting adults" and just because you can, doesn't mean you should.
I definitely don't consent to your code, and I wouldn't allow it to be merged in main.
If you or your team members have code like this, and it's regularly getting pushed into main, I think the issue is that you don't have safeguards for design or architecture
The difference with JavaScript "hurr durr add object and array" - is that it is not an architectural thing. That is a runtime / language semantics thing. One would be right to complain about that
I don't understand the problem with your first example. The __init__ method isn't special and B.__init__ is just a function. Your code boils down to:
def some_function(obj):
obj.prop = 2
class Foo:
def __init__(self):
some_function(self)
# or really just like
class Foo:
def __init__(self):
self.prop = 2
Which like, yeah of course that works. You can setattr on any object you please. Python's inheritance system ends up being sane in practice because it promises you nothing except method resolution and that's how it's used. Inheritance in Python is for code reuse.
Your examples genuinely haven't even scratched the surface of the weird stuff you can do when you take control of Python's machinery—self is just a convention, you can remove __init__ entirely, types are made up and the points don't matter. Foo() isn't even special it's just __call__ on the classes type and you can make that do anything.
With the assumptions typical of static class-based OO (but which may or may not apply in programs in Python), this naively seems like a type error, an even when it isn't it introduces a coupling where the class where the call is made likely depends on the internal implementation (not just the public interface) of the called class, which is...definitely an opportunity to introduce unexpected bugs easily.
Yes it is, as it is about the semantics of type hierarchies, not their syntax. If your software has type hierarchies, then it is a good idea for them conform to the principle, regardless of whether the implementation language syntax includes inheritance.
It might be argued that CLU is no better than typical OO languages in supporting the principle, but the principle is still valid - and it was particularly relevant at the time Liskov proposed it, as inheritance was frequently being abused as just a shortcut to do composition (fortunately, things are better now, right?)
CLU implemeted abstract data types. What we commonly call generics today.
The Liskov substitute principle in that context pretty much falls out naturally. As the entire point is to substitute in types into your generic data structure.
No, because the LSP is specifically about inheritance, or subtyping more generally. No inheritance/subtyping, no LSP.
It is true that an interface defines certain requirements of things that claim to implement it, but merely having an interface lacks the critical essence of the LSP. The LSP is not merely a banal statement that "a thing that claims to implement an interface ought to actually implement it". It is richer and more subtle than that, though perhaps from an academic perspective, still fairly basic. In the real world a lot of code technically violates it in one way or another, though.
I mean, it's not that hard to understand, why composition is to be preferred, when you could easily just use composition instead of inheritance. It's just that people, who don't want to think have been cargo-culting inheritance ever since they first heard about it, as they don't think much further than the first reuse of a method through inheritance.
I have some data types (structs or objects), that I want to serialize, persist, and that they have some common attributes of behaviors.
In swift I can have each object to conform to Hashable, Identifiable, Codabele, etc etc...
and keep repeating the same stuff over and over, or just create a base DataObject, and have the specific data object inherit it and just .
In swift you can do it by both protocols, (and extensions of them), but after a while they start looking exactly like object inheritance, and nothing like commposition.
Composition was preferred when many other languages didn't support object oriented out the gate (think Ada, Lua, etc), and tooling (IDEs) were primitive, but almost all modern languages do support it, and the tooling in insanely great.
Composition is great when you have behaviour that can be widely different, depending on runtime conditions. But, when you keep repeating yourself over and over by adopting the same protocols, perhaps you need some inheritance.
The one negative of inheretance is that when you change some behaviour of a parent class, you need to do more refactoring as there could be other classes that depend on it. But, again, with today's IDEs and tooling, that is a lot easier.
TLDR: Composition was preferred in a world where the languages didn't suport propper object inheretance out of the gate, and tooling and IDEs were still rudemmentary.
> In swift I can have each object to conform to Hashable, Identifiable, Codabele, etc etc... and keep repeating the same stuff over and over, or just create a base DataObject, and have the specific data object inherit it and just .
But then if you need a DataObject with an extra field, suddenly you need to re-implement serialization and deserialization. This only saves time across classes with exactly the same fields.
I'd argue that the proper tool for recursively implementing behaviours like `Eq`, `Hashable`, or `(De)Serialize` are decorator macros, e.g. Java annotations, Rust's `derive`, or Swift's attached macros.
Except that Smalltalk is so aggressively duck-typed that inheritance is not particularly first class except as an easy way to build derived classes using base classes as a template. When it comes to actually working with objects, the protocol they follow (roughly: the informally specified API they implement) is paramount, and compositional techniques have been a part of Smalltalk best practice since forever ago (something it took C++ and Java devs decades to understand). This allows you to abuse the snotdoodles out of the doesNotUnderstand: operator to delegate received messages to another object or other objects; and also the become: operator to substitute one object for another, even if they lie worlds apart on the class-hierarchy tree, usually without the caller knowing the switch has taken place. As long as they respond to the expected messages in the right way, it all adds up the same both ways.
"Composition" is a word that can mean several things, and without having read the original source I never really understood which version they mean. As a rule, I've always viewed "composition" as "gluing together things that don't know necessarily know about each other", and that definition works well enough, but that doesn't necessarily eliminate inheritance.
So then I start thinking in less-useful, more abstract definitions, like "inheritance is vertical, composition is horizontal", but of course that doesn't really mean anything.
And at some point, it seems like I just end up defining "composition" to mean "gluing together in a way that's not inheritance". Again, not really a useful definition.
I find the Monoid/Semigroup typeclass pretty concisely captures what is generally meant by "composition" in the minimal sense.
> As a rule, I've always viewed "composition" as "gluing together things that don't know necessarily know about each other"
The extension to this definition given the context of Monoids would be "combining two things of the same type such that they produce a new thing of the same type". The most trivial example of this is adding integers, but a more practical example is function composition where two functions can be combined to create a new function. You can also think of an abstraction that let's you combine two web components to create a new one, combining two AI agents to make a new one, etc.
> "inheritance is vertical, composition is horizontal", but of course that doesn't really mean anything.
This can actually be clearly defined, what you're hinting at is the distinction between sum types and product types. The latter of which describes inheritance. The problem with restricting yourself to only product types is that you can only add things to an existing thing, but in real life that rarely makes sense, and you will find yourself backed into a corner. Sum types let you have much more flexibility, which in turn make it easier to implement truly composable systems.
I actually knew most of that (I've done a lot of Haskell). I don't really disagree with what you said, but I feel like like you eliminate a lot of stuff that people would consider "composition" but aren't as easily classified in happy categories.
For example, a channel-based system like what Go or Clojure has; to me that is pretty clearly "composition", but I'm not 100% sure how you'd fully express something like that with categories; you could use something like a continuation monad but I think that loses a bit because the actual "channel" object has separate intrinsic value.
In Clojure, there's a "compose" function `comp` [1], which is regular `f(g(x))` composition, but lets suppose instead I had functions `f` and `g` running in separate threads and they synchronize on a channel (using core.async)? Is that still composition? There are two different things that can result in a very similar output, and both of which are considered by some to be composition. So which one of these should I "prefer" instead of inheritance?
Of course this is the realm of Pi Calculus or CSP if you want to go into theory, but I'm saying that I don't think that there's a "one definition to rule them all" for composition.
I think there's still a category theoretic expression of this, but it's not necessarily easy to capture in language type systems.
The notion of `f` producing a lazy sequence of values, `g` consuming them, and possibly that construct getting built up into some closed set of structures - (e.g. sequences, or trees, or if you like dags).
I've only read a smattering of Pi theory, but if I remember correctly it concerns itself more with the behaviour of `f` and `g`, and more generally bridging between local behavioural descriptions of components like `f` and `g` and the global behaviour of a heterogeneous system that is composed of some arbitrary graph of those sending messages to each other.
I'm getting a bit beyond my depth here, but it feels like Pi theory leans more towards operational semantics for reasoning about asynchronicity and something like category theory / monads / arrows and related concepts lean more towards reasoning about combinatorial algebras of computational models.
"Gluing together in a way that's not inheritance" is useful enough by itself. Most class hierarchies are wrong, and even when they're right people tend to implement th latest and greatest feature by mucking with the hierarchy in a way which generates wrongness, mostly because it's substantially easier, given a hierarchy, to implement the feature that way. Inheritance as a way of sharing code is dangerous.
The thing composition does differently is to prevent the effects of the software you're depending on from bleeding further downstream and to make it more explicit which features of the code you're using you actually care about.
Inheritance has a place, but IME that place is far from any code I'm going to be shackled to maintaining. It's a sometimes-necessary evil rather than a go-to pattern (or, in some people's books, that would make it a pattern like "go-to").
I don't think that it really is a useful enough definition. There are lots of ways to glue things together that aren't inheritance that are very different from each other.
I could compose functions together like the Haskell `.`, which does the regular f(g(x)), and I don't think anyone disputes that that is composition, but suppose I have an Erlang-style message passing system between two processes? This is still gluing stuff together in a way that is not inheritance, but it's very different than Haskell's `.`.
But both of those avoid the pitfalls of inheritance. "Othering" is a common phenomenon, and I think it's useful when creating an appropriate definition of composition.
But I don't think it's terribly useful; there are plenty of things that you could do that the people who coined the term would definitely not agree with.
Instead of inheritance, I could just copy and paste lots of different functions for different types. This would be different than inheritance but I don't think it would count as "composition", and it's certainly not something you should "prefer".
Each has its place. There's some things that inheritance makes possible, and some things that are best handled by composition. I use both, quite frequently.
It Depends™.
Composition can add a lot of complexity to a design, and give bugs a lot more corners to hide in, but inheritance can be such a clumsy tool, that it just shouldn't be used for some tasks.
That goes for almost everything in software. Becoming zealous about "The Only Correct Way" can be quite destructive.
I dunno. It's easy to say, "there are trade-offs, it depends" any time two things are compared, and it's never entirely untrue. However, sometimes one option is just generally worse than the other.
I'm not saying it's malpractice to use inheritance or anything, but it's a tool I definitely hesitate to reach for. Go and Rust removed inheritance entirely, and I'd say those languages are better-off without it.
Sure, but "favor x over y" or, put another way, "use y only if x is unsuitable" is compatible with this. Nothing in "prefer composition over inheritance" says that composition is the only correct way.
Objects and inheritance are good when you need big contracts. Functions are good when you want small contracts. Sometimes you want big contracts. Sometimes you want small contracts.
In mainstream/SV coding, I would say the scales just barely tipped toward composition in the late 10s... There are plenty of programmers still completely oblivious, the inertia is huge. Plus the swing back is too strong, inheritance is very powerful, just not as generic as originally thought.
I never liked inheritance. It seems like something that works well in a world where you assume things don’t evolve rapidly. It also feels like it adds mental debt—every new thing needs to comply with old things to stay compatible. Every update has to take into account how old components are working. Probably, the static nature helps big teams and big companies. But I’ve found that some duplicated code is way easier to deal with, especially now that LLMs can generate new code so quickly.
i think inheritance got a bad name due to abuse of multiple inheritance and overly fragile base classes in c++ (and maybe java) codebases of the 90s and early 00s.
it's mentally satisfying to create a beautiful class hierarchy that perfectly compresses the logic with no repetition, but i think long term readability, maintainability and extensibility are much better when inheritance is avoided in favor of flat interfaces. (also easier to turn into rpcs as all the overcomplicated object rpc things of the 90s were put to bed).
Rpcs really can't be understated in terms of the effect they had on classes and inheritance.
While in theory it should be straightforward to ship instance state over a wire, in practice most languages have no built-in support for it (or the support is extremely poor in the general case; I remember my first experiments with trying to ship raw Java objects over the wire using the standard library tools back in the early 2000s, and boy was that incredibly inefficient). Additionally, the ability to attach arbitrary methods to instances in some languages really complicates the story, and I think fundamentally people are coming around to the idea that the wire itself is something you have to be able to inspect and debug so being able to understand the structure in transit on the wire is extremely important.
Classes and their inheritance rules make exactly the wrong things implicit for this use case.
It really helps me to think of it all as extensive metaphors. Math included. The point is to tell an active story using symbols as metaphorical representations of something. With a lot of assumed language implied (through teachings) by choices of naming things. (As a fun example, don't focus on the name Algebraic if you aren't going to lean in on grade school algebra for things.)
That said, I think this is also a good way to approach framing things. Agreed that the idea of "prefer composition" is often a thought termination trick. Instead, try them both! The entire point of preferring one technique over the other is that it is felt to give more workable solutions. If you don't even know what the worked solution would look like with the other technique, you should consider trying it. Not with a precommitment that you will make it work; but to see what it illuminates on the ideas.
What I like about the modern¹ approach (interfaces + composition) is that it cleanly untangles polymorphism from behaviour-sharing.
When you inherit from a parent class, you have to be careful to only override methods in ways that the parent expects, so the parent's invariants aren't broken². There's a whole additional set of keywords (private/protected/final) meant to express these parent-child contracts. With interfaces + composition, those are unnecessary: You can compose an object and use it however you want; then, if you want the wrapper object to uphold the inner object's contract, you can additionally implement an interface to formalize that. The behaviours you use and the polymorphic guarantees you make are totally separate.
Inheritance mixes these ideas together and ends up worse for it. Not only the modern approach is simpler, it's more powerful: (1) Polymorphic extension (i.e. extending a polymorphic parent class at runtime) is doable, and (2) multiple inheritance is a non-issue.
[1]: I call it "modern" because newer languages like Go and Rust have eliminated inheritance in favour of exclusively using interfaces/traits.
Smalltalk protocols and message categories were a step toward this (for example you could classify messages as implementing a particular interface, such as the collection or stream protocols), but Smalltalk lacked the type and interface checking supported by Java and other languages.
When I first took an object oriented programming class it was all about inheritance so that's what I tried to use for everything. Then I started writing real programs and realized that inheritance sucked then finally found the succent "Favor composition over inheritance".
There are days I hate the mapping of plain English terms of art over actual in-language effects.
Considering sets, if something is, in set terms a specific subset with a defining membership or characteristic of a definable superset, representing that at compile time effects a hard constraint which honours the set Venn diagram.
If that set/subset constraint doesn't exist then you have to ask yourself if applying a compile time constraint is appropriate.
Hierarchy (and thus "inheritance") is a way to express that several different things share the same quality. They are different in general, but same in some way. It is a very natural way for people to express such a thing and no wonder it is so widespread. But it is not the only way nor the general way, of course.
Composition is not an opposite to inheritance. An opposite would be something like:
Message A ( ... ):
Type B: { ... }
Type C: { ... }
Or, if the body of the method is same ("a parent method"):
Type B, Type C:
Message A ( ... ): { ... }
Here we do not give A and B places in the hierarchy but merely say they respond to the same message or that even the procedure is same.
I do not know if any meaningful and systematic alternative to a hierarchical way exists in any programming notations. Interface spec is a partial way, but that's all. (I know only a few notations, of course).
the article seems to be digging into justifications for using inheritance. one thing I've heard and it seems to work is inheritance is ok for interfaces but usually not good for implementations.
I’ll be honest. I don’t really understand the point of this article. Maybe that’s just a preference thing. The philosophy behind these abstractions is the least interesting part of the question for me. What problems do these various methods of polymorphism solve and create? What solutions do they enable or prevent? That’s the only part that matters. But citing some discussion about the philosophy behind the theory from 40 years ago is not particularly enlightening. Not because it’s not relevant. But because we have 40 years more experience now and dozens of new languages that have different takes on this topic. What has been learned and what has been discovered?
I usually think of the ideas behind "composition" as "how do I assist a future developer to replace the current (exported) implementation of a type with a new one by restricting external visibility of its internal implementation through the use of private methods and data".
In "inheritance", it often feels like the programmer's mindset is static, along the lines of "here is a deep relationship that I discovered between 2 seemingly unrelated types", which ends up being frozen in time. For example, a later developer might want to make a subtle innovation to the base type; it can be quite frightening to see how this flows though the "derived" types without any explicit indiction.
Of course, YMMV, but I think of "composition" as "support change" and "inheritance" as "we found the 'correct way to think about this' and changes can be quite difficult".
Since I think that the key to building large systems handling complex requirements is 'how do we support disciplined change in the future' (empowering intellectual contributions by later generations of developers rather than just drudge maintenance).
> This contrasts inheritance as a “white box” form of reuse, because the inheriting class has full visibility over the implementation details of the inherited class; with composition as a “black box” form of reuse, because the composing object only has access to the interface of the constituent object.
So, we just need devs to stop trying to be overly clever? I can get behind that, “clever” devs are just awful to work with.
How about not favoring anything. There are many paradigms and each one has its place. Franky I do not really understand why do developers fight these religious wars about languages, frameworks etc.
> There are many paradigms and each one has its place.
That's a thought-terminating cliché. The argument against inheritance has been laid out pretty clearly. It's reasonable to rebut that argument. It's not reasonable to say, "you shouldn't criticize inheritance because Everything Has Its Place." Everything does not have its place. Sometimes we discover that something is harmful and we just stop using it.
Gameplay logic inherently leans more towards composition, with a little hint of inheritance.
You can have players and monsters, which are all types of "characters" or "units", which is inheritance, but instead of having a separate FlyingPlayer and a separate FlyingMonster, which use the same code for flight, you could have a FlyingComponent, which is composition.
I've been going all in on composition and it's amazing for quickly implementing new gameplay ideas. For example, instead of a monolithic `Player` class you could have a `PlayerControlComponent` then you can move that between different characters to let the player control monsters, drones, etc.
Imagine instead of only Pac-Man being able to eat the pills, you could also give the ghosts the `PillEaterComponent` in some crazy special game modes :)
I've also been fantasizing about a hypothetical language that is built from the ground up for coding gameplay, that doesn't use the word "class" at all but something else that could be a hybrid of inheritance+composition.
"Single responsibility" isn't an especially useful yardstick. If you actually need to decompose a complex piece of logic into modules, the place to start is by identifying areas of high cohesion and separating them into loosely coupled functions. Ideally you can match those up to a DDD-style ubiquitous language, so your code will make intuitive sense to people familiar with the domain. "Does this have one responsibility?" really isn't the right question to ask.
The open-closed principle is straight-up wrong. Code should be easy to modify and easy to delete, and you only rarely need to add hooks for extensibility. Liskov substitution is fine, but it has more to do with correctness than cleanliness. Dependency inversion is a source of premature abstraction—you shouldn't open the door to polymorphism until you need to. Interface segregation is good, though.
In general, I think SOLID is overly enamoured with the features of object orientation. Objects themselves just aren't that big of a deal. It'd be like making the whole acronym about if-statements. If I were going to make a pithy acronym about legible code, it'd have more to say about statelessness, coupling, and unit tests. It'd reference Ousterhout's idea of deep modules, and maybe say something about "Parse, don't validate," or at least something against null values.
An important point not mentioned by the article is that of "co-recursion" with inheritance (of implementation).
That is: an instance of a subclass calls a method defined on a parent class, which in turn may call a method that's been overridden by the subclass (or even another sub-subclass in the hierarchy) and that one in turn may call another parent method, and so on. It can easily become a pinball of calls around the hierarchy.
Add to that the fact that "objects" have state, and each class in the hierarchy may add more state, and modify state declared on parents. Perfect combinatory explosion of state and control-flow complexity.
I've seen this scenario way too many times in projects, and worse thing is: many developers think it's fine... and are even proud of navigating such a mess. Heck, many popular "frameworks" encourage this.
Basically: every time you modify a class, you must review the inner implementation of all other classes in the hierarchy, and call paths to ensure your change is safe. That's a horrendous way to write software, against the most basic principles of modularity and low coupling.
This is only the case when the language does not distinguish between methods that can be overridden versus those that cannot. C++ gives you the keyword "virtual" to put in front of each member function that you want to opt into this behavior, and in my experience people tend to give it some thought on which should be virtual. So I rarely have this issue in C++. But in languages like Python where everything is overridable, the issue you mention is very real.
Good point. In Java and many other languages you can opt out instead... which might make a big difference. Is it more of a "cultural" thing?... again, many frameworks encourage it by design, and so do many courses/tutorials... so those devs would be happy to put "virtual" everywhere in C++
Sounds like someone didn’t follow the SOLID principles
Arguably the answer is “When Barbara Liskov invented CLU”. It literally didn’t support inheritance, just implementation of interface and here we have her explaining 15 odd years later why she was right the first time.
I used to do a talk about Liskov that included the joke “CLU didn’t support object inheritance. The reason for this is that Barbara Liskov was smarter than Bjarne Stroustrup.”
There is a reason C++ devs and only C++ devs have nightmares of diamond inheritance.
Oh the damage that language has done to a generation, but at least it is largely passed us now.
Diamond inheritance is its own special kind of hell, but “protected virtual” members of java and c# are the “evil at scale” that’s still with us today. An easy pattern that leads to combinatorial explosion beyond the atoms in the universe. Trivially.
People need to look at a playing deck. 52 cards, and you get 8×10^67 possible orders of the deck. Don’t replicate this in code.
Every language that permits diamond inheritance causes the devs who dare to use this feature at least some nightmare. It's not a C++ issue.
> at least it is largely passed us now
What does this mean? There doesn't seem to be a popular alternative to C++ yet, unfortunately.
Aside from game dev, Rust is being used in quite a lot of green field work where C++ would have otherwise been used.
Game dev world still has tons of C++, but also plenty of C#, I guess.
Agreed that it’s not really behind us though. Even if Rust gets used for 100% of C++’s typical domains going forward (and it’s a bit more complicated than that), there’s tens? hundreds? of millions (or maybe billions?) of lines of working C++ code out there in the wild that’ll need maintained for quite a long time - likely order decades.
The problem in Rust is that if B is inside of A,
you can't have a writeable reference to both A and B at the same time. This is alien to the way C/C++ programmers think. Yes, there are ways around it, but you spend a lot of time in Rust getting the ownership plumbing right to make this work.I'm spoiled by Python's incredibly sane inheritance and I always have to keep in mind that inheritance is a very different beast in other languages.
And python didn't get it right the first time either. It wasn't until python 2.3 when method resolution order was decided by C3 linearization that the inheritance in python became sane.
http://mail.python.org/pipermail/python-dev/2002-October/029...
Inheritance being "sane" in Python is a red herring for which many smart people have fallen (e.g. https://www.youtube.com/watch?v=EiOglTERPEo). It's like saying that building a castle with sand is not a very good idea because first, it's going to be very difficult to extract pebbles (the technical difficulty) and also, it's generally been found to be a complicated and tedious material to work with and maintain. Then someone discovers a way to extract the pebbles. Now we have a whole bunch of castles sprouting that are really difficult to maintain.
I'm always surprised by how arrogant and unaware Python developers are. JavaScript/C++/etc developers are quite honest about the flaws in their language. Python developers will stare a horrible flaw in their language and say "I see nothing... BTW JS sucks so hard.".
Let me give you just one example of Python's stupid implementation of inheritance.
In Python you can initialize a class with a constructor that's not even in the inheritance chain (sorry, inheritance tree because Python developers think multiple inheritance is a good idea).
And before you say "but no one does that", no, I've see that myself. Imagine you have a class that inherits from SteelMan but calls StealMan in it's constructor and Python's like "looks good to me".I've seen horrors you people can't imagine.
* I've seen superclass constructors called multiple times.
* I've seen constructors called out of order.
* I've seen intentional skipping of constructors (with comments saying "we have to do this because blah blah blah)
* I've seen intentional skipping of your parent's constructor and instead calling your grandparent's constructor.
* And worst of all, calling constructors which aren't even in your inheritance chain.
And before you say "but that's just a dumb thing to do", that's the exact criticism of JS/C++. If you don't use any of the footguns of JS/C++, then they're flawless too.
Python developers would say "Hurr durr, did you know that if you add a object and an array in JS you get a boolean?", completely ignoring that that's a dumb thing to do, but Python developers will call superclass constructors that don't even belong to them and think nothing of it.
------------------------------
Oh, bonus point. I've see people creating a second constructor by calling `object.__new__(C)` instead of `C()` to avoid calling `C.__init__`. I didn't even know it was possible to construct an object while skipping its constructor, but dumb people know this and they use it.
Yes, instead of putting an if condition in the constructor Python developers in the wild, people who walk among us, who put their pants on one leg at a time like the rest of us, will call `object.__new__(C)` to construct a `C` object.
And Python developers will look at this and say "Wow, Python is so flawless".> In Python you can initialize a class with a constructor that's not even in the inheritance chain
No, you can't. Or, at least, if you can, that’s not what you’ve shown. You’ve shown calling the initializer of an unrelated class as a cross-applied method within the initializer. Initializers and constructors are different things.
> Oh, bonus point. I've see people creating a second constructor by calling `object.__new__(C)` instead of `C()` to avoid calling `C.__init__`.
Knowing that there are two constructors that exist for normal, non-native, Python classes, and that the basic constructoe Class.__new__, and that the constructor Class() itself calls Class.__new__() and then, if Class.__new__() returns an instance i of Class, also calls Class.__init__(i) before returning i, is pretty basic Python knowledge.
> I didn't even know it was possible to construct an object while skipping its constructor, but dumb people know this and they use it.
I wouldn’t use the term “dumb people” to distinguish those who—unlike you, apparently—understand the normal Python constructors and the difference between a constructor and an initializer.
For what it's worth, pyright highlights the problem in your first example:
ty and pyrefly give similar results. Unfortunately, mypy doesn't see a problem by default; you need to enable strict mode.Oh I've seen one team constructing an object while skipping the constructor for a class owned by another team. The second team responded by rewriting the class in C. It turns out you cannot call `object.__new__` if the class is written in native code. At least Python doesn't allow you to mess around when memory safety is at stake.
1. Your first example is very much expected, so I don't know what's wrong here.
2. Your examples / post in general seems to be "people can break semantics and get to the internals just to do anything" which I agree is bad, but python works of the principle of "we're all consenting adults" and just because you can, doesn't mean you should.
I definitely don't consent to your code, and I wouldn't allow it to be merged in main.
If you or your team members have code like this, and it's regularly getting pushed into main, I think the issue is that you don't have safeguards for design or architecture
The difference with JavaScript "hurr durr add object and array" - is that it is not an architectural thing. That is a runtime / language semantics thing. One would be right to complain about that
I don't understand the problem with your first example. The __init__ method isn't special and B.__init__ is just a function. Your code boils down to:
Which like, yeah of course that works. You can setattr on any object you please. Python's inheritance system ends up being sane in practice because it promises you nothing except method resolution and that's how it's used. Inheritance in Python is for code reuse.Your examples genuinely haven't even scratched the surface of the weird stuff you can do when you take control of Python's machinery—self is just a convention, you can remove __init__ entirely, types are made up and the points don't matter. Foo() isn't even special it's just __call__ on the classes type and you can make that do anything.
With the assumptions typical of static class-based OO (but which may or may not apply in programs in Python), this naively seems like a type error, an even when it isn't it introduces a coupling where the class where the call is made likely depends on the internal implementation (not just the public interface) of the called class, which is...definitely an opportunity to introduce unexpected bugs easily.
If CLU only supported composition, was the Liskov substitution principle still applicable to CLU?
Yes it is, as it is about the semantics of type hierarchies, not their syntax. If your software has type hierarchies, then it is a good idea for them conform to the principle, regardless of whether the implementation language syntax includes inheritance.
It might be argued that CLU is no better than typical OO languages in supporting the principle, but the principle is still valid - and it was particularly relevant at the time Liskov proposed it, as inheritance was frequently being abused as just a shortcut to do composition (fortunately, things are better now, right?)
CLU implemeted abstract data types. What we commonly call generics today.
The Liskov substitute principle in that context pretty much falls out naturally. As the entire point is to substitute in types into your generic data structure.
No, because the LSP is specifically about inheritance, or subtyping more generally. No inheritance/subtyping, no LSP.
It is true that an interface defines certain requirements of things that claim to implement it, but merely having an interface lacks the critical essence of the LSP. The LSP is not merely a banal statement that "a thing that claims to implement an interface ought to actually implement it". It is richer and more subtle than that, though perhaps from an academic perspective, still fairly basic. In the real world a lot of code technically violates it in one way or another, though.
I mean, it's not that hard to understand, why composition is to be preferred, when you could easily just use composition instead of inheritance. It's just that people, who don't want to think have been cargo-culting inheritance ever since they first heard about it, as they don't think much further than the first reuse of a method through inheritance.
Composition folks can get very dogmatic.
I have some data types (structs or objects), that I want to serialize, persist, and that they have some common attributes of behaviors.
In swift I can have each object to conform to Hashable, Identifiable, Codabele, etc etc... and keep repeating the same stuff over and over, or just create a base DataObject, and have the specific data object inherit it and just .
In swift you can do it by both protocols, (and extensions of them), but after a while they start looking exactly like object inheritance, and nothing like commposition.
Composition was preferred when many other languages didn't support object oriented out the gate (think Ada, Lua, etc), and tooling (IDEs) were primitive, but almost all modern languages do support it, and the tooling in insanely great.
Composition is great when you have behaviour that can be widely different, depending on runtime conditions. But, when you keep repeating yourself over and over by adopting the same protocols, perhaps you need some inheritance.
The one negative of inheretance is that when you change some behaviour of a parent class, you need to do more refactoring as there could be other classes that depend on it. But, again, with today's IDEs and tooling, that is a lot easier.
TLDR: Composition was preferred in a world where the languages didn't suport propper object inheretance out of the gate, and tooling and IDEs were still rudemmentary.
> In swift I can have each object to conform to Hashable, Identifiable, Codabele, etc etc... and keep repeating the same stuff over and over, or just create a base DataObject, and have the specific data object inherit it and just .
But then if you need a DataObject with an extra field, suddenly you need to re-implement serialization and deserialization. This only saves time across classes with exactly the same fields.
I'd argue that the proper tool for recursively implementing behaviours like `Eq`, `Hashable`, or `(De)Serialize` are decorator macros, e.g. Java annotations, Rust's `derive`, or Swift's attached macros.
I mean, duh. The spicier take is that Barbara Liskov is smarter than Alan Kay.
Except that Smalltalk is so aggressively duck-typed that inheritance is not particularly first class except as an easy way to build derived classes using base classes as a template. When it comes to actually working with objects, the protocol they follow (roughly: the informally specified API they implement) is paramount, and compositional techniques have been a part of Smalltalk best practice since forever ago (something it took C++ and Java devs decades to understand). This allows you to abuse the snotdoodles out of the doesNotUnderstand: operator to delegate received messages to another object or other objects; and also the become: operator to substitute one object for another, even if they lie worlds apart on the class-hierarchy tree, usually without the caller knowing the switch has taken place. As long as they respond to the expected messages in the right way, it all adds up the same both ways.
"Composition" is a word that can mean several things, and without having read the original source I never really understood which version they mean. As a rule, I've always viewed "composition" as "gluing together things that don't know necessarily know about each other", and that definition works well enough, but that doesn't necessarily eliminate inheritance.
So then I start thinking in less-useful, more abstract definitions, like "inheritance is vertical, composition is horizontal", but of course that doesn't really mean anything.
And at some point, it seems like I just end up defining "composition" to mean "gluing together in a way that's not inheritance". Again, not really a useful definition.
I find the Monoid/Semigroup typeclass pretty concisely captures what is generally meant by "composition" in the minimal sense.
> As a rule, I've always viewed "composition" as "gluing together things that don't know necessarily know about each other"
The extension to this definition given the context of Monoids would be "combining two things of the same type such that they produce a new thing of the same type". The most trivial example of this is adding integers, but a more practical example is function composition where two functions can be combined to create a new function. You can also think of an abstraction that let's you combine two web components to create a new one, combining two AI agents to make a new one, etc.
> "inheritance is vertical, composition is horizontal", but of course that doesn't really mean anything.
This can actually be clearly defined, what you're hinting at is the distinction between sum types and product types. The latter of which describes inheritance. The problem with restricting yourself to only product types is that you can only add things to an existing thing, but in real life that rarely makes sense, and you will find yourself backed into a corner. Sum types let you have much more flexibility, which in turn make it easier to implement truly composable systems.
I actually knew most of that (I've done a lot of Haskell). I don't really disagree with what you said, but I feel like like you eliminate a lot of stuff that people would consider "composition" but aren't as easily classified in happy categories.
For example, a channel-based system like what Go or Clojure has; to me that is pretty clearly "composition", but I'm not 100% sure how you'd fully express something like that with categories; you could use something like a continuation monad but I think that loses a bit because the actual "channel" object has separate intrinsic value.
In Clojure, there's a "compose" function `comp` [1], which is regular `f(g(x))` composition, but lets suppose instead I had functions `f` and `g` running in separate threads and they synchronize on a channel (using core.async)? Is that still composition? There are two different things that can result in a very similar output, and both of which are considered by some to be composition. So which one of these should I "prefer" instead of inheritance?
Of course this is the realm of Pi Calculus or CSP if you want to go into theory, but I'm saying that I don't think that there's a "one definition to rule them all" for composition.
[1] https://clojuredocs.org/clojure.core/comp
I think there's still a category theoretic expression of this, but it's not necessarily easy to capture in language type systems.
The notion of `f` producing a lazy sequence of values, `g` consuming them, and possibly that construct getting built up into some closed set of structures - (e.g. sequences, or trees, or if you like dags).
I've only read a smattering of Pi theory, but if I remember correctly it concerns itself more with the behaviour of `f` and `g`, and more generally bridging between local behavioural descriptions of components like `f` and `g` and the global behaviour of a heterogeneous system that is composed of some arbitrary graph of those sending messages to each other.
I'm getting a bit beyond my depth here, but it feels like Pi theory leans more towards operational semantics for reasoning about asynchronicity and something like category theory / monads / arrows and related concepts lean more towards reasoning about combinatorial algebras of computational models.
"Gluing together in a way that's not inheritance" is useful enough by itself. Most class hierarchies are wrong, and even when they're right people tend to implement th latest and greatest feature by mucking with the hierarchy in a way which generates wrongness, mostly because it's substantially easier, given a hierarchy, to implement the feature that way. Inheritance as a way of sharing code is dangerous.
The thing composition does differently is to prevent the effects of the software you're depending on from bleeding further downstream and to make it more explicit which features of the code you're using you actually care about.
Inheritance has a place, but IME that place is far from any code I'm going to be shackled to maintaining. It's a sometimes-necessary evil rather than a go-to pattern (or, in some people's books, that would make it a pattern like "go-to").
I don't think that it really is a useful enough definition. There are lots of ways to glue things together that aren't inheritance that are very different from each other.
I could compose functions together like the Haskell `.`, which does the regular f(g(x)), and I don't think anyone disputes that that is composition, but suppose I have an Erlang-style message passing system between two processes? This is still gluing stuff together in a way that is not inheritance, but it's very different than Haskell's `.`.
But both of those avoid the pitfalls of inheritance. "Othering" is a common phenomenon, and I think it's useful when creating an appropriate definition of composition.
But I don't think it's terribly useful; there are plenty of things that you could do that the people who coined the term would definitely not agree with.
Instead of inheritance, I could just copy and paste lots of different functions for different types. This would be different than inheritance but I don't think it would count as "composition", and it's certainly not something you should "prefer".
I have always heard "prefer composition to inheritance" also referred to as "has a" instead of "is a." Meaning:
Yep. "Composition" has many meanings, but in the context of "inheritance vs. composition" it's just referring to "x has a y".
Each has its place. There's some things that inheritance makes possible, and some things that are best handled by composition. I use both, quite frequently.
It Depends™.
Composition can add a lot of complexity to a design, and give bugs a lot more corners to hide in, but inheritance can be such a clumsy tool, that it just shouldn't be used for some tasks.
That goes for almost everything in software. Becoming zealous about "The Only Correct Way" can be quite destructive.
I dunno. It's easy to say, "there are trade-offs, it depends" any time two things are compared, and it's never entirely untrue. However, sometimes one option is just generally worse than the other.
I'm not saying it's malpractice to use inheritance or anything, but it's a tool I definitely hesitate to reach for. Go and Rust removed inheritance entirely, and I'd say those languages are better-off without it.
Sure, but "favor x over y" or, put another way, "use y only if x is unsuitable" is compatible with this. Nothing in "prefer composition over inheritance" says that composition is the only correct way.
Same way I see it.
Objects and inheritance are good when you need big contracts. Functions are good when you want small contracts. Sometimes you want big contracts. Sometimes you want small contracts.
Sometimes the right answer is to mix and match.
In mainstream/SV coding, I would say the scales just barely tipped toward composition in the late 10s... There are plenty of programmers still completely oblivious, the inertia is huge. Plus the swing back is too strong, inheritance is very powerful, just not as generic as originally thought.
I never liked inheritance. It seems like something that works well in a world where you assume things don’t evolve rapidly. It also feels like it adds mental debt—every new thing needs to comply with old things to stay compatible. Every update has to take into account how old components are working. Probably, the static nature helps big teams and big companies. But I’ve found that some duplicated code is way easier to deal with, especially now that LLMs can generate new code so quickly.
i think inheritance got a bad name due to abuse of multiple inheritance and overly fragile base classes in c++ (and maybe java) codebases of the 90s and early 00s.
it's mentally satisfying to create a beautiful class hierarchy that perfectly compresses the logic with no repetition, but i think long term readability, maintainability and extensibility are much better when inheritance is avoided in favor of flat interfaces. (also easier to turn into rpcs as all the overcomplicated object rpc things of the 90s were put to bed).
Rpcs really can't be understated in terms of the effect they had on classes and inheritance.
While in theory it should be straightforward to ship instance state over a wire, in practice most languages have no built-in support for it (or the support is extremely poor in the general case; I remember my first experiments with trying to ship raw Java objects over the wire using the standard library tools back in the early 2000s, and boy was that incredibly inefficient). Additionally, the ability to attach arbitrary methods to instances in some languages really complicates the story, and I think fundamentally people are coming around to the idea that the wire itself is something you have to be able to inspect and debug so being able to understand the structure in transit on the wire is extremely important.
Classes and their inheritance rules make exactly the wrong things implicit for this use case.
It really helps me to think of it all as extensive metaphors. Math included. The point is to tell an active story using symbols as metaphorical representations of something. With a lot of assumed language implied (through teachings) by choices of naming things. (As a fun example, don't focus on the name Algebraic if you aren't going to lean in on grade school algebra for things.)
That said, I think this is also a good way to approach framing things. Agreed that the idea of "prefer composition" is often a thought termination trick. Instead, try them both! The entire point of preferring one technique over the other is that it is felt to give more workable solutions. If you don't even know what the worked solution would look like with the other technique, you should consider trying it. Not with a precommitment that you will make it work; but to see what it illuminates on the ideas.
What I like about the modern¹ approach (interfaces + composition) is that it cleanly untangles polymorphism from behaviour-sharing.
When you inherit from a parent class, you have to be careful to only override methods in ways that the parent expects, so the parent's invariants aren't broken². There's a whole additional set of keywords (private/protected/final) meant to express these parent-child contracts. With interfaces + composition, those are unnecessary: You can compose an object and use it however you want; then, if you want the wrapper object to uphold the inner object's contract, you can additionally implement an interface to formalize that. The behaviours you use and the polymorphic guarantees you make are totally separate.
Inheritance mixes these ideas together and ends up worse for it. Not only the modern approach is simpler, it's more powerful: (1) Polymorphic extension (i.e. extending a polymorphic parent class at runtime) is doable, and (2) multiple inheritance is a non-issue.
[1]: I call it "modern" because newer languages like Go and Rust have eliminated inheritance in favour of exclusively using interfaces/traits.
[2]: See the fragile base class problem.
> modern¹ approach (interfaces + composition)
Smalltalk protocols and message categories were a step toward this (for example you could classify messages as implementing a particular interface, such as the collection or stream protocols), but Smalltalk lacked the type and interface checking supported by Java and other languages.
When I first took an object oriented programming class it was all about inheritance so that's what I tried to use for everything. Then I started writing real programs and realized that inheritance sucked then finally found the succent "Favor composition over inheritance".
There are days I hate the mapping of plain English terms of art over actual in-language effects.
Considering sets, if something is, in set terms a specific subset with a defining membership or characteristic of a definable superset, representing that at compile time effects a hard constraint which honours the set Venn diagram.
If that set/subset constraint doesn't exist then you have to ask yourself if applying a compile time constraint is appropriate.
I went out of my way to implement inheritance and then make it multiple. Of course I'm going to use it.
Inheritance is just a more deeply integrated form of composition which puts the inherited parts on equal footing with the new parts.
That reduces certain indirections and frictions, which is sometimes useful when making things out of other things.
When someone realized that the inheritance glass castle is doomed to always get shattered upon contact with the real world.
Inheritance might be OK for formally finite domains but I can’t envision other cases where it should be favored.
Hierarchy (and thus "inheritance") is a way to express that several different things share the same quality. They are different in general, but same in some way. It is a very natural way for people to express such a thing and no wonder it is so widespread. But it is not the only way nor the general way, of course.
Composition is not an opposite to inheritance. An opposite would be something like:
Or, if the body of the method is same ("a parent method"): Here we do not give A and B places in the hierarchy but merely say they respond to the same message or that even the procedure is same.I do not know if any meaningful and systematic alternative to a hierarchical way exists in any programming notations. Interface spec is a partial way, but that's all. (I know only a few notations, of course).
You can get Liskov from interfaces too. I rarely (like once in a career) need inheritance.
Looks like this one was reupped from a week or so ago, there was another submission with three comments too:
https://news.ycombinator.com/item?id=45845505
In 2006 when changing code that had lots of inheritance.
the article seems to be digging into justifications for using inheritance. one thing I've heard and it seems to work is inheritance is ok for interfaces but usually not good for implementations.
Yeah inheritance is just not the point of OO. It’s fine but it’s not what’s really useful.
The only time I use inheritance is when I have an abstract base class, and several flavours of subtypes, all sealed.
I’ll be honest. I don’t really understand the point of this article. Maybe that’s just a preference thing. The philosophy behind these abstractions is the least interesting part of the question for me. What problems do these various methods of polymorphism solve and create? What solutions do they enable or prevent? That’s the only part that matters. But citing some discussion about the philosophy behind the theory from 40 years ago is not particularly enlightening. Not because it’s not relevant. But because we have 40 years more experience now and dozens of new languages that have different takes on this topic. What has been learned and what has been discovered?
I usually think of the ideas behind "composition" as "how do I assist a future developer to replace the current (exported) implementation of a type with a new one by restricting external visibility of its internal implementation through the use of private methods and data".
In "inheritance", it often feels like the programmer's mindset is static, along the lines of "here is a deep relationship that I discovered between 2 seemingly unrelated types", which ends up being frozen in time. For example, a later developer might want to make a subtle innovation to the base type; it can be quite frightening to see how this flows though the "derived" types without any explicit indiction.
Of course, YMMV, but I think of "composition" as "support change" and "inheritance" as "we found the 'correct way to think about this' and changes can be quite difficult".
Since I think that the key to building large systems handling complex requirements is 'how do we support disciplined change in the future' (empowering intellectual contributions by later generations of developers rather than just drudge maintenance).
> This contrasts inheritance as a “white box” form of reuse, because the inheriting class has full visibility over the implementation details of the inherited class; with composition as a “black box” form of reuse, because the composing object only has access to the interface of the constituent object.
So, we just need devs to stop trying to be overly clever? I can get behind that, “clever” devs are just awful to work with.
It takes about 2-3 years of experience in current enterprise scale to deeply realize that inheritance fundamentally doesn't work.
If it's a car with extra wheels, do inheritance.
If you're adding a device for navigation that could be used by other things, go for composition.
Once upon a time inheritance was a way to compose classes out of pieces of orthogonal, general functionality.
Is-a Winged, TurbinePowered, Piloted, Aircraft, etc
How about not favoring anything. There are many paradigms and each one has its place. Franky I do not really understand why do developers fight these religious wars about languages, frameworks etc.
> There are many paradigms and each one has its place.
That's a thought-terminating cliché. The argument against inheritance has been laid out pretty clearly. It's reasonable to rebut that argument. It's not reasonable to say, "you shouldn't criticize inheritance because Everything Has Its Place." Everything does not have its place. Sometimes we discover that something is harmful and we just stop using it.
> Sometimes we discover that something is harmful and we just stop using it.
And that is not remotely the case here. So yeah, there are many paradigms and each has its place.
Gameplay logic inherently leans more towards composition, with a little hint of inheritance.
You can have players and monsters, which are all types of "characters" or "units", which is inheritance, but instead of having a separate FlyingPlayer and a separate FlyingMonster, which use the same code for flight, you could have a FlyingComponent, which is composition.
I've been going all in on composition and it's amazing for quickly implementing new gameplay ideas. For example, instead of a monolithic `Player` class you could have a `PlayerControlComponent` then you can move that between different characters to let the player control monsters, drones, etc.
Imagine instead of only Pac-Man being able to eat the pills, you could also give the ghosts the `PillEaterComponent` in some crazy special game modes :)
I've also been fantasizing about a hypothetical language that is built from the ground up for coding gameplay, that doesn't use the word "class" at all but something else that could be a hybrid of inheritance+composition.
When they put away childish things and read about the SOLID principles. Different time for every engineer.
SOLID is a childish thing, imo. Very undergrad.
"Single responsibility" isn't an especially useful yardstick. If you actually need to decompose a complex piece of logic into modules, the place to start is by identifying areas of high cohesion and separating them into loosely coupled functions. Ideally you can match those up to a DDD-style ubiquitous language, so your code will make intuitive sense to people familiar with the domain. "Does this have one responsibility?" really isn't the right question to ask.
The open-closed principle is straight-up wrong. Code should be easy to modify and easy to delete, and you only rarely need to add hooks for extensibility. Liskov substitution is fine, but it has more to do with correctness than cleanliness. Dependency inversion is a source of premature abstraction—you shouldn't open the door to polymorphism until you need to. Interface segregation is good, though.
In general, I think SOLID is overly enamoured with the features of object orientation. Objects themselves just aren't that big of a deal. It'd be like making the whole acronym about if-statements. If I were going to make a pithy acronym about legible code, it'd have more to say about statelessness, coupling, and unit tests. It'd reference Ousterhout's idea of deep modules, and maybe say something about "Parse, don't validate," or at least something against null values.