Inheritance, which allows classes to reuse state and methods of other classes.
This is the absolute worst feature of typical OOP languages. I don’t know of any case where it is the best way to solve a problem and very often becomes a nightmare if you don’t get the exact hierarchy of types right. It becomes a nightmare once you have something that does not quite fit into the assumptions you original made when you started out. Which happens all the time.
The examples given with the logger can be solved just as well if not better with interfaces/traits with solutions that don’t have that problem.
I’ve seen some surprisingly fragile OOP solutions that require tons of internal knowledge about how the classes work. It seems to be a popular approach to writing code that just isn’t very flexible.
It requires you model your problem perfectly from the start, then it can work alright. But in reality you cannot know the future and when new requirements come in that don’t fit the model you created you are left in a crappy spot of needing to refactor everything or just cram it in however you can.
Note that I am explicitly calling out inheritance here rather than OOP as a whole. There are many things about OOP that are not that bad or quite ok, like composition for instance. It is generally badly designed inheritance that leads to
require tons of internal knowledge about how the classes work
And it is very hard to create a good inheritance structure that does not devolve over time as new requirements get added. While there are other patterns that OOP languages have started to adopt in more recent years (like composition and interfaces) that solve a lot of the same problems but in a vastly more maintainable way.
is very hard to create a good inheritance structure that does not devolve over time as new requirements get added
That’s such an important point. Whatever else folks take from this thread, I hope they catch that.
And I’ll pile on to add - more layers is more risk. One layer of inheritance is a lot easier to keep maintaining than inheritance that goes four layers deep.
I do not agree. Very often, when using libraries for example, you need some extra custom handling on types and data. So the easy way is to inherit and extend to a custom type while keeping the original functionality intact. The alternative is to place the new functionality in some unrelated place or create non-obvious related methods somewhere else. Which makes everything unnecessary complex.
And I think the trait system (in Rust for example) creates so much duplicate or boilerplate code. And in Rust this is then solved by an even more complex macro system. But my Rust knowledge might just nog be mature enough, feel free to correct me if I’m wrong…
So the easy way is to inherit and extend to a custom type while keeping the original functionality intact.
You can do this with traits and interfaces in rust/go. You can add any methods you want onto existing types. Which IMO is better. No need to subclass, in just just create a new trait, implement it on the type you want and you have new behavior attached to that type without needing to convert the existing thing you got from something into a new type.
And I think the trait system (in Rust for example) creates so much duplicate or boilerplate code.
It really does not. You can have implementation on traits that don’t need to be re-implemented on every type - like the Iterator - it provides 76 methods of which you need to implement only 1 for new types. You can implement others for custom behavior which is great for specialization (aka using a more efficient implementation for types that have more info, like calling skip on an array which knows all its elements vs the default which needs to call next n times).
But it creates a vastly more flexible system. Take a very basic example - read/writing to something. How do you model that with inheritance? Basically you cannot. Not without painting yourself into a corner eventually. For instance, you can read/write to a file, to a network socket, to stdin/stdout but each of these is very different. Stdin for instance cannot be written to and Stdout cannot be read from. You might want to have a buffered reader/writer as well that wraps these types making read operation cheaper.
You cannot put these into a inheritance tree. Everything either needs to inherit from the same generic base that can both read/write and probably also close. But then for some types you need to implement these methods that don’t make sense that do what? Nothing when called? or throw an exception? It is a poor way to model this behavior.
Read and Write are orthogonal ideas - they have nothing to do with each other except they might be useful on some of the same types. With interfaces/traits you are free to separate these and implement them on whichever types make sense for them.
I have not yet seen a problem that is solvable with inheritance that cannot be better solved with some other language feature in a better way. It sort of works for some things, but other solutions also work at least equally well. Which leave it in a state where what is the point of it? If it is not solving things better then other solutions we have these days?
Composition is far better and immensely more flexible than inheritance. Extracting duplicate code into helper classes or static functions is a good option.
Conformance to interfaces or protocols with default implementations is a great alternative as well.
I like OOP more than other styles, it’s just often badly done. Complex inheritance, huge classes that do too much, overuse of factories and similar patterns, can ruin it.
This is the absolute worst feature of typical OOP languages. I don’t know of any case where it is the best way to solve a problem and very often becomes a nightmare if you don’t get the exact hierarchy of types right. It becomes a nightmare once you have something that does not quite fit into the assumptions you original made when you started out. Which happens all the time.
The examples given with the logger can be solved just as well if not better with interfaces/traits with solutions that don’t have that problem.
I’ve seen some surprisingly fragile OOP solutions that require tons of internal knowledge about how the classes work. It seems to be a popular approach to writing code that just isn’t very flexible.
It requires you model your problem perfectly from the start, then it can work alright. But in reality you cannot know the future and when new requirements come in that don’t fit the model you created you are left in a crappy spot of needing to refactor everything or just cram it in however you can.
I recently did some refactoring with injector and composition patterns already there and it was a breeze.
OOP isn’t bad but like anything it requires some care.
Note that I am explicitly calling out inheritance here rather than OOP as a whole. There are many things about OOP that are not that bad or quite ok, like composition for instance. It is generally badly designed inheritance that leads to
And it is very hard to create a good inheritance structure that does not devolve over time as new requirements get added. While there are other patterns that OOP languages have started to adopt in more recent years (like composition and interfaces) that solve a lot of the same problems but in a vastly more maintainable way.
That’s such an important point. Whatever else folks take from this thread, I hope they catch that.
And I’ll pile on to add - more layers is more risk. One layer of inheritance is a lot easier to keep maintaining than inheritance that goes four layers deep.
And if you only have one layer then why not just use interfaces/traits? Which are a vastly better design than inheritance.
I do not agree. Very often, when using libraries for example, you need some extra custom handling on types and data. So the easy way is to inherit and extend to a custom type while keeping the original functionality intact. The alternative is to place the new functionality in some unrelated place or create non-obvious related methods somewhere else. Which makes everything unnecessary complex.
And I think the trait system (in Rust for example) creates so much duplicate or boilerplate code. And in Rust this is then solved by an even more complex macro system. But my Rust knowledge might just nog be mature enough, feel free to correct me if I’m wrong…
You can do this with traits and interfaces in rust/go. You can add any methods you want onto existing types. Which IMO is better. No need to subclass, in just just create a new trait, implement it on the type you want and you have new behavior attached to that type without needing to convert the existing thing you got from something into a new type.
It really does not. You can have implementation on traits that don’t need to be re-implemented on every type - like the Iterator - it provides 76 methods of which you need to implement only 1 for new types. You can implement others for custom behavior which is great for specialization (aka using a more efficient implementation for types that have more info, like calling
skip
on an array which knows all its elements vs the default which needs to call next n times).But it creates a vastly more flexible system. Take a very basic example - read/writing to something. How do you model that with inheritance? Basically you cannot. Not without painting yourself into a corner eventually. For instance, you can read/write to a file, to a network socket, to stdin/stdout but each of these is very different. Stdin for instance cannot be written to and Stdout cannot be read from. You might want to have a buffered reader/writer as well that wraps these types making read operation cheaper.
You cannot put these into a inheritance tree. Everything either needs to inherit from the same generic base that can both read/write and probably also close. But then for some types you need to implement these methods that don’t make sense that do what? Nothing when called? or throw an exception? It is a poor way to model this behavior.
Read and Write are orthogonal ideas - they have nothing to do with each other except they might be useful on some of the same types. With interfaces/traits you are free to separate these and implement them on whichever types make sense for them.
I have not yet seen a problem that is solvable with inheritance that cannot be better solved with some other language feature in a better way. It sort of works for some things, but other solutions also work at least equally well. Which leave it in a state where what is the point of it? If it is not solving things better then other solutions we have these days?
Yeah inheritance isn’t always the best solution. But even Java, the much maligned example for this, doesn’t do it for the i/o example you give.
Composition is far better and immensely more flexible than inheritance. Extracting duplicate code into helper classes or static functions is a good option.
Conformance to interfaces or protocols with default implementations is a great alternative as well.
I like OOP more than other styles, it’s just often badly done. Complex inheritance, huge classes that do too much, overuse of factories and similar patterns, can ruin it.