One very important issue in OOP is packaging together variable names. You can see the issue by looking at this atrocious function signature from the Python Pandas library:
An OOP approach would define a Reader object that has many methods supporting various configuration options (setSkipRows(..), setNaFilter(...), etc), perhaps using a fluent style. Finally you call a read() method that returns the DataFrame.
The author brings up the basically the canonical example of where OOP style design shines, and where functional programming will falter.
The simple truth however is that overly going into either functional or OOP camp will hurt because strict adherence becomes subscribing to a silver-bullet.
The middle road is simply a better engineering option, use a practical language that supports both paradigms.
Keep data transforms and algorithmic calculations in functional style because those tend to become hot messes if you rely overly on mutation (even if there is performance gains, correctness is far far easier to get right and write tests for with a functional approach), then there are other concerns where an OOP derived system with inheritance abstractions will make things easier.
This article describes the OOP approach of changing, say, a Logger to an abstract class with a default implementation, without needing to modify code that uses it, and also describes this as something that could be done in C++. But this doesn’t work at all in C++ if there is ever a Logger on the stack!
Logger l;
That’s a Logger, and it will not magically become a subclass.
And changing this to go through pointers everywhere and to use virtual functions, in C++, is not very performant. A good JIT compiler may be able to effectively devirtualize it, but C++ compilers are unlikely to be able to do this effectively.
In moderation, yes. The problem with OOP, like all other paradigms that came before, is applying it dogmatically and excessively, which always happens when there's a lot of hype, novelty, and $$$ to be made.
I agree but "moderation" isn't the best word. OOP is good when used appropriately and not when you fixate that everything has to be within the OOP structure.
It's the same thing with languages, IMO. People try to shoehorn tasks into a language when it would be better off in another language.
> Subtyping, where if a type B implements the public interface of type A, values of type B can be passed as A.
I am confused by this statement or it is going against what I understand.
If you have created a Type B variable, and you also have a new interface called A, and B implements A, then why would Type B variable's values be passed to A's values. 'A' is only an interface.
I noticed there's an entire paragraph explaining what OOP is, but it might be helpful to clarify that OOP stands for Object-Oriented Programming. Even though it's a well-known acronym, adding that explanation could benefit readers who are new to the concept.
I changed my mine on this. I did OOP since the mid-1990s when I learned it in high school up until about 10 years ago. I find OOP works best when you have a single coder who can store the model of the system in this mind and work out how to design the base and abstract classes well. And they also have freedom to refactor THE WHOLE CODEBASE when they get it wrong (probably multiple times.) Then you can make these webs of elegant ontologies work.
But in real life, when there is a team, you run into the fragile base class [1] constantly and changing that base class causes horrible issues across your code base.
I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies.
I agree with encapsulation and modularity and well defined interfaces (typed function signatures are amazing.) I just completely disagree with inheritance in all forms.
There are no benefits to it (besides feeling smart because you've made an elegant but ultimately brittle ontology of objects and methods), just a ton of downsides.
I totally agree that class hierarchies are mostly a trap, especially when there's more than one developer on a project.
What I don't understand is why many programmers still use classes even when they plan on avoiding inheritance. I've seen this sort of thing on the past few major projects I've been involved with; initial creators heavily relied on inheritance, future developers realize that was a mistake, then said future developers continue using the class construct of whatever given language.
At that point, why not just use functions and regular objects? Without inheritance, classes tend to have these other rules and complications that don't seem to really add anything when there is no hierarchy.
>I have found that OOP with inheritance is actually a form of tight coupling
You are not the only one. Prefer composition to inheritance is a saying for a reason. Inheritance is powerful and useful for small problems, but as you say it introduces tight coupling and so should only be used where that is intended. Tight coupling isn't always bad, but it is bad often enough to avoid it.
> I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies.
Tight coupling is the basis of oop. A method is tied to an instance. Methods cannot be composed with other methods or functions without instantiating state. Methods cannot be moved into other scope.
The literal definition of an object is a tightly bound set of methods that cannot ever be used without instantiating state.
Coupling behaviors to types is not a problem. Class inheritance and multi-inheritance with their weird taxonomical trees are the problem.
There's little practical reason to build a family tree of supertypes and subtypes outside of building GUIs, yet this is how class-based OOP is designed and taught.
Trait-based OOP gets this right. The hierarchy is completely flat. You simply implement the behaviors for the types you want and don't have to think about grandparent behavior and final interfaces.
I don't think a method being tied to an instance is the best case for calling it "tight coupling".
OOP can be used to design relatively uncoupled systems.
One of the lessons learned in all these decades is what the grandparent post alludes to, which is distilled into "prefer composition over inheritance". Implementation inheritance (as opposed to interface inheritance) indeed introduces coupling and is therefore discouraged in current advice.
> OOP can be used to design relatively uncoupled systems.
Never go full-Object-oriented programming.
In this case I think it is valuable to make a distinction between OOP which is a style of programming, and object-oriented languages, which are just a language designed with that style in mind.
I have seen issues in codebases where developers have used OOP as style to aspire to, using it in an academic sense. They tend to try to use inheritance frequently, have deep inheritance trees, and suffer from hidden coupling through these.
On the other hand, those who use object oriented languages in a mostly functional style (side-effect free functions, effective immutability, and almost no use of inheritance) tend to be much healthier in the long term.
So it's fine to use OO languages, but never go full OO programming.
Exactly my thoughts. When I started really getting functional programming it was a breath of fresh air. For me the test is not just how fast someone else can pick up the codebase but how fast I can remember how it all works in n years.
> I find OOP works best when you have a single coder who can store the model of the system in this mind and work how out to design the base and abstract classes well.
When all the states of the program and all the transitions between them fit into one programmer's head - every programming paradigm works well.
My thoughts as well. Inheritance forces you into spaghettification sooner or later if you didn't consider everything you'll ever need from the start to the end of the universe. Even if you're working on it by yourself the refactors become frequent and take too much time for little gain.
Self contained encapsulated parts with an eventbus for entirely decoupled data propagation are much easier to manage and edit. Just duplicate a bit more than you think would be best to reuse, disk space is cheap and if you get that part wrong you're screwed and will need another rewrite.
And it's probably more performant too with newer hardware since you can usually just spin out any self contained part as its own thread if need be, often with almost zero changes.
>I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies [..] I just completely disagree with inheritance in all forms
This is pretty common advice (at least IME), usually distilled into the form "prefer composition to inheritance".
> This is pretty common advice (at least IME), usually distilled into the form "prefer composition to inheritance".
I think this is why in the current JavaScript era, it is rare to find a popular library that makes significant use of inheritance (as opposed to just supporting interfaces) even though it is supported in the language.
ThreeJS is one of those exceptions, but they are few and far between.
Similar thinking, I've been through much pain from a class hierarchy degrading into an unusable mess, also done a lot of rework refactoring those and it doesn't pay off.
Instead of inheritance I much rather prefer the composition approach, not extending classes but defining interfaces for the API, using other classes in the composition of an object rather than relying on overrides/implementations from a base class. It's much clearer to reason about (no more 3-4 layers of indirection), easier to refactor as well.
The GoF book did a lot of damage. I've finally read it and was amazed that the entire book is in fact about writing GUIs, which is just one tiny part of programming.
Several patterns are trivial, others are very similar and are just a linked list of objects that are searched for performing some action.
The composition over inheritance meme in the book does not make things easier (there is no "composition" going on anyway, it is just delegation).
Objects themselves for resource cleanup like RAII are fine of course.
Although short, this article is quite interesting because it presents code examples from "both sides" and the author seems to have a good understanding of both.
Having chewed on this for a while now, my personal synthesis is this: The problem with OO is actually a problem with "inheritance" as the default tool you reach for. Get rid of that and you have what is effectively a different paradigm, with its own cost/benefit tradeoffs.
Inheritance's problem is not that it is "intrinsically" bad, but that it is too big. It is the primary tool for "code reuse" in an inheritance-based language, and it is also the primary tool for "enforcing interfaces" in an inheritance-based language.
However, these two things have no business being bound together like that. Not only do I quite often just want one but not the other, a criticism far more potent than the size of the text in this post making it indicates (this is a huge problem), the binding introduces its own brand new problem, the Liskov Substitution Principle, which in a nutshell is that any subclass must be able to be be fully substituted into any place where the superclass appears and not only "function correctly" but continue to maintain all properties of the superclass. This turns out to be vastly more limiting than most OO programmers realize, and they break it quite casually. And this is unfortunately one of those pernicious errors that doesn't immediately crash the program and blow up, but corrodes not only the code base, but the architecture as you scale up. The architecture tends to develop such that it creates situations where LSP violations are forced. A simple example would be that you need to provide some instance of a deeply-inherited class in order to do some operation, but you need that functionality in a context that can not provide all the promises necessary to have an LSP-compliant class. As a simple example of that, imagine the class requires having some logging functionality but you can't provide it for some reason, but you have to jam it in anyhow.
It is far better to uncouple these two things. Use interfaces/traits/whatever your language calls them that anything can conform to, and use functions for code reuse. Become comfortable with the idea that you may have to provide a "default method" implementation that other implementers may have to explicitly pick up once per data type rather than get "automatically" through a subclass inheritance. In my experience this turns out to happen a lot less than you'd think anyhow, but still, in general, I really suggest being comfortable with the idea that you can provide a lot of functionality through functions and composed objects and don't strain to save users of that code one line of invocation or whatever.
Plus, getting rid of inheritance gets rid of the LSP, which turns out to be a really good thing since almost nobody is thinking about it or honoring it anyhow. I don't mean that as a criticism against programmers, either; it's honestly a rather twitchy principle in real life and in my opinion ignoring it is generally the right answer anyhow, for most people most of the time. But that becomes problematic when you're working in a language that technically, secretly, without most people realizing it, actually requires it for scaling up.
> Plus, getting rid of inheritance gets rid of the LSP
No it doesn't. Interfaces need to follow substitutability rules too. Any type you substitute for another does, and that includes things like functions too.
One very important issue in OOP is packaging together variable names. You can see the issue by looking at this atrocious function signature from the Python Pandas library:
> pandas.read_csv(filepath_or_buffer, *, sep=<no_default>, delimiter=None, header='infer', names=<no_default>, index_col=None, usecols=None, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skipinitialspace=False, skiprows=None, skipfooter=0, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=<no_default>, skip_blank_lines=True, parse_dates=None, infer_datetime_format=<no_default>, keep_date_col=<no_default>, date_parser=<no_default>, date_format=None, dayfirst=False, cache_dates=True, iterator=False, chunksize=None, compression='infer', thousands=None, decimal='.', lineterminator=None, quotechar='"', quoting=0, doublequote=True, escapechar=None, comment=None, encoding=None, encoding_errors='strict', dialect=None, on_bad_lines='error', delim_whitespace=<no_default>, low_memory=True, memory_map=False, float_precision=None, storage_options=None, dtype_backend=<no_default>)
An OOP approach would define a Reader object that has many methods supporting various configuration options (setSkipRows(..), setNaFilter(...), etc), perhaps using a fluent style. Finally you call a read() method that returns the DataFrame.
The author brings up the basically the canonical example of where OOP style design shines, and where functional programming will falter.
The simple truth however is that overly going into either functional or OOP camp will hurt because strict adherence becomes subscribing to a silver-bullet.
The middle road is simply a better engineering option, use a practical language that supports both paradigms.
Keep data transforms and algorithmic calculations in functional style because those tend to become hot messes if you rely overly on mutation (even if there is performance gains, correctness is far far easier to get right and write tests for with a functional approach), then there are other concerns where an OOP derived system with inheritance abstractions will make things easier.
Semi-mutable, semi-private, semi-nullable, semi-determistic, and semi-statically-typed. Best of both camps.
How do functional styles falter? Erlang's telemetry/logger systems are absolutely fantastic.
This article describes the OOP approach of changing, say, a Logger to an abstract class with a default implementation, without needing to modify code that uses it, and also describes this as something that could be done in C++. But this doesn’t work at all in C++ if there is ever a Logger on the stack!
That’s a Logger, and it will not magically become a subclass.And changing this to go through pointers everywhere and to use virtual functions, in C++, is not very performant. A good JIT compiler may be able to effectively devirtualize it, but C++ compilers are unlikely to be able to do this effectively.
In moderation, yes. The problem with OOP, like all other paradigms that came before, is applying it dogmatically and excessively, which always happens when there's a lot of hype, novelty, and $$$ to be made.
I agree but "moderation" isn't the best word. OOP is good when used appropriately and not when you fixate that everything has to be within the OOP structure.
It's the same thing with languages, IMO. People try to shoehorn tasks into a language when it would be better off in another language.
> Subtyping, where if a type B implements the public interface of type A, values of type B can be passed as A.
I am confused by this statement or it is going against what I understand.
If you have created a Type B variable, and you also have a new interface called A, and B implements A, then why would Type B variable's values be passed to A's values. 'A' is only an interface.
[delayed]
I noticed there's an entire paragraph explaining what OOP is, but it might be helpful to clarify that OOP stands for Object-Oriented Programming. Even though it's a well-known acronym, adding that explanation could benefit readers who are new to the concept.
I remember the early days of HTML, when people excitedly used hyperlinks to define acronyms.
I changed my mine on this. I did OOP since the mid-1990s when I learned it in high school up until about 10 years ago. I find OOP works best when you have a single coder who can store the model of the system in this mind and work out how to design the base and abstract classes well. And they also have freedom to refactor THE WHOLE CODEBASE when they get it wrong (probably multiple times.) Then you can make these webs of elegant ontologies work.
But in real life, when there is a team, you run into the fragile base class [1] constantly and changing that base class causes horrible issues across your code base.
I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies.
I agree with encapsulation and modularity and well defined interfaces (typed function signatures are amazing.) I just completely disagree with inheritance in all forms.
There are no benefits to it (besides feeling smart because you've made an elegant but ultimately brittle ontology of objects and methods), just a ton of downsides.
[1] https://en.wikipedia.org/wiki/Fragile_base_class#:~:text=The....
I totally agree that class hierarchies are mostly a trap, especially when there's more than one developer on a project.
What I don't understand is why many programmers still use classes even when they plan on avoiding inheritance. I've seen this sort of thing on the past few major projects I've been involved with; initial creators heavily relied on inheritance, future developers realize that was a mistake, then said future developers continue using the class construct of whatever given language.
At that point, why not just use functions and regular objects? Without inheritance, classes tend to have these other rules and complications that don't seem to really add anything when there is no hierarchy.
>I have found that OOP with inheritance is actually a form of tight coupling
You are not the only one. Prefer composition to inheritance is a saying for a reason. Inheritance is powerful and useful for small problems, but as you say it introduces tight coupling and so should only be used where that is intended. Tight coupling isn't always bad, but it is bad often enough to avoid it.
> I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies.
Tight coupling is the basis of oop. A method is tied to an instance. Methods cannot be composed with other methods or functions without instantiating state. Methods cannot be moved into other scope.
The literal definition of an object is a tightly bound set of methods that cannot ever be used without instantiating state.
> Tight coupling is the basis of oop.
This is not what OOP people are talking about when they talk about tight or loose coupling though.
They are talking about the relationship between classes.
Coupling behaviors to types is not a problem. Class inheritance and multi-inheritance with their weird taxonomical trees are the problem.
There's little practical reason to build a family tree of supertypes and subtypes outside of building GUIs, yet this is how class-based OOP is designed and taught.
Trait-based OOP gets this right. The hierarchy is completely flat. You simply implement the behaviors for the types you want and don't have to think about grandparent behavior and final interfaces.
I don't think a method being tied to an instance is the best case for calling it "tight coupling".
OOP can be used to design relatively uncoupled systems.
One of the lessons learned in all these decades is what the grandparent post alludes to, which is distilled into "prefer composition over inheritance". Implementation inheritance (as opposed to interface inheritance) indeed introduces coupling and is therefore discouraged in current advice.
> OOP can be used to design relatively uncoupled systems.
Never go full-Object-oriented programming.
In this case I think it is valuable to make a distinction between OOP which is a style of programming, and object-oriented languages, which are just a language designed with that style in mind.
I have seen issues in codebases where developers have used OOP as style to aspire to, using it in an academic sense. They tend to try to use inheritance frequently, have deep inheritance trees, and suffer from hidden coupling through these.
On the other hand, those who use object oriented languages in a mostly functional style (side-effect free functions, effective immutability, and almost no use of inheritance) tend to be much healthier in the long term.
So it's fine to use OO languages, but never go full OO programming.
Exactly my thoughts. When I started really getting functional programming it was a breath of fresh air. For me the test is not just how fast someone else can pick up the codebase but how fast I can remember how it all works in n years.
> I find OOP works best when you have a single coder who can store the model of the system in this mind and work how out to design the base and abstract classes well.
When all the states of the program and all the transitions between them fit into one programmer's head - every programming paradigm works well.
My thoughts as well. Inheritance forces you into spaghettification sooner or later if you didn't consider everything you'll ever need from the start to the end of the universe. Even if you're working on it by yourself the refactors become frequent and take too much time for little gain.
Self contained encapsulated parts with an eventbus for entirely decoupled data propagation are much easier to manage and edit. Just duplicate a bit more than you think would be best to reuse, disk space is cheap and if you get that part wrong you're screwed and will need another rewrite.
And it's probably more performant too with newer hardware since you can usually just spin out any self contained part as its own thread if need be, often with almost zero changes.
>I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies [..] I just completely disagree with inheritance in all forms
This is pretty common advice (at least IME), usually distilled into the form "prefer composition to inheritance".
> This is pretty common advice (at least IME), usually distilled into the form "prefer composition to inheritance".
I think this is why in the current JavaScript era, it is rare to find a popular library that makes significant use of inheritance (as opposed to just supporting interfaces) even though it is supported in the language.
ThreeJS is one of those exceptions, but they are few and far between.
Similar thinking, I've been through much pain from a class hierarchy degrading into an unusable mess, also done a lot of rework refactoring those and it doesn't pay off.
Instead of inheritance I much rather prefer the composition approach, not extending classes but defining interfaces for the API, using other classes in the composition of an object rather than relying on overrides/implementations from a base class. It's much clearer to reason about (no more 3-4 layers of indirection), easier to refactor as well.
The GoF book did a lot of damage. I've finally read it and was amazed that the entire book is in fact about writing GUIs, which is just one tiny part of programming.
Several patterns are trivial, others are very similar and are just a linked list of objects that are searched for performing some action.
The composition over inheritance meme in the book does not make things easier (there is no "composition" going on anyway, it is just delegation).
Objects themselves for resource cleanup like RAII are fine of course.
Some people will say that the book is not a training guide but a naming guide for patterns (or antipatterns) people already were using with success.
Although short, this article is quite interesting because it presents code examples from "both sides" and the author seems to have a good understanding of both.
Having chewed on this for a while now, my personal synthesis is this: The problem with OO is actually a problem with "inheritance" as the default tool you reach for. Get rid of that and you have what is effectively a different paradigm, with its own cost/benefit tradeoffs.
Inheritance's problem is not that it is "intrinsically" bad, but that it is too big. It is the primary tool for "code reuse" in an inheritance-based language, and it is also the primary tool for "enforcing interfaces" in an inheritance-based language.
However, these two things have no business being bound together like that. Not only do I quite often just want one but not the other, a criticism far more potent than the size of the text in this post making it indicates (this is a huge problem), the binding introduces its own brand new problem, the Liskov Substitution Principle, which in a nutshell is that any subclass must be able to be be fully substituted into any place where the superclass appears and not only "function correctly" but continue to maintain all properties of the superclass. This turns out to be vastly more limiting than most OO programmers realize, and they break it quite casually. And this is unfortunately one of those pernicious errors that doesn't immediately crash the program and blow up, but corrodes not only the code base, but the architecture as you scale up. The architecture tends to develop such that it creates situations where LSP violations are forced. A simple example would be that you need to provide some instance of a deeply-inherited class in order to do some operation, but you need that functionality in a context that can not provide all the promises necessary to have an LSP-compliant class. As a simple example of that, imagine the class requires having some logging functionality but you can't provide it for some reason, but you have to jam it in anyhow.
It is far better to uncouple these two things. Use interfaces/traits/whatever your language calls them that anything can conform to, and use functions for code reuse. Become comfortable with the idea that you may have to provide a "default method" implementation that other implementers may have to explicitly pick up once per data type rather than get "automatically" through a subclass inheritance. In my experience this turns out to happen a lot less than you'd think anyhow, but still, in general, I really suggest being comfortable with the idea that you can provide a lot of functionality through functions and composed objects and don't strain to save users of that code one line of invocation or whatever.
Plus, getting rid of inheritance gets rid of the LSP, which turns out to be a really good thing since almost nobody is thinking about it or honoring it anyhow. I don't mean that as a criticism against programmers, either; it's honestly a rather twitchy principle in real life and in my opinion ignoring it is generally the right answer anyhow, for most people most of the time. But that becomes problematic when you're working in a language that technically, secretly, without most people realizing it, actually requires it for scaling up.
> Plus, getting rid of inheritance gets rid of the LSP
No it doesn't. Interfaces need to follow substitutability rules too. Any type you substitute for another does, and that includes things like functions too.
It’s pretty bad, actually. Especially the Smalltalk/Objective-C flavor with its late binding and messages