huh :o) the wisdom since late 90's has been to avoid it like a plague, at least in c++ circles. for example, see numerous articles in 'guru-of-the-week' by herb-sutter.
the same wisdom can be applied elsewhere as well, there is no need to keep discovering the same truths in different contexts again, and again.
>huh :o) the wisdom since late 90's has been to avoid it like a plague
I am sure that hundreds of thousands of students have at some point heard that these hierarchical organization of data is how you should model a system.
IMHO inheritance works and only works for graphics related programming, e.g. GUI and games. Visual objects can be abstracted in the manner of inheritance.
Well, it does work well for the engine part of game engines, and graphics related code in particular as the GP says - it's just gameplay code where OO falls a bit flat.
I don't follow - role playing games are often most suited to ECS, because you want the kind of emergent gameplay that independent gameplay systems acting on component composition gives you - and the kind of flexibility it allows in composing weapons, spells, buffs/debuffs, and so on, vs a strictly hierarchical OOP approach.
Not really, the data oriented design movement in games programming that eschews OOP is driven by performance concerns of traditional OOP code on areas like rendering.
As a graphics programmer for many years I can also say that OOP is not really a good fit design wise for modern rendering engines, it mostly just gets in the way.
I only really know of Bevy that uses ECS for the rendering backend, but iirc even that doesn't really use entities and components, but rather its resource system, such that rendering commands are stored in the ECS "world" but as globally unique resources, rather than entities and components. But, my knowledge doesn't extend to AAA engines, perhaps things are done more extensively there.
The implementation of inheritance (more specifically, polymorphism and dynamic dispatch via vtables) is a problem in games, because it adds an extra layer of indirection, and screws with cache locality. But the semantics of inheritance (X ISA Y) still apply. In an ECS, the implementation is different (struct-of-arrays) but you can still think of an entity as "inheriting from" the various components its built from.
> you can still think of an entity as "inheriting from" the various components its built from.
If it's based on components, wouldn't that mean you would think of it as being... _composed_? I typically don't hear "it's made of many components" and think of inheritance
That's not really accurate. Component based systems are designed specifically to address situations that traditional inheritance doesn't handle well.
Examples are usually something like having a base entity, a player that inherits from entity and an enemy that inherits from entity. Then you have a magic user and a barbarian inherit from enemy but now you also want your player to be able to use magic. Traditional OOP doesn't make it easy to share the implementation.
Even with composition and interfaces you still have problems with most traditional OOP languages when you want to do things like change the set of components of an entity dynamically at runtime (player gains or loses the ability to use magic during the game).
"Is a" is often not the relationship you want to model. A player and an NPC both have the "has a" relationship to an inventory, not the "is a" for example.
For games having your character as a type of a general class fits well. Having enemies be a type of a class with a base of abilities can fit well too. Weapons, etc all module well. Game play. Scenes.
This has more or less been my experience too. Things like UI toolkits or game engine primitives are often solved nicely with inheritance.
I've been doing a lot with LeafletJS lately, and it has some light inheritance around its drawing primitives (things like boxes, circles and lines you draw on top of maps). It works well.
For 3D engines you still need to be quite careful with where you apply it though. It's possible to get into a huge mess if you use it too much.
Especially in games, inheritance doesn't work all that great and had been replaced by composition via components a long time ago where each component implements one specific aspect or feature of a game object and can be (more or less) freely combined with other components.
Most of us have no contact with Java. But Java offers little else than inheritance to use to organize a system, so Java coders use it for everything. They are not exactly wrong, except in using Java at all. But sometimes is all that is allowed.
This is much less true for modern Java than it used to be, with records, pattern matching, sealed classes, etc.
The trick though is you actually have to use modern Java, which means you need to both be on the right version of Java, and have developers that understand the value/power of these newer constructs. Which is surprisingly rare for a programmer that self-identifiers as a Java programmer.
"correct" is certainly not the right way to put that. Inheritance and composition here are both fully valid methods for modeling the relationship, and the decision to use either should be dependent on how the models are being used and the expectation for future extension.
By "correct" here you only mean fashionable. Either approach works. Each has its merits and its costs.
Any big enough system will have parts that are most sensibly built object-oriented, and other parts that are more reasonably functional, plus anything else you can think of.
At one point "Object Oriented" became "Blockchain" (or now Gen AI) of those times. You had to be "object oriented" in order to be taken seriously. This applied to everything. Even finished software products were called "built using object oriented".
The esoteric concept of inheritance became popular after that. At some point it became so popular that people figured out that it is not really a good thing.
Pragmatists used inheritance because it gave a quick benefit now, and the longer-term costs were ignored. Some pragmatists became successful because they moved quickly, so the "Programmers from the Church of Purity" used inheritance because successful companies used inheritance. When inheritance was no longer the quickest way to move quickly, the pragmatists moved on. The Church of Purity now bangs on about Functional Programming in the same way for the same reasons.
Doing every simple thing that used to be done in "normal procedural" way in functional paradigm instead when using something like Scala or fp-ts in TypeScript.
Causing engineers to completely change their mental model of how the code runs, which I still have intuitive trouble imagining correctly and I see it with other developers as well. A lot of energy goes into trying to understand how to do a simple action. It is much harder to read the code and have a correct mental model as well.
It likely also depends on the person so I couldn't speak on behalf of everyone. I have been exposed to it to some extent, not the majority of work for over 5 years and it still intuitively is challenging for me. So similar task would take longer to code and existing code would take longer to understand. Significantly more effort would go to that.
I feel like I spend more time and energy on how to get something done functionally rather than focusing on what the correct business logic should be.
lol it's the opposite? map, flatmap, fold etc are very clearly defined operations with very clear use cases and rules. loops are not, you can do whatever you want (often mutating the underlying, rug pulling you every iteration)
not being familiar with fp doesn't mean it's objectively worse
I saw a lot more composition than inheritance in C++ and C#. I think inheritance and polymorphism have their place. Like anything, they can be misused.
Also a semi religion, I'm always seeing people say before OOP everything was terrible, and everything good thing is due to OOP (eg structs, methods, polymorphism)
In the 1990s, it coincided with the proliferation of GUIs and their respective programming interfaces. Most frameworks use hierarchies like Object->View->Control->Button->ImageButton. Then people decided that this is the way for modeling abstract problems that don't have to deal with visual or real-world entities whatsoever.
And you would get nice automatic completion when doing object name, dot, and waiting for the IDE to list all possible methods. This was exploratory programming, the copilot of its time.
I think there’s a tension between the tinkerer, blogger and hobbyist side of the field, and anonymous 9–to-5 workhorse line of business programming in this discussion.
The core reason everyone uses it seems to me to be essentially the same reason the former groups hate it - it’s what everyone else does, it’s an incredibly square and “day-job”-esque approach to one’s tooling and architecture, it often reeks of bureaucracy, compartmentalization, and organizational superstructures often unrelated to the technical work, dictated primarily by managerial fiefdoms and needs to organizationally coordinate, and it’s a well-defined space with extremely predictable solutions and headaches, and therefore has very little excitement or novelty to offer.
Many of the headaches associated with Rust come from an inability to reference from owned to owner. Affine types have much more impact on design than inheritance vs. composition.
OOP is easy to understand and explain and it makes sense initially, plus a lot of people specialize in languages/frameworks that enforce it, so it becomes easy to get trapped in the OOP world. It also usually comes with DDD (which is a solution to a problem you shouldn't have had in the first place) which is a way to limit the damages of OOP into contextual areas.
I also think the blanket statement (OOP is bad) does not apply necessarily everywhere. A good mix of composability and a bit of inheritance where it makes sense is the best way imo.
I disagree. Starting with separating class and object have never been helpful when I've tried to teach computer programming, while functions seems to have been more intuitive to those people.
After a while you get to closures, which are pretty much objects without classes, and then factory functions that produce closures and there you have something like a class as well. If I were to design a course I'd probably follow this flow to get to 'OOP' in that sense.
But you go from the bottom up here, I don't like to describe it that way.
I prefer to say that we naturally classify objects around us using fuzzy boundaries like "a house", "a cat", which don't exactly mean anything: they are templates we use later to actually generate an actual house, or an actual cat (on a piece of paper as a drawing for instance).
The world being separated between our internal classes and the interactive instances of them, we can code that way too: we look for what we actually need in a concept, name it and define only what we need, then instantiate several concrete actors interacting together. Each actor is complete, well defined, with contracts for interactions.
You can then start building on top of this more complex systems, talking in English and defining rationally the world of your little model.
If you start talking about functions, you're basically aliasing a memory address for a bunch of code: you'll goto that function address, with some parameters at some other address, do a bunch of things and put the result in another address for the next function to process. You're building at best a suite of pipelines, which ends up being a little bit too technical and static for my taste.
Another way to defend modelling a software with classes and object, in my view, without trying to talk about functions, is the blank page effect: imagine arriving at random at some assignment. You understand vaguely the business problem, now you need to code a solution. You have 6 months, will generate a few millions a year and will require a team of 10 people eventually to test, maintain, operate and debug: you start with defining functions generating functions with callback parameters in Python, or you launch IntelliJ and you do a Java class model ? I'd be terrified modelling complex problems with just functional pipelines, I think it's just way more obvious to talk about objects at all time if you're gonna do something that is not just processing inputs into well defined outputs.
I think you identified the selling point of OOP, it's easy to reason about it and logically it makes sense to human because we do think in terms of objects (the user, the order, the item etcetc) and responsibility segregation (User->login(), Order->fulfil(), etc).
To your example of just launching intellij and using java (or ruby or whatever) to build something, yeah it works and it works fast and it's easy to add new features, and it's also easy to end up with a >3k loc User class in python that no one can touch (true story).
I prefer to start with an interactive programming environment, like a REPL, to keep the feedback loop tight and code short. If I needed to talk for ten minutes about templates and instances and how you need to have a little meeting with yourself and do modeling I'd have lost their interest right away. These words mean nothing to a newbie, unless they're some kind of philosophy nerd so I can hook into ancient greek ideas about Forms or something.
'Here is a way to do simple math, here is a way to glue text to text, now that has grown a bit, you can shorten it for repetitions by giving it a name like this', and so on.
And to me, starting out functionally is easy. Data is pretty much always a given, it's very rare in practice that I first need to model speculatively and then generate data ex nihilo unless I'm creating mocks. Usually there is a data source, a file, a network API, whatever, and then I start building against that. Small, simple things at first, like mapping all the input to an output interface, then add transformations and output for these, and so on.
In general I spend more time thinking about the shape of data I already have than architecting code or inventing models.
Some modern programming languages do not even have classes to inherit from. Instead they have concepts like traits and structs, which decouple structure and behavior.
Smalltalk having a more or less revolutionary developer experience was likely quite important. A graphical interface to your system where you can traverse a tree and inspect _everything_, and more importantly, change it.
In Smalltalks as I know them, which is sporadically and shallowly, inheritance seems to be used as a factoring tool, when you break down functions into smaller functions there's a structure in which to place them with the implicit incentive to put them as high up as possible.
To me it seems to work pretty well, but it's also a rather weird environment where even classes themselves are objects.
In Java one can get around inheritance by using injection into an attribute in a wrapper class that extends or changes an object API. This leads to a similar decoupling as more functional composition, but you pay by having more objects swimming around in the VM during execution. Extending on the class level (i.e. in declarations of classes, interfaces, traits and the like) instead of the object level might have effects on performance and resource management.
Usually that likely doesn't matter, but it might. In Java-like languages I tend to use inheritance as a quick way to abstract over or extend functionality in a library class, I get the public methods 'for free' in my own class without having to write the wrappers myself. And that's usually where I stop, one level of inheritance, since Java isn't as inspectable and moldable as, say, Pharo, and doesn't give the same runtime ergonomics with large class/object trees.
I work on a very large codebase. We use inheritance and composition. I totally agree that inheritance should be used carefully. However, there are times when composition requires a lot more code and downstream maintenance.
Let's stay I have a base class A.
Let's say I have 70 classes that inherit from A.
These classes must serialize/deserialize from disk.
My choice here is that I can add a new data member to A and then update the read/write method in one place, or I add a new data member to all the classes that are derived from A, meaning that I get to update 70 classes. Seriously? Who would think this is a good idea? You'll have to write 70 times as much code... there will be times when that's definitely a very bad idea.
Maybe don't use inheritance. Then give every class its own "Name" data member like std::string c_nName. Every class can have its own function to set the name, or we could use an unencapsulated free function and do things like C. Except then the experts will say things like "well, now you're doing C with classes". Then you get a ticket that the user can enter bad names. You can then figure out some way to validate the names, right? That could be a free function, or maybe write some class called NameValidator. Except now the experts say you're writing C++ like Java. Too many classes, too few classes, too many objects, too few objects, too may free functions, too few free functions.
C++ expert of the month may say X, Y, Z, but look at Microsoft's APIs.
Inheritance is everywhere.
Look at any open source C++ project.
Inheritance is everywhere.
Does that mean that you should make something like:
A
--B : public A
----C : public B
------D : public C
--------E : public D
----------F : public E
Or this:
A B
\ /
C
Not unless you have a damn good reason. But the point is that inheritance is a valid tool and it can reduce bugs, code duplication, maintenance.
Right tool for the job, yeah? I once read something in the C++ FAQ that said something like "Don't take advice from people who don't understand your business problems."
The answer is that you retain A as a pure interface and maybe you add a convenience base below A that adds the data member, then you inherit from from the convenience base for your 70 leaf classes. You can also use CRTP for the convenience base, if it's appropriate. You can still avoid confusing interface and implementation inheritance.
If anything is common, someone will try to seem like a brilliant iconoclast by writing an essay against it. The particular essay in this case was from a Pythonista (not surprising, given Python's weird love/hate with objects):
I don't think inheritance is bad at all. It is very often the easiest way by far to model a problem. Sure, it's not perfect, but I think it is wildly overhated by a vocal minority.
Depends on how you use it, but in Java I prefer to use interfaces and no subclassing. I find classes with abstract methods hard to reason about, and much prefer to leverage Functions/lambdas where possible.
Have you ever seen wildly over-architected OO code where everything seems to be an abstract class and it seems impossible to find out where stuff actually happens?
Inheritance is like a lot specialised tools - it can be useful in some situations but I think those are rarer than supporters might thing. Completely refusing to use inheritance and always using inheritance both seem like extreme views that should be avoided.
You haven't lived life to the fullest until you've had to debug an issue in a 12-layer-of-inheritance class with the original call ping-ponging a couple dozen times across the layers and overrides everywhere.
I guess one could say this would be workable with proper tools, but the IDEs just aren't there. Move up a level in inheritance, ctrl-click on a call? It was overriden somewhere in the hierarchy, but the IDE will send you to the parent-class definition regardless.
I have actually seen one (1) beautiful C++ codebase with very good use of OOP and multiple inheritance, and author (I was his intern) painstakingly taught me about inheritance and its good uses. He used Design and Evolution of C++ (!) as a bludgeon. It was 2006.
Feels like that time my physics PhD student girlfriend asked me "hey you're a programmer, right" and I was one until I discovered ROOT and suddenly I lost taste for life and anything related to Computers.
Sure, but I have also seen C code where random structs with function pointers are passed to god-knows-where, and I will take inheritance over that any time of the day.
I have found that single inheritence can be useful but it is very restrictive.
More than once, I have found an inheritance hierarchy that made sense when first created no longer models the problem well. It becomes hard to change it at that point.
Frequently I find I really want to mix in cross cutting concerns across different hierarchies. This isn't really a surprise; most problems do not naturally decompose to only a single view of things.
I don't mind abstract base classes containing common functionality, so it's useful there, but mix ins would be better to be honest.
I think that’s just what you’re used to. Since I switched from Java to Go 7 years ago, I don’t think I’ve missed inheritance a single time. I haven’t needed to model anything with inheritance once. There are definitely things I’ve missed from Java, but not inheritance.
A lot of people who only know inheritance and can't understand why a lot of us slag on it so much don't understand how inheritance conflates implementation and interface. Polymorphism based on interface is a fundamental tool of programming. The way inheritance also drags along an implementation turns out not to be, and to probably be more trouble than it's worth. There are other ways of bringing along implementation, including the basic function, and those work better.
When inheritance is your only tool for interface, I absolutely agree that it looks fundamental, but the fundamentalness is coming from the interface side. Once you separate out interface from implementation, it turns out I have little to no use for the implementation portion.
I have also been programming in Go for a long time now. I sketched out an inheritance system once for an exercise. It's doable, though it's a terrible pain in the ass to use. I've kept it in my back pocket in case it is ever the right solution to a problem even so... but it never has been. And that's not because of any irrational hate for the pattern itself. I've imported other foreign patterns sometimes; I've got a few sum types, even though Go isn't very good at it, because it was still the best solution, and I've got a couple of bits of code that are basically dynamically typed, again because it was the best solution, so I'm not against importing a foreign paradigm if it is the correct local solution. Inheritance just... isn't that useful. The other tools that are available are sufficient, and not just begrudgingly sufficient or "I'm insisting they're sufficient to win the argument even though I know they really aren't"... they really are sufficient. Functions are powerful things, as the functional programming community (it's right there in the name) well knows.
I have never really seen it work. To me it seems it can work if and only if you are doing serious waterfall projects, with a tightly bounded scope.
If you have to model your data exactly once I believe it can work. But if you are doing any kind of agile, or even somewhat flexible waterfall, you will find out that at some point none of your data models work.
> I don't think inheritance is bad at all. It is very often the easiest way by far to model a problem.
I think the point of the article is that in some (very popular) languages, inheritance comes with a lot of baggage - precisely because classes in those languages support 3 different use cases. It's not saying that your usage is bad, but that your language didn't provide you with the best tool(s).
A lot of how people use classes can be solved via ADTs - which are much simpler to grok.
A lot of how people use classes can be solved via namespaces/modules, but not all languages have good support for them - so they use classes.
IDK, I recently did a big presentation on the topic in my company's Python community and there were tons of people that were genuinely surprised it's not great. And people in my department love throwing random classes that inherit from everything in the universe for no reason at all... for python. Like not even Java or C#. And coming in as the lone new-comer its not going to be easy to convince everyone that has been there for years that the common sense aint great.
Kotlin has a few nice patterns here. It allows inheritance but only if you mark your class as open (it's closed by default). This prevents people inheriting from things that weren't designed from that. It also has extension functions, which allows you to add functions and properties to types without having to inherit from them. This is very nice for fixing things that come with Java libraries to be a bit more Kotlin friendly. Spring offers a lot of Kotlin extension functions for its Java internals out of the box. Another nice pattern is interface delegation where you can implement an interface and delegate to another object that implements that interface:
class Foo(private val myMap: Map<String,String>=mapOf()): Map<String,String> by myMap
This creates a Foo class that is a Map that delegates to the myMap property under the hood. So you side step a lot of the issues with inheritance; like exposing the internal state of myMap. But you can still override and extend the behavior. Replacing inheritance with delegation is built into the language.
The net result of this is that inheritance is rarely needed/used in Kotlin. It's there if you really need it but usually there are better ways.
Scala and other languages of course have similar/more powerful constructs. But Kotlin is a nice upgrade over Java's everything is non final by default (or generally defaulting to the wrong thing in almost any situation). Go has a nice pattern based on duck typing where instead of declaring interfaces, you can just treat something that implements all the right functions as if it implements a type. Rust similarly has some nice mechanisms here. All these are post-Java languages that de-emphasize inheritance.
One of the ways that I think Kotlin and Rust are objectively better than Java and C++ is in that they have saner defaults than their predecessors (like open/final and mut/const).
I've lost count of how many talks I've watched by Kate Gregory where she advocates for people tagging everything they can as const in their C++, but asking people to eat their veggies never works.
The article has a nice history review of inheritance, but it would have been useful for there to be some concrete examples.
The worst example of inheritance I've ever found is in java Minecraft's mobs[0]. It is very deep in some places and has numerous examples of sub classes not fully implementing their parent interfaces.
Blaze has a bit for 'Is on fire', but how is this any different from Mob 'Is aggressive'?
Blaze > Monster > Pathfinder Mob > Mob > Living Entity > Entity
Yes, now take it further. Anyone not using components for all domain modeling is insane. Why does everyone assume this wonderful practice only applies to games?
Because the negative consequences are more pronounced in games. You can more easily get away with not doing it in other domains, so there it may be silly, but not "insane".
Any recommendations for where we can learn about this approach, or recommended search terms? I'm struggling to find anything through Google that isn't about the Unity component system in specific. Is it this? [1]
Sometimes inheritance is extremely useful.
Sometimes it's harmful.
Sometimes it's not the best but the most practical nontheless.
Language concepts are tools. These people with extreme black and white views live under the delusion that there can be some "Perfect Language which will cause the program to never develove/get technical debt TM".
Pure delusion.
Real life is an approximation where you do what is best in practice, not in some delusional daydream of perfection which breaks the moment you try and bring it to reality.
I feel people don't understand what inheritance and (object orientation in general) is useful for, misuse it, and then it gets a bad reputation. It's not about making nice hierarchies of Cars, Fruits, and Ovals.
For me the main point is (runtime) polymorphism. E.g. you have a function that takes a general type, and you can pass multiple specific types and it will do the right thing. And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them.
You can get this without implementation inheritance, it is also possible to just have something like interfaces. But I do find it very convenient to put common code in a base class.
Polymorphism is doable in plain old C with lookup tables and function pointers. If that is the only benefit, what is the point of creating a language where everything is an object?
Presumably one would write an object-oriented version of quicksort to go with their OO C library design and not use the stdlib functions. For example GObject is OOP in C, and I imagine it has collection types with sort methods.
Inheritance is what will bite hard when hand-rolling OOP in C. For one, you can forget about the compiler enforcing substitutability and co/contravariance for you.
> what is the point of creating a language where everything is an object?
I think that's the ultimate culprit in everyone hating inheritance. If it weren't for Java, I think we'd all have a healthier view of OO in general.
I learned OO with C++ (pre C++-11), and now I work at a Java shop, but I'm luck that I get to write R&D code in whatever I need to, and I spend most of my time in Python.
In C++ and Python, you get to pick the best tool for the job. If I just need a simple function, I use it. If I need run-time polymorphism, I can use it. If I need duck-typing I can do it (in Python).
Without the need for strict rules (always/never do inheritance) I can pick what makes the best (fastest? most readable? most maintainable? most extensible? - It depends on context) code for the job.
Related to TFA, I rarely use inheritance because it doesn't make sense (unless you shoehorn it in like everyone in the threat is complaining about). But in the cases where it really does work (there really is a "is a" relation), then it does make life easier and it is the right thing.
Context matters, and human judgement and experience is often better than rules.
Yes, you're right. I've done that in high-performance code where I couldn't afford the double function call of a virtual function. I forgot about that.
> If it weren't for Java, I think we'd all have a healthier view of OO in general.
>
> I learned OO with C++ (pre C++-11), and now I work at a Java shop, but I'm luck that I get to write R&D code in whatever I need to, and I spend most of my time in Python.
>
> In C++ and Python, you get to pick the best tool for the job. If I just need a simple function, I use it. If I need run-time polymorphism, I can use it. If I need duck-typing I can do it (in Python).
Those first two you can do in (modern) Java. The third is a mess to be avoided at all costs. Interfaces and lambdas will cover most reasonable use cases for polymorphism.
Control flow is doable with gotos. What benefit therefore is structured programming?
Dynamic dispatch is implemented under the hood with lookup tables and function pointers. Sometimes, it is nice for a language to wrap a fiddly thing in a more abstract structure to make it easier to read, understand, and write.
Yes. I have a framework for an embedded system that uses various types of sensors. When changing a sensor, instead of rewriting the polling loop for every new case, I can keep looping through ‘sensor[i]->sampleNow()’ and add the specifics in a class inheriting from SensorClass.
> For me the main point is (runtime) polymorphism. E.g. you have a function that takes a general type, and you can pass multiple specific types and it will do the right thing.
The runtime part is what I dislike. If I have a fruit which is an apple or a banana, I can't pass that to a method expecting an apple or banana. It can only be passed as a fruit.
> And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them.
This is common OO wisdom that I strongly disagree with. For example, in my program I have a few types (Application, Abstraction, Variable, etc.), and a lot of transformations to perform on those types (TypeCheck, AnfConvert, ClosureConvert, LambdaLift, etc.).
I prefer to have all the type-checking code inside the TypeCheck module, and all the closure-converting code inside the ClosureConvert module. I'd take the "huge if-else" statements inside TypeCheck rather than scatter typechecking across all my datatypes.
> If I have a fruit which is an apple or a banana, I can't pass that to a method expecting an apple or banana. It can only be passed as a fruit.
You can by overriding the method on apple or banana. If your method is on some other object, then yes, you cannot do this unless your programming language supports multiple dispatch.
> I prefer to have all the type-checking code inside the TypeCheck module
There are ways to have your cake and eat it too, here, at least in some languages and patterns.
For example, in Go you could define "CheckType" as part of the interface contract, but group all implementors' versions of that method in the same file, calling out to nearby private helper functions for common logic.
Ruby's open classes and Rust's multiple "impl" blocks can also achieve similar behavior.
And yeah, sure, some folks will respond with "but go isn't OO", but that's silly. Modelling polymorphic behavior across different data-bearing objects which can be addressed as implementations of the same abstract type quacks, very loudly, like a duck in this case.
> And yeah, sure, some folks will respond with "but go isn't OO", but that's silly. Modelling polymorphic behavior across different data-bearing objects which can be addressed as implementations of the same abstract type quacks, very loudly, like a duck in this case.
It's less "this is not OO" and more this is not inheritance, which is why a lot of people are saying you can find more elegant solutions (like this) rather than use inheritance for no clear benefit.
> The runtime part is what I dislike. If I have a fruit which is an apple or a banana, I can't pass that to a method expecting an apple or banana. It can only be passed as a fruit.
Heh? An apple is a fruit, you can pass it to any place expecting the former. Like, this is Liskov’s substitution’s one half.
With generics, you can be even more specific (co/in/contra-variance).
Fruit* fruit = new Apple();
ConsumeApple(fruit); // Doesn't work; requires Apple*
fruit = new Banana();
ConsumeBanana(fruit); // Doesn't work; requires Banana*
ConsumeFruit(fruit); // Okay, function signature is void(Fruit*)
But why would you want to be able to pass an apple to a function expecting a banana? Doesn't even make sense, the whole point is that the type system forces you to consider this stuff.
If OP wanted to be allowed to do whatever and just have the software fail at runtime, JS is right there.
> But why would you want to be able to pass an apple to a function expecting a banana?
I would never. I know that I'm holding a banana, I want to pass it to a method that receives a banana. But what I can't do is put my banana through a rotateFruit function first, because then Java will forget that it's a banana and start treating it as a fruit.
Inheritance is just a tool, that you use if it fits you software architecture. You use other tools, like generics, interface, where it makes senses to ise them. I think people wants the one true way to not have to design their software. And when they use inheritance in a ways that does not fit, they blame inheritance.
If you pass your object to another function, you only get static dispatch. What you want instead in this case, is a simple instance method. Then yourApple.rotateFruit() would do what you want, and rutateFruit could be an interface method declared in Apple, the superclass/interface.
if i were writing an intro to programming book, i would introduce OO as a means of building encapsulation. i'd only get into inheritance in later chapters.
The core feature of OOP is just bundling functions with the state it processes.
When you bundle state and functions together, you can't predict what calling the function will do without knowing both the code and state.
You can say its 'the real benefit', I guess, but that feels like circular reasoning. Its pretty much the definition of what OOP is, so calling it a benefit feels weird.
Unfortunately, designing systems as a collection of distributed state machines tends to become maintenance nightmare. Functions and data being separated tends to make code better, even when working in so-called 'OOP' languages.
> You can say its 'the real benefit', I guess, but that feels like circular reasoning. Its pretty much the definition of what OOP is, so calling it a benefit feels weird.
It's a benefit compared to how people were writing code before it became mainstream (for polymorphism: by jumping to data-defined parts of the code and hoping for the best, and for encapsulation: subroutines working on global variables).
Except inheritance is the premature optimisation of interfaces.
Inheritance forces you to define the world in terms of strict tree hierarchies, which is very easy to get wrong. You may even do a great job today, but tomorrow such properties don't hold anymore.
Regular composition allows the same functionality without making such strong assumptions on the data you are modelling.
> Inheritance forces you to define the world in terms of strict tree hierarchies,
No, it doesn’t.
Inheritance is the outcome of deciding to model some part of the problem space with a tree hierarchy (that potentially intersects other such heirarchies). It doesn’t force you to do anything.
I suppose if there was a methodology which forced you, as the only modeling method, to exclusively use single inheritance, that would force you to do what you describe, but…that’s not inheritance, that’s a bunch of weird things layered on top of it.
It may not force you directly, but I think it's safe to say that when a language focuses on inheritance (e.g. c++) it does not offer good alternatives (e.g. rust traits). This means that if you want features such as runtime polymorphism, you are _kinda_ forced into inheritance.
True. Modelling the world as a tree oversimplifies it, as there are also lateral and even backwards dependencies, and at a sufficiently complex level abstractions start to leak all over, making it a mess. But at some low to medium level of complexity it might work.
You’re describing the strategy pattern, which is probably one of the most practical coding design patterns. Ex: each chess AI difficulty gets its own class, which all extend a common interface.
But you don't actually care about runtime polymorphism here. You care about polymorphic behavior, which can be implemented in a much more composable way with parametric polymorphism.
You can’t build a dynamic list of objects implementing the same interface in different ways with parametric polymorphism.
As another example, the Unix file interface (open(), read(), write(), flush(), close()) etc. is an example of runtime polymorphism, where the file descriptors identify objects with different implementations (depending on whether it’s a regular file, a directory, a pipe, a symbolic link, a device, and so on).
All operating systems and many libraries tend to follow this pattern: You can create objects to which you receive a handle, and then you perform various operations on the objects by passing the respective handle to the respective operation, and under the hood different objects will map to different implementations of the operation.
Not without runtime polymorphism. Parametric polymorphism does not imply nor by itself implement runtime polymorphism. I.e. C++ templates, or generics in other languages, provide parametric polymorphism, but not runtime polymorphism.
Unfortunately a lot of developers are conditioned so heavily to believe that inheritance is intrinsically bad, that they contort their code into an unreadable mess just to re-implement something that could have been trivial to do with inheritance.
I'm not saying that I like it everywhere; IMHO it's a tool to be used sparingly and not with deep hierarchies. But it's not reasonable to avoid it 100% of the time because we're soiling ourselves over the thought of possibly encountering the diamond problem.
I feel like Clojure-style multimethods accomplish this better than inheritance. I can simply write a dispatcher that dispatches the correct function based on some kind of input.
This is evaluated at runtime, thus giving me the runtime polymorphism, but doesn't make me muck with any kind of taxonomy trees. I can also add my own methods that work with the dispatcher, without having to modify the original code. I don't feel like it's any less convenient than inheritance, and it can be a lot more flexible. That said, I suspect it performs worse, so pick your poison.
Sorry, but right off the bat this is just painting you as an example of "There are N camps, and each camp declares the other camp as wrong."
Yes, that's what inheritance is for you. For others it is something else. Why is your way the one that "correctly understands" it?
The article itself covers this - that some languages have lumped 3 different concepts into one that they call inheritance, leading to the different camps and comments like yours. Your camp is specifically mentioned:
> Abstract data type inheritance is about substitution: this thing behaves in all the ways that thing does and has this behaviour (this is the Liskov substitution principle)
I feel people don't understand what inheritance and (object orientation in general) is useful for, misuse it, and then it gets a bad reputation. It's not about making nice hierarchies of Cars, Fruits, and Ovals.
Agree 100%. It starts from the earliest programming course where we just teach it all wrong; way to abstract (no pun intended).
One point to add to yours: well executed OOP allows for "structural" flow of control where the object hierarchy, inheritance, events, and overrides allow for the "structure" of the hierarchy to control the flow of logic.
I wrote two articles on this with concrete examples and use cases:
I think the first part of the first article immediately introduces an anti-pattern - forcing the user to make an instance of a class just to be able to call a pure function. It's just unnecessary noise, either make them static methods, group them in an object literal, or import the module as a namespace.
Adding the "pattern" as a mutable public field is a bit sketchy, and would make it show up in intellisense, but hopefully nobody will access/modify it. Making the pattern as a `const` at module scope solves that problem but you handwave away that approach saying it "pollute the symbol space for intellisense", which isn't true (non-exported items aren't available outside the module).
The next section on validation is a good example of another anti-pattern. The example of:
public get isUserValid(): boolean
is not a good way of doing it, because it relies on hopes and prayers that the user of the class remembers to call this. A better signature would be:
function isUser(input: unknown): input is User
Notice the key difference - you can't get an instance of User without it being valid. There shouldn't be a notion of "yeah I have a User, but I don't know if it's a valid User". Your way allows people to operate with User, blissfully unaware of whether it is valid or not, hoping that they might notice the right method to call. Instead: Parse, Don't Validate [1]
Note that to do this correctly, you have to either:
1. Separate data and functions
2. If you must use a class, then hide the constructor and provide a static constructor with a return type that indicates the function can fail
Part 2 was an interesting exercise in how the popular OOP patterns from the GoF book vastly overcomplicated code, negatively impact readability, and make following the code feel like Mario (the princess is in another castle)
No need for inheritance, abstract base classes, of any of that complexity, all of that could be done with:
type ShippingCalculator = () => number
const shippingCalculators = {
USPS: calculateUSPS,
UPS: calculateUPS,
FedEx: calculateFedEx,
DHL: calculateDHL,
} as const satisfies Record<ShippingMethod, ShippingCalculator>
Intellisense works fine of course, and code navigation (via F12) is straightforward and easy to navigate.
> "yeah I have a User, but I don't know if it's a valid User".
This, imo, is one of the big reasons people so easily dismiss OOP. They put whatever data _they think they probably need_ in an object using setters/builders/what have you. This leads to abstractions of data that don't accurately reflect state. They will then let an external entity (service or whatever pattern) manipulate this data. At this point people might as well use something analogous to a C struct where anything can be done to the values. Objects are not managing their own invariants. When you rip this responsibility from an object, then nothing becomes responsible for the validity of an object. Due to this, people wonder why they get bugs and have trouble growing their software.
This also leads to things like "isValid". People don't understand that an object should be constructed with only valid data. The best example I've found of protecting variants and construction of valid objects to be this strategy in F#:
In psychology, there is an idea of "locus of control".
I think OOP done well and applied in suitable scenarios results in entities that have internal locus of control; it's mutations of state are internally managed.
OOP done poorly has external locus of control where some external "service" or "util" or "helper" manages the mutation of state.
I used to be an OO hater until I started playing with Smalltalk. In particular, I worked through the Pharo MOOC (https://mooc.pharo.org/), which teaches you exactly this: designing the hierarchy IS designing the control flow of the program.
That said, Smalltalky hierarchies are a nightmare in most other languages because another key part of the Smalltalk env is the tooling -- staying inside a running system and being able to edit code from within the debugger is absolutely great and keeps you in a flow state much better than any other workflow I've been exposed to (including Lisp + Paredit + SLIME). The result is that editing class hierarchies in blub-y OO languages is usually a massive pain in the ass, while doing it in Pharo or a similar Smalltalk env is fun and painless. This is why you can't practically write Smalltalkly in Python/C#/Java/etc. even if on paper they have all or almost all of the same features.
If people get it wrong so regularly, what value is it providing as a concept? These concepts are supposed to help us reach something better, if you have to add 30 caveats to every part of it, all it did was hide its own complexity from you, instead of managing it for you.
Because the tree is a nice abstraction for some problems. But sometimes you need a collection of pure functions. Sometimes it’s best to think of your object as data blobs going through a line of map,filter,reduce functions. Not every part of your application is the same, use the right abstraction for the job.
You can have polymorphism without inheritance and inheritance without polymorphism. The big problem with inheritance is that it is really tricky to write those base classes. Making class inheritable by default in programming language is often considered a big mistake, because only carefully designed and documented class can be safely extended.
I see that a lot - the alternative to inheritance, when inheritance does make sense, is code duplication, which is much worse than inheritance, or first-order functions, which many languages don't actually support or don't support efficiently.
> you have a function that takes a general type, and you can pass multiple specific types and it will do the right thing
What does this have to do with inheritance? This is just a generic function.
> And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them
This doesn't sound any different from how people usually talk about inheritance.
I am not convinced it gives you anything more than composition does. Composition is very easy to setup and easy to change. And there are certainly functional ways to do runtime polymorphism.
> > you have a function that takes a general type, and you can pass multiple specific types and it will do the right thing
>
> What does this have to do with inheritance? This is just a generic function.
It's a generic function with compile-time compatibility checks.
Inheritance is not so bad for native UI stuff where it makes sense for the class libraries.
But in work code I don’t see it much. If it is used it is light weight. Often used incorrectly for splitting code into different files rather than actual inheritance (giveaway is one one derived class!)
I only tend to see inheritance in engines and libraries, where it makes sense to create more generic, reusable and composable code, since most of the functionality in these is defined by technical people.
It makes no sense to use inheritance in the business layer, because a single feature request can make a lot of the carefully crafted abstractions obsolete.
I've never seen it put quite like this, but it feels right and is refreshingly concrete. Trust the abstractions you can actually design/control for, treat all the other ones as suspect. One still needs the wisdom to tell the difference, but at least focusing on "feature request" focuses the mind. This is at least simple even if it is not "easy".
An argument against OOP where you first need to define/explain differences between composition/inheritance, Liskov Sub, compare and contrast with traits, etc is not really that effective when trying to mentor junior folks. If they understood or were interested in such things then they probably wouldn't need such guidance.
388 comments
[ 2.0 ms ] story [ 347 ms ] threadthe same wisdom can be applied elsewhere as well, there is no need to keep discovering the same truths in different contexts again, and again.
I am sure that hundreds of thousands of students have at some point heard that these hierarchical organization of data is how you should model a system.
As a graphics programmer for many years I can also say that OOP is not really a good fit design wise for modern rendering engines, it mostly just gets in the way.
If it's based on components, wouldn't that mean you would think of it as being... _composed_? I typically don't hear "it's made of many components" and think of inheritance
Examples are usually something like having a base entity, a player that inherits from entity and an enemy that inherits from entity. Then you have a magic user and a barbarian inherit from enemy but now you also want your player to be able to use magic. Traditional OOP doesn't make it easy to share the implementation.
Even with composition and interfaces you still have problems with most traditional OOP languages when you want to do things like change the set of components of an entity dynamically at runtime (player gains or loses the ability to use magic during the game).
"Is a" is often not the relationship you want to model. A player and an NPC both have the "has a" relationship to an inventory, not the "is a" for example.
I've been doing a lot with LeafletJS lately, and it has some light inheritance around its drawing primitives (things like boxes, circles and lines you draw on top of maps). It works well.
For 3D engines you still need to be quite careful with where you apply it though. It's possible to get into a huge mess if you use it too much.
The only problem is if you're bad at using the hammer, and you only know how to use a hammer.
The trick though is you actually have to use modern Java, which means you need to both be on the right version of Java, and have developers that understand the value/power of these newer constructs. Which is surprisingly rare for a programmer that self-identifiers as a Java programmer.
if you have a person class and you have students and teachers
the correct thing to to is NOT to make student and teacher inherit from person
it's to make student and teacher have a person attribute + the other attributes that constitute a student and teacher respectively
Any big enough system will have parts that are most sensibly built object-oriented, and other parts that are more reasonably functional, plus anything else you can think of.
At one point "Object Oriented" became "Blockchain" (or now Gen AI) of those times. You had to be "object oriented" in order to be taken seriously. This applied to everything. Even finished software products were called "built using object oriented".
The esoteric concept of inheritance became popular after that. At some point it became so popular that people figured out that it is not really a good thing.
Causing engineers to completely change their mental model of how the code runs, which I still have intuitive trouble imagining correctly and I see it with other developers as well. A lot of energy goes into trying to understand how to do a simple action. It is much harder to read the code and have a correct mental model as well.
I feel like I spend more time and energy on how to get something done functionally rather than focusing on what the correct business logic should be.
not being familiar with fp doesn't mean it's objectively worse
Because at some point mainstream OO languages made inheritance easy and composition harder, nothing more.
Composition with Java used to be verbose. Inheritance declaration was simply a single keyword.
If Java had "mixins" from the start, people would have used composition way more.
FP in the industry is a niche thing, so the two phenomena are not really comparable.
The core reason everyone uses it seems to me to be essentially the same reason the former groups hate it - it’s what everyone else does, it’s an incredibly square and “day-job”-esque approach to one’s tooling and architecture, it often reeks of bureaucracy, compartmentalization, and organizational superstructures often unrelated to the technical work, dictated primarily by managerial fiefdoms and needs to organizationally coordinate, and it’s a well-defined space with extremely predictable solutions and headaches, and therefore has very little excitement or novelty to offer.
After a while you get to closures, which are pretty much objects without classes, and then factory functions that produce closures and there you have something like a class as well. If I were to design a course I'd probably follow this flow to get to 'OOP' in that sense.
I prefer to say that we naturally classify objects around us using fuzzy boundaries like "a house", "a cat", which don't exactly mean anything: they are templates we use later to actually generate an actual house, or an actual cat (on a piece of paper as a drawing for instance).
The world being separated between our internal classes and the interactive instances of them, we can code that way too: we look for what we actually need in a concept, name it and define only what we need, then instantiate several concrete actors interacting together. Each actor is complete, well defined, with contracts for interactions.
You can then start building on top of this more complex systems, talking in English and defining rationally the world of your little model.
If you start talking about functions, you're basically aliasing a memory address for a bunch of code: you'll goto that function address, with some parameters at some other address, do a bunch of things and put the result in another address for the next function to process. You're building at best a suite of pipelines, which ends up being a little bit too technical and static for my taste.
Another way to defend modelling a software with classes and object, in my view, without trying to talk about functions, is the blank page effect: imagine arriving at random at some assignment. You understand vaguely the business problem, now you need to code a solution. You have 6 months, will generate a few millions a year and will require a team of 10 people eventually to test, maintain, operate and debug: you start with defining functions generating functions with callback parameters in Python, or you launch IntelliJ and you do a Java class model ? I'd be terrified modelling complex problems with just functional pipelines, I think it's just way more obvious to talk about objects at all time if you're gonna do something that is not just processing inputs into well defined outputs.
'Here is a way to do simple math, here is a way to glue text to text, now that has grown a bit, you can shorten it for repetitions by giving it a name like this', and so on.
And to me, starting out functionally is easy. Data is pretty much always a given, it's very rare in practice that I first need to model speculatively and then generate data ex nihilo unless I'm creating mocks. Usually there is a data source, a file, a network API, whatever, and then I start building against that. Small, simple things at first, like mapping all the input to an output interface, then add transformations and output for these, and so on.
In general I spend more time thinking about the shape of data I already have than architecting code or inventing models.
In Smalltalks as I know them, which is sporadically and shallowly, inheritance seems to be used as a factoring tool, when you break down functions into smaller functions there's a structure in which to place them with the implicit incentive to put them as high up as possible.
To me it seems to work pretty well, but it's also a rather weird environment where even classes themselves are objects.
In Java one can get around inheritance by using injection into an attribute in a wrapper class that extends or changes an object API. This leads to a similar decoupling as more functional composition, but you pay by having more objects swimming around in the VM during execution. Extending on the class level (i.e. in declarations of classes, interfaces, traits and the like) instead of the object level might have effects on performance and resource management.
Usually that likely doesn't matter, but it might. In Java-like languages I tend to use inheritance as a quick way to abstract over or extend functionality in a library class, I get the public methods 'for free' in my own class without having to write the wrappers myself. And that's usually where I stop, one level of inheritance, since Java isn't as inspectable and moldable as, say, Pharo, and doesn't give the same runtime ergonomics with large class/object trees.
Let's stay I have a base class A. Let's say I have 70 classes that inherit from A.
These classes must serialize/deserialize from disk.
My choice here is that I can add a new data member to A and then update the read/write method in one place, or I add a new data member to all the classes that are derived from A, meaning that I get to update 70 classes. Seriously? Who would think this is a good idea? You'll have to write 70 times as much code... there will be times when that's definitely a very bad idea.
Maybe don't use inheritance. Then give every class its own "Name" data member like std::string c_nName. Every class can have its own function to set the name, or we could use an unencapsulated free function and do things like C. Except then the experts will say things like "well, now you're doing C with classes". Then you get a ticket that the user can enter bad names. You can then figure out some way to validate the names, right? That could be a free function, or maybe write some class called NameValidator. Except now the experts say you're writing C++ like Java. Too many classes, too few classes, too many objects, too few objects, too may free functions, too few free functions.
C++ expert of the month may say X, Y, Z, but look at Microsoft's APIs.
Inheritance is everywhere.
Look at any open source C++ project.
Inheritance is everywhere.
Does that mean that you should make something like:
A
--B : public A
----C : public B
------D : public C
--------E : public D
----------F : public E
Or this:
Not unless you have a damn good reason. But the point is that inheritance is a valid tool and it can reduce bugs, code duplication, maintenance.Right tool for the job, yeah? I once read something in the C++ FAQ that said something like "Don't take advice from people who don't understand your business problems."
The alternative case is that you have 70 classes that CONTAIN A. You still only have to change A.
Inheritance isn't necessarily bad.
If anything is common, someone will try to seem like a brilliant iconoclast by writing an essay against it. The particular essay in this case was from a Pythonista (not surprising, given Python's weird love/hate with objects):
https://solovyov.net/blog/2020/inheritance/
And not everyone uses inheritance.
Maybe you have another question, like "why would someone use inheritance?"
Inheritance is like a lot specialised tools - it can be useful in some situations but I think those are rarer than supporters might thing. Completely refusing to use inheritance and always using inheritance both seem like extreme views that should be avoided.
I guess one could say this would be workable with proper tools, but the IDEs just aren't there. Move up a level in inheritance, ctrl-click on a call? It was overriden somewhere in the hierarchy, but the IDE will send you to the parent-class definition regardless.
More than once, I have found an inheritance hierarchy that made sense when first created no longer models the problem well. It becomes hard to change it at that point.
Frequently I find I really want to mix in cross cutting concerns across different hierarchies. This isn't really a surprise; most problems do not naturally decompose to only a single view of things.
I don't mind abstract base classes containing common functionality, so it's useful there, but mix ins would be better to be honest.
When inheritance is your only tool for interface, I absolutely agree that it looks fundamental, but the fundamentalness is coming from the interface side. Once you separate out interface from implementation, it turns out I have little to no use for the implementation portion.
I have also been programming in Go for a long time now. I sketched out an inheritance system once for an exercise. It's doable, though it's a terrible pain in the ass to use. I've kept it in my back pocket in case it is ever the right solution to a problem even so... but it never has been. And that's not because of any irrational hate for the pattern itself. I've imported other foreign patterns sometimes; I've got a few sum types, even though Go isn't very good at it, because it was still the best solution, and I've got a couple of bits of code that are basically dynamically typed, again because it was the best solution, so I'm not against importing a foreign paradigm if it is the correct local solution. Inheritance just... isn't that useful. The other tools that are available are sufficient, and not just begrudgingly sufficient or "I'm insisting they're sufficient to win the argument even though I know they really aren't"... they really are sufficient. Functions are powerful things, as the functional programming community (it's right there in the name) well knows.
If you have to model your data exactly once I believe it can work. But if you are doing any kind of agile, or even somewhat flexible waterfall, you will find out that at some point none of your data models work.
I think the point of the article is that in some (very popular) languages, inheritance comes with a lot of baggage - precisely because classes in those languages support 3 different use cases. It's not saying that your usage is bad, but that your language didn't provide you with the best tool(s).
A lot of how people use classes can be solved via ADTs - which are much simpler to grok.
A lot of how people use classes can be solved via namespaces/modules, but not all languages have good support for them - so they use classes.
Etc.
The net result of this is that inheritance is rarely needed/used in Kotlin. It's there if you really need it but usually there are better ways.
Scala and other languages of course have similar/more powerful constructs. But Kotlin is a nice upgrade over Java's everything is non final by default (or generally defaulting to the wrong thing in almost any situation). Go has a nice pattern based on duck typing where instead of declaring interfaces, you can just treat something that implements all the right functions as if it implements a type. Rust similarly has some nice mechanisms here. All these are post-Java languages that de-emphasize inheritance.
I've lost count of how many talks I've watched by Kate Gregory where she advocates for people tagging everything they can as const in their C++, but asking people to eat their veggies never works.
The worst example of inheritance I've ever found is in java Minecraft's mobs[0]. It is very deep in some places and has numerous examples of sub classes not fully implementing their parent interfaces.
Example 1: Donkey[1]
Donkey > Chested_Horse > Abstract Horse > Animal > Ageable Mob > Pathfinder Mob > Mob > Living Entity > Entity
Example 2: Blaze[2]
Blaze has a bit for 'Is on fire', but how is this any different from Mob 'Is aggressive'? Blaze > Monster > Pathfinder Mob > Mob > Living Entity > Entity
[0] https://wiki.vg/Entity_metadata#Entity
[1] https://wiki.vg/Entity_metadata#Donkey
[2] https://wiki.vg/Entity_metadata#Blaze
https://en.wikipedia.org/wiki/Entity_component_system
Sometimes inheritance is extremely useful. Sometimes it's harmful. Sometimes it's not the best but the most practical nontheless.
Language concepts are tools. These people with extreme black and white views live under the delusion that there can be some "Perfect Language which will cause the program to never develove/get technical debt TM".
Pure delusion.
Real life is an approximation where you do what is best in practice, not in some delusional daydream of perfection which breaks the moment you try and bring it to reality.
For me the main point is (runtime) polymorphism. E.g. you have a function that takes a general type, and you can pass multiple specific types and it will do the right thing. And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them.
You can get this without implementation inheritance, it is also possible to just have something like interfaces. But I do find it very convenient to put common code in a base class.
Not without casting. qsort is still:
Inheritance is what will bite hard when hand-rolling OOP in C. For one, you can forget about the compiler enforcing substitutability and co/contravariance for you.
I think that's the ultimate culprit in everyone hating inheritance. If it weren't for Java, I think we'd all have a healthier view of OO in general.
I learned OO with C++ (pre C++-11), and now I work at a Java shop, but I'm luck that I get to write R&D code in whatever I need to, and I spend most of my time in Python.
In C++ and Python, you get to pick the best tool for the job. If I just need a simple function, I use it. If I need run-time polymorphism, I can use it. If I need duck-typing I can do it (in Python).
Without the need for strict rules (always/never do inheritance) I can pick what makes the best (fastest? most readable? most maintainable? most extensible? - It depends on context) code for the job.
Related to TFA, I rarely use inheritance because it doesn't make sense (unless you shoehorn it in like everyone in the threat is complaining about). But in the cases where it really does work (there really is a "is a" relation), then it does make life easier and it is the right thing.
Context matters, and human judgement and experience is often better than rules.
Everything is an object so there's no if-it's-an-object do this but if-it's-not-an-object do that.
Those first two you can do in (modern) Java. The third is a mess to be avoided at all costs. Interfaces and lambdas will cover most reasonable use cases for polymorphism.
https://www.cs.virginia.edu/~evans/cs655/readings/smalltalk....
Dynamic dispatch is implemented under the hood with lookup tables and function pointers. Sometimes, it is nice for a language to wrap a fiddly thing in a more abstract structure to make it easier to read, understand, and write.
The runtime part is what I dislike. If I have a fruit which is an apple or a banana, I can't pass that to a method expecting an apple or banana. It can only be passed as a fruit.
> And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them.
This is common OO wisdom that I strongly disagree with. For example, in my program I have a few types (Application, Abstraction, Variable, etc.), and a lot of transformations to perform on those types (TypeCheck, AnfConvert, ClosureConvert, LambdaLift, etc.).
I prefer to have all the type-checking code inside the TypeCheck module, and all the closure-converting code inside the ClosureConvert module. I'd take the "huge if-else" statements inside TypeCheck rather than scatter typechecking across all my datatypes.
You can by overriding the method on apple or banana. If your method is on some other object, then yes, you cannot do this unless your programming language supports multiple dispatch.
There are ways to have your cake and eat it too, here, at least in some languages and patterns.
For example, in Go you could define "CheckType" as part of the interface contract, but group all implementors' versions of that method in the same file, calling out to nearby private helper functions for common logic.
Ruby's open classes and Rust's multiple "impl" blocks can also achieve similar behavior.
And yeah, sure, some folks will respond with "but go isn't OO", but that's silly. Modelling polymorphic behavior across different data-bearing objects which can be addressed as implementations of the same abstract type quacks, very loudly, like a duck in this case.
It's less "this is not OO" and more this is not inheritance, which is why a lot of people are saying you can find more elegant solutions (like this) rather than use inheritance for no clear benefit.
Heh? An apple is a fruit, you can pass it to any place expecting the former. Like, this is Liskov’s substitution’s one half.
With generics, you can be even more specific (co/in/contra-variance).
If OP wanted to be allowed to do whatever and just have the software fail at runtime, JS is right there.
I would never. I know that I'm holding a banana, I want to pass it to a method that receives a banana. But what I can't do is put my banana through a rotateFruit function first, because then Java will forget that it's a banana and start treating it as a fruit.
It's pretty much the definition of OOP.
The core feature of OOP is just bundling functions with the state it processes.
When you bundle state and functions together, you can't predict what calling the function will do without knowing both the code and state.
You can say its 'the real benefit', I guess, but that feels like circular reasoning. Its pretty much the definition of what OOP is, so calling it a benefit feels weird.
Unfortunately, designing systems as a collection of distributed state machines tends to become maintenance nightmare. Functions and data being separated tends to make code better, even when working in so-called 'OOP' languages.
It's a benefit compared to how people were writing code before it became mainstream (for polymorphism: by jumping to data-defined parts of the code and hoping for the best, and for encapsulation: subroutines working on global variables).
Inheritance forces you to define the world in terms of strict tree hierarchies, which is very easy to get wrong. You may even do a great job today, but tomorrow such properties don't hold anymore.
Regular composition allows the same functionality without making such strong assumptions on the data you are modelling.
No, it doesn’t.
Inheritance is the outcome of deciding to model some part of the problem space with a tree hierarchy (that potentially intersects other such heirarchies). It doesn’t force you to do anything.
I suppose if there was a methodology which forced you, as the only modeling method, to exclusively use single inheritance, that would force you to do what you describe, but…that’s not inheritance, that’s a bunch of weird things layered on top of it.
But you don't actually care about runtime polymorphism here. You care about polymorphic behavior, which can be implemented in a much more composable way with parametric polymorphism.
As another example, the Unix file interface (open(), read(), write(), flush(), close()) etc. is an example of runtime polymorphism, where the file descriptors identify objects with different implementations (depending on whether it’s a regular file, a directory, a pipe, a symbolic link, a device, and so on).
All operating systems and many libraries tend to follow this pattern: You can create objects to which you receive a handle, and then you perform various operations on the objects by passing the respective handle to the respective operation, and under the hood different objects will map to different implementations of the operation.
Yes you can. that's the whole point of type classes.
For example in c++ std::function exhibits parametric polymorphism without templates.
I'm not saying that I like it everywhere; IMHO it's a tool to be used sparingly and not with deep hierarchies. But it's not reasonable to avoid it 100% of the time because we're soiling ourselves over the thought of possibly encountering the diamond problem.
This is evaluated at runtime, thus giving me the runtime polymorphism, but doesn't make me muck with any kind of taxonomy trees. I can also add my own methods that work with the dispatcher, without having to modify the original code. I don't feel like it's any less convenient than inheritance, and it can be a lot more flexible. That said, I suspect it performs worse, so pick your poison.
> For me ...
Sorry, but right off the bat this is just painting you as an example of "There are N camps, and each camp declares the other camp as wrong."
Yes, that's what inheritance is for you. For others it is something else. Why is your way the one that "correctly understands" it?
The article itself covers this - that some languages have lumped 3 different concepts into one that they call inheritance, leading to the different camps and comments like yours. Your camp is specifically mentioned:
> Abstract data type inheritance is about substitution: this thing behaves in all the ways that thing does and has this behaviour (this is the Liskov substitution principle)
One point to add to yours: well executed OOP allows for "structural" flow of control where the object hierarchy, inheritance, events, and overrides allow for the "structure" of the hierarchy to control the flow of logic.
I wrote two articles on this with concrete examples and use cases:
https://medium.com/codex/structural-control-flow-with-object...
https://medium.com/@chrlschn/weve-been-teaching-object-orien...
I think the first part of the first article immediately introduces an anti-pattern - forcing the user to make an instance of a class just to be able to call a pure function. It's just unnecessary noise, either make them static methods, group them in an object literal, or import the module as a namespace.
Adding the "pattern" as a mutable public field is a bit sketchy, and would make it show up in intellisense, but hopefully nobody will access/modify it. Making the pattern as a `const` at module scope solves that problem but you handwave away that approach saying it "pollute the symbol space for intellisense", which isn't true (non-exported items aren't available outside the module).
The next section on validation is a good example of another anti-pattern. The example of:
is not a good way of doing it, because it relies on hopes and prayers that the user of the class remembers to call this. A better signature would be: Notice the key difference - you can't get an instance of User without it being valid. There shouldn't be a notion of "yeah I have a User, but I don't know if it's a valid User". Your way allows people to operate with User, blissfully unaware of whether it is valid or not, hoping that they might notice the right method to call. Instead: Parse, Don't Validate [1]Note that to do this correctly, you have to either:
1. Separate data and functions 2. If you must use a class, then hide the constructor and provide a static constructor with a return type that indicates the function can fail
[1] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
_________
Part 2 was an interesting exercise in how the popular OOP patterns from the GoF book vastly overcomplicated code, negatively impact readability, and make following the code feel like Mario (the princess is in another castle)
No need for inheritance, abstract base classes, of any of that complexity, all of that could be done with:
Intellisense works fine of course, and code navigation (via F12) is straightforward and easy to navigate.This, imo, is one of the big reasons people so easily dismiss OOP. They put whatever data _they think they probably need_ in an object using setters/builders/what have you. This leads to abstractions of data that don't accurately reflect state. They will then let an external entity (service or whatever pattern) manipulate this data. At this point people might as well use something analogous to a C struct where anything can be done to the values. Objects are not managing their own invariants. When you rip this responsibility from an object, then nothing becomes responsible for the validity of an object. Due to this, people wonder why they get bugs and have trouble growing their software.
This also leads to things like "isValid". People don't understand that an object should be constructed with only valid data. The best example I've found of protecting variants and construction of valid objects to be this strategy in F#:
https://fsharpforfunandprofit.com/series/designing-with-type...
I'm yet to find a good way to do this in a language such as Java unfortunately.
I think OOP done well and applied in suitable scenarios results in entities that have internal locus of control; it's mutations of state are internally managed.
OOP done poorly has external locus of control where some external "service" or "util" or "helper" manages the mutation of state.
That said, Smalltalky hierarchies are a nightmare in most other languages because another key part of the Smalltalk env is the tooling -- staying inside a running system and being able to edit code from within the debugger is absolutely great and keeps you in a flow state much better than any other workflow I've been exposed to (including Lisp + Paredit + SLIME). The result is that editing class hierarchies in blub-y OO languages is usually a massive pain in the ass, while doing it in Pharo or a similar Smalltalk env is fun and painless. This is why you can't practically write Smalltalkly in Python/C#/Java/etc. even if on paper they have all or almost all of the same features.
What does this have to do with inheritance? This is just a generic function.
> And if you want to avoid huge if-else statements, you should put the code for the special cases in the classes, not in each function that operates on them
This doesn't sound any different from how people usually talk about inheritance.
I am not convinced it gives you anything more than composition does. Composition is very easy to setup and easy to change. And there are certainly functional ways to do runtime polymorphism.
It's a generic function with compile-time compatibility checks.
But in work code I don’t see it much. If it is used it is light weight. Often used incorrectly for splitting code into different files rather than actual inheritance (giveaway is one one derived class!)
It makes no sense to use inheritance in the business layer, because a single feature request can make a lot of the carefully crafted abstractions obsolete.
An argument against OOP where you first need to define/explain differences between composition/inheritance, Liskov Sub, compare and contrast with traits, etc is not really that effective when trying to mentor junior folks. If they understood or were interested in such things then they probably wouldn't need such guidance.