I've found that inheritance is very useful in one situation, and adds nothing over mixins otherwise.
If a class does two broad things simultaneously then inheritance can work great. For example, a User class that inherits from a DB mapper class. I don't want to have to tell my class how to write a record to the DB. All that code can be centralized into one thing and then relied upon for its uniformity across all my models.
This isn't true the way most people use inheritance though. They do things like Sword inherits from Weapon and Weapon inherits from Item. But this just asks for trouble because as requirements get more complex there are more edge cases and the complexity bubbles up into overriding the inherited methods, which makes them less reliable from different calling contexts, or flipping the OO script and pushing class-based-if-statements in the ancestor class.
Then you step back and say "why did we make Sword a weapon in the first place?" and the answer was we had logic somewhere else in the code that did things like check if a user was armed. Well we don't need inheritance for that at all. We can use plain old methods and properties / duck typing.
The problem is that we're trying to formalise relationships that make perfect sense in natural language by assuming the same relationships make perfect sense in code.
"Sword" instances may have specific properties that make them incompatible with a "weapon" superclass. Is a broken sword still a weapon? How about a sword with no handle? Or a sword that has been magically transformed into a flower? Is that still a sword and a weapon, or is it now "really" a flower, and should inherit all flower methods while throwing away all weapon methods?
You can duck type and/or RTTI your way out of this problem, but you can't avoid the fact that traditional OOP is very bad at handling these "it depends" cases, because the only relationship it supports is a strict and static inheritance hierarchy.
Unfortunately many domains, including natural language, can only be described by mutable context-dependent relationships.
In the real world, swords don't turn into flowers, so you might think you're safe. (Except that you might want to include object mutability in a game...)
But in NL the meaning of a phrase can change according to social setting, unstated subtext, speaker gender, age, and even time of day. It's all context, and it can't be ignored without losing essential detail.
All current typing systems seem to be attempts to enforce limited-scope static relationships between opaque atomic objects with more or less static properties.
Mutable context-dependent relationships are everyone's worst nightmare in CS. Academic CS seems to have spent most of its career trying to pretend they don't exist, or if they do, to make them go away.
This is sold on the basis of making more reliable code. In fact it simply doesn't work elegantly for entire classes of problems, including many problems for which it seems to work just fine if you describe them in words, until you have to think about all the possible details.
The worst case output is intolerant brittle code with limited features, and the best is an encrustation of exceptions, edge cases, and work-arounds.
Note I'm not saying there's a simple answer, because there isn't. This is a research-grade problem, and it's barely been considered.
I am saying - beware of simple principles like LSP that claim to solve this problem. Because there are many situations in which they simply don't.
The problem with LSP is that it is an incredibly hard property to prove (behavioural subtyping in general is undecidable). So in theory yes, it sounds great in practice with a sufficiently complex system good luck ensuring that you adhere to it.
Except Firearms require the reload() method be called periodically between calls to use(). So now we have to think about whether Swords need an empty reload() method or whether there need to be separate Firearms and Blade interfaces based on Weapon.
Yup, that's how it works. And why inheritance and polymorphism is so popular: it's powerful, easy to explain, and captures elegantly a lot of problems we need to model.
In contrast, non class based languages such as Haskell struggle to model problems that are trivial in OOP, such as how to reuse 90% of existing functionality but override 10% with more specialized behavior. Good luck solving that problem elegantly with type classes.
That always seems to the problem with deep hierarchies after a while. Suddenly you have something where one of the inherited methods shouldn't be there. You can't take it away so you have to do something clunky like throwing an exception or making it empty.
The funny thing is that languages like C# and Java abandoned mixins but kept deep inheritance hierarchies. When I did C++ more we used multiple inheritance a lot but with only one or two layers deep. This worked extremely well and now that that I am doing more in C# I miss it a lot.
> I don't want to have to tell my class how to write a record to the DB
You don’t need inheritance to achieve this.
Your User class contains data and probably some business logic. The fact that it’s going to be persisted in a database at some point is a detail that the class shouldn’t know.
Consider using the data mapper pattern (or an ORM implementing this pattern). In this pattern, your User object doesn’t even know it will be persisted. It doesn’t have any parent class. You just manipulate your entities like a graph of plain objects, and when you have finished you ask the data mapper to persist them. In my experience this is much better than what you are describing, which look like the Active Record pattern.
> If a class does two broad things
Doing this is a violation of one very commonly accepted principle: The single responsibility principle.
Active Record should be considered an anti-pattern and is definitely in violation of single responsibility at the least.
Myself, I wouldn't even put any business logic in the User object, except maybe representational logic (e.g. have a couple of helper methods that return multiple representation of the same piece of data).
One of the best things to do is separate behavior from data in the first place, which feels kind anti-OO philosophy, but is definitely easier to reason about and work with.
They can be modeled as inheritance or as composition plus delegation, but that's not saying much since inheritance itself can be modelled as composition plus delegation.
I think composition and mixins should be kept separate. In my mind (and maybe I have an incorrect view of it), mixins throw a bunch of new properties on the base object, whereas composition keeps the original object encapsulated in a single property. Would you consider the second type to be a mixin? I've never heard it described that way.
If a class does two broad things simultaneously then inheritance can work great. For example, a User class that inherits from a DB mapper class. I don't want to have to tell my class how to write a record to the DB. All that code can be centralized into one thing and then relied upon for its uniformity across all my models.
This isn't true the way most people use inheritance though. They do things like Sword inherits from Weapon and Weapon inherits from Item. But this just asks for trouble because as requirements get more complex there are more edge cases and the complexity bubbles up into overriding the inherited methods, which makes them less reliable from different calling contexts, or flipping the OO script and pushing class-based-if-statements in the ancestor class.
Then you step back and say "why did we make Sword a weapon in the first place?" and the answer was we had logic somewhere else in the code that did things like check if a user was armed. Well we don't need inheritance for that at all. We can use plain old methods and properties / duck typing.