Polymorphism and other important aspects of inheritance
utcc.utoronto.ca
utcc.utoronto.ca
I feel like this "hot take" is actually a pretty common take.
You can have polymorphism without inheritance; you can't really have inheritance without polymorphism. To the extent that inheritance necessarily "includes" polymorphism, so polymorphism is in that sense an aspect -- yeah, it's more important and fundamental.
And I think this is actually a pretty common take. All the takes saying inheritance itself is not fundamental to OO (and even sometimes saying OO is better without inheritance), are variations of saying this.
"private inheritance" which is really composition is inheritance without polymorphism
I always though that dichotomy missed the point. You can give-up inheritance for generics, for interfaces (as if those were a different thing), for type classes, or for type ducking; but you can't give-up inheritance for composition because the only thing composition solves is code reuse.
Interfaces are a different thing than inheritance, precisely because of what we are talking about. Inheritance is about code re-use, and interfaces are about polymorphism. Interfaces are the polymorphism part of "inheritance", while dropping the code re-use part.
It is true that composition does not give you polymorphism, absolutely.
> because the only thing composition solves is code reuse.
Exactly. Inheritance combines polymorphism and code re-use in one form. If you instead don't have inheritance, than interfaces (or something similar by a different name; or "duck-typing" if you don't have static typing and also don't really want any kind of typing at all) give you polymorphism, so what gives you code re-use? Composition.
If you split the code re-use function from the inheritance function, interfaces plus composition is the (or one very common/obvious) solution.
I don't think we are disagreeing, I mean to show you we are all violently agreeing. :)
I do see some value on the "automatic linking" part, but deciding you don't want the complex mixed concept on your idiom isn't any radical idea that will change your life (neither for the better nor for the worse).
Not necessarily; see "Interface vs. Implementation (eg. private derivation in C++) inheritance".
I think the big abstraction problem inheritance solves is overrides. I think there’s a lot of FP cargo culting that overlooks the benefits of the ability to override (when used wisely). But aside from that I’m not sure there’s any reason to think inheritance is any more or less polymorphic than composition for a language with sufficient reflection/introspection support. If you lack type introspection it’s a different story. Maybe I’m missing your point though.
Favor composition is just that your solution will most likely be better with it. There are still cases where you want inheritance but also most likely you don't want to create class hierarchy with 5 levels and everything inheriting form everything else but maybe have have a base class. So even in single application or microservice you might still have some inheritance and most likely more composition.
Besides composition uses interfaces heavily because you want to use DI with constructor injection and then you can switch implementation even at run time.
Interfaces are fine.
The problem with inheritance is the code reuse mechanism
That's not true. In C++ you get inheritance without (runtime) polymorphism if all methods are non-virtual. It's very common actually.
The four principles of object oriented programming (what they are and why they are wrong!) https://www.youtube.com/watch?v=YpYLXq4htKY
> Abstraction, Polymorphism, Inheritance and Encapsulation… Yikes! What the heck does all that mean and why should anyone care?
> Programming is full of jargon. Object Oriented Programming takes jargon to a whole new level. But even so, that list of the “four fundamentals of object oriented programming” is missing something important. In this video, I explain what the four principles are. And I then explain why they are making a big deal out of something that is essentially quite simple."
Later in the video:
> I'd say polymorphism is probably a hundred dollar word for a 50 cent idea. These days computer science teachers and writers have extended the meaning of polymorphism to such an extent that it's often hard to figure out what the heck they're trying to describe.
The other place you want code sharing is the implementation of that external interface. Inheritance is one well-known way to do this, but composition/delegation is another strategy to achieve the same thing. Golang's embedded structs are a pretty streamlined (but fairly limited) way of achieving this, and e.g. Kotlin has explicit delegation: `class Derived(b: Base) : Base by b`. Languages like e.g. Java (where classes are open by default) push you towards inheritance, while languages like Kotlin (where classes are final by default, and where you have explicit support for delegation) push you towards composition.
A whole lot of confusion comes from most mainstream OOP languages conflating inheritance and subtype polymorphism.
In C# I could easily write:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class Employee : Person // Employee inherits from Person
{
public string Company { get; set; }
}
Whereas in Go I might have to do this for "data embedding": type Person struct {
Name string
Age int
}
type Employee struct {
Person // This is embedding Person struct in Employee
Company string
}
There's nothing stopping me from doing that in C#. public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class Employee
{
public Person person { get; set; }
public string Company { get; set; }
}
> I do think it's important for a programming language to separate these aspects from classical inheritance itself...they are already separated?
interface IPerson {
public string Name { get; set; }
public int Age { get; set; }
}
public class Person : IPerson
{
public string Name { get; set; }
public int Age { get; set; }
}
public class Employee : IPerson
{
private Person _person;
public string Name {
get { return _person.Name; }
set { _person.Name = value; }
}
public int Age {
get { return _person.Age; }
set { _person.Age = value; }
}
public string Company { get; set; }
}
Employee conforms to the same interfaces as Person, but achieves this via delegation (the famous "favour composition over inheritance" thing)Please, note I don't dispute the "prefer composition over inheritance" thing. In fact, I heartily agree with it.
But in this specific case, where every sinle detail of Person is publicly exposed in Employee (no pun intended), I don't see a material difference. If anything, the example uses composition as a tool to implement inheritance on top of.
Am I missing something?
* https://wiki.c2.com/?AlanKaysDefinitionOfObjectOriented
* https://wiki.c2.com/?AlanKayOnMessaging
"OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme LateBinding of all things." ~ Alan Kay
"The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be." ~ Alan Kay
The antithesis of OOP is exposing the name and age of a Person to the greater system at large.
That definition is dead. Wish it well, then bid it farewell. "OOP" means "Java" and we can just call Erlang and co "Agent Model".
1. The code the is used to implement languages or the main "library" of said languages and your code to implement products don't have to and probably should NOT mirror each other.
Other ways to think about it: Just because Java standard library has a generic List -> LinkedList or ArrayList etc..., doesn't mean your software related to managing Patients does NOT mean you need this complication:
Human <- BillablePerson <- Patient (this is somewhat whacky).
I often see developers do things like this because its "a best practice" and maybe there's a 1% of your code that will take advantage of something like that. In reality it makes a lot more sense to just generate a Bill and send it to an Patient. Whether they are a BillablePerson or not doesn't really matter. Anyone opening the user interface can see a payment screen, right? You can flip the status on a Bill to paid, right?
Yes the pedants will come running. And yes if you're app is a framework for medical billing software (you're not a billing company but a company that makes billing frameworks) _maybe_ this is useful? But my answer to that is - even in that case, more and more we're outsourcing screens and concepts to other cloud systems. The concept of polymorphism is neat to iterate a LinkedList and an ArrayList, but it's not buying much for an invoice that gets paid in freshbooks and a patient managed in some EHR software.
So as much as it's nice to "wrap all the things" the software we write is increasingly outsourced to yet another API call.
Last two companies I worked at the config.json file was 50 lines, almost 90% of which is API tokens to various cloud SaaS vendors that take care of billing or email or storage or uploads.
Wrap all the things? It's just not feasible anymore.
2. Second thing I've learned: Don't react to Everything.
Have a bad deployment? Have some requirements that were lost? You can definitely implement a process or two especially for repeating issues. But don't react to everything. If you make a new policy or process or 2-step program for any frown you generate, you'll never get back to coding up the solution. You'll drag your software developers through red tape.
> Human <- BillablePerson <- Patient (this is somewhat whacky).
Whenever I see something like this I push the implementor to point me to some code that accepts a "Human" as a parameter and actually does something with it (other than casting it down to Patient). If they can't, I try to encourage them to drop the unneeded hierarchy.
> managing Patients does NOT mean you need this complication:
> Human <- BillablePerson <- Patient (this is somewhat whacky).
Also: class hierarchies don't have to match real-life hierarchies. Often I find it more useful to implement hierarchies that make the program flow and algorithms work more smoothly and accept hierarchies that don't mirror anything in the physical world.
Of course now you have to talk about semantic polymorphism, e.g. will the outcome of calling some polymorphic functions have the right effect on the system? This is where static type systems have it easier because you typically tie stuff together in related hierarchies.
The Go structure embedding thing was a surprise to me, having grown up with C and C++ it looks unexpected. Not sure I like it but I think I do.
Go and TypeScript are examples of languages with statically checked structural type systems. While C# has statically checked nominal type system.
I don't use dynamic languages much, so maybe someone else could chime in with a few examples.