The Case Against OOP Is Wildly Overstated
medium.com
medium.com
Well put. To turn it around, the ideal of state management is:
- Immutable, isolated state
- State operations are singular - I guess meaning centralized as well as atomic
- State operations are modular and cohesive units of code
- Can be understood in its totality - which implies centralized state management, where operations are not scattered thoroughout and intertwined with, for example, UI code
In my experience, I generally agree with the sentiment against "proper" OOP, as an accepted or preferred style of code organization. This is related to some fundamental truths and advantages that the rising popularity of functional programming is revealing.
Even in the world of UI and software for the web, the classic OOP patterns are getting "disrupted" in a healthy way, and it seems to be moving in the direction of composing immutable states, idempotent functions with no side-effects, where state changes and effects are centally managed in an isolated way.
State and zip code always change together. Having had to fix code that passed 5 variables around for an address (which needed to go to 6 for “address 2”) I replaced it with an address object.
For me, true OOP requires some sort of inheritance and/or runtime method dispatch, at least.
Can we at least agree that limiting operations on the structure to the code that controls that structure is following the definition of encapsulation, which is what I should have said anyway?
Not sure I understand what this means. Could you give an example?
Here's a data structure that represents a bill of materials. It has a list of components, and a total cost, which is the sum of the costs of the components on the list. That's the invariant - that the total cost is the sum of the costs of the components in the list.
If it's just a structure, then someone can add or remove a component, and forget to update the total cost, and the invariant is violated. But if it's an object, if the code is bound to the data (in the way OOP normally calls encapsulation), then only the member functions can modify the structure. So if you want to add a component, you have to use the object's addComponent method, which (in a properly debugged object) isn't going to forget to update the total cost.
Now, you could say that the structure could have a code library to go with it, and that could have an addComponent function. That's true. But the user doesn't have to use that function - they can mess with the list directly, if they think they know what they're doing. Whereas with OOP encapsulation, they have to use the existing function. That function can guarantee that the object's invariants are maintained.
All this said, this is basically OOP in C, and the code is still bound to the data, only it is bound by naming (point_add()) not by a scope (point.add()).
I love opaque structures, but the one downside in embedded is they can’t be truly opaque since you want to avoid dynamic memory allocation - and therefore the place you instantiate the struct needs to know the size, and therefore members of the struct.
You can achieve the same kind of hiding in Java by using an existential type. E.g. you could have a library that looked like:
interface Cursor<T> {
T start();
T nextStep(T currentState, int move);
void finish(T);
}
class DataStructure {
Cursor<?> traverse();
}
And then you don't know what the internal state of the cursor is because the T type is hidden from you, but the compiler checks that you always call nextStep() with the same state you got back from start(), and you can't possibly mix up the state from two different cursors.[1]: https://craftinginterpreters.com/representing-code.html#the-...
There's a potential performance cost here, but that usually doesn't matter a whole lot.
Yes, but this is not possible in general. For instance, what if you want to endow your List with a "MaxTotalCost", set at creation. You can't ensure that your list respects that invariant, other than by encapsulating it behind some custom interface.
But then again, what you want there is modularity, which OOP provides, but is also well-supported in other paradigms.
[1] Object Oriented Programming Is An Expensive Disaster Which Must End
http://www.smashcompany.com/technology/object-oriented-progr...
A: OOP isn't so bad, actually.
B: But what about all these terrible messes?
A: Oh, that's not a problem with OOP,
they're just doing it wrong.
I'm sorry, but when 80% of the industry (and 100% of new grads) are "just doing it wrong", it isn't helpful to no-true-scotsman the critics.The only way I see out of this endless game of semantics is for someone to canonize the tightrope of practices which are "OOP done right", and then give it a different name.
Our industry has a naming problem when it comes to practices, especially when the practice is subtle and complex but its name is simple and trendy (maybe it's not specific to software, IDK).
Dev teams are mislead into thinking that they can deduce the practice from the name (and maybe a 1h training with a self-proclaimed expert in the practice).
This is how we got aberrations like: - "TDD" devs writing their tests afterwards - "Agile" teams with 6-month release period - "SCRUM" masters that act as classic managers
New names referring to "good/right/trendy" practices are doomed to get claimed by "80% of the industry", in a way that doesn't respect the original practice.
Alan Kay recently said that maybe he should have called it "Message-oriented programming" ( instead of OOP ). While not having "object" mostly eliminate the risk of the "one-class-per-real-world-object" antipattern, we might as well had ended up with other anti-patterns based on wrong interpretation of what constitutes a "message".
This is why I think changing the name of any practice is inefficient. The misunderstanding isn't accidental, it's systemic (Moreover, renaming a practice would probably create even more confusion)
Names that refer to common mistakes wouldn't get claimed ; no team is going to proudly explain to their customer that they're using the "big ball of mud" antipattern, the "scrum but" team organisation, or the "debug later" dev practice.
So maybe we should better canonize OOP antipatterns?
Though when a microservice mutates something, it's typically persisted in a database or similar (and, you'd hope, not sprinkled throughout its code). But, yeah, a bunch of communicating microservices are a lot like communicating objects - for better or worse. Same with Erlang-style actors.
The engineering question is what layer is it appropriate to do so. Under traditional OOP (your pre functional forward Java/C#/OOP C++), the answer was that code is almost always bound to data.
Haskell style functional programming binds code to data at the last moment possible.
The best answer is likely somewhere in the middle, depending on your application and usage. I find, for example, that UI controls work well with OOP, but most other stuff I tend to default to functional style programming.
Microservices require every action you take be a message and enforce that messages are the only way to communicate which makes it easier to avoid indirect (and thus hard to track) dependencies.
For instance if the person's age is updated when you calculate their hours worked, in OO that might be hidden from view but in message passing you would at least see that a new person object came back that replaced the one you had.
None of this stops you from doing evil unobvious things with your hidden information (updating everyones age whenever anyone gets overtime) but it is usually harder to accidentally do that.
To be clear while I like microservices I think they have many problems just like OO. For instances caching becomes super complicated and there is a real cost of going over the network for everything you do. The tradeoffs are just a different set of tradeoffs.
If you're going this far, you're essentially throwing away typing as well.
Are you actually just opposed to private fields?
For example, a hash map data structure hides the details of how the hash map is implemented, but it doesn't hide the data. In some versions of OO, the object should not only hide the implementation details but also the data. Any function that needs that data MUST be a method of that object. That's where things go haywire.
Devs aren't clairvoyant so when we're making a new type we might sometimes get this wrong. But this is a fundamental issue with type systems in general and not OOP, is it not?
Most of the languages which are suggested to be superior alternatives to OOP in this thread have immutable data records and algebraic data types as core building blocks. These are very different than C-style structs, as they can provide stronger safety guarantees without a massive ream of boilerplate.
A class or method interface promises unfortunately little around essential topics like mutability, concurrency, or termination.
That is the mechanism for immutability that is traditionally provided. It is easy to opt out of that protection.
Java and C# are getting records. They're a convenience but are in no way opposed or counter to OOP.
What OOP offers on top of modules is extendability and polymorphism.
And these two capabilities are possible to achieve in various ways.
There arent many hash map methods that will actually modify the data.
Standard OOP, on the other hand, seems to encourage such behavior. So, for example, a Company object could contain a bunch of Department objects, each of which contained a bunch of Employee objects.
These people objects could also be referenced in areas outside of the company object.
If I called a company.giveFinanceDepartmentARaiseInDollars(10000) function on the Company object, it would have an unpredictable impact on the Department/Employee objects. It may also have cascading effects in other parts of the application that may not be clear.
On the other hand, if you represented this as a company hashmap, with the keys the department structs, you would have to do something like the following instead:
company["finance"].employees.forEach(x => x.salary = x.salary + 10000); company["finance"].base_salary = company["finance"].base_salary 10000
This contrived example shows some of the pros/cons. OOP allows us to hide a lot of behavior/changes, which may mean less repetitive code. OTOH, it hides a lot of behavior/changes.
More generally the issue of global mutable state and keeping your own sanity as a developer is a problem across paradigms.
My “functional” code wasn’t actually functional.
If I had written that correctly, by returning a modified copy using a map instead of modifying the strict in a loop, your example would also be covered, because the function you’re describing would not modify any of its inputs, but instead would return a new copy. And the function wouldn’t have any side effects either, so it would once again be completely clear what’s happening.
Where things would get silly is if you tried to make all state internal. But that's what some purist OO approaches would do for business objects.
By contrast, in an 'extreme procedural' style, you would have a struct that is an array of buckets and an integer, and functions that can take such a struct and return information from some particular place inside it. In normal use you would just use the function, but if you're passing your hash map to someone else, there is no longer a guarantee that the length field still matches the actual buckets, or that objects are still arranged into buckets based on their hashes.
Anybody modifying Customer object would hopefully be in the right mindset regarding personal information, but once it gets out as a string from GetName or GetEmail, all bets are off, it gets into all kind of logs.
Anything security related is the same.
A better example might be a linked list. With information hiding, you manipulate the list through functions like push and pop. Without information hiding, you manipulate the pointers directly. For instance, I've worked on C codebases where linked list manipulation isn't even hidden behind macros.
Object methods are just sugar for this, so whats the difference to you? When you say "separate" do you mean you just want it in different files? Different curly braces? Different namespaces?
Is it inheritance and virtual functions?
I used to see that type of approach in some complex Fortran simulation programs where every function referenced a huge common block. I really don't think that leads to easier to manage solutions.
1.) Who can access this state, and which of those accesses allow writes vs. which are read-only?
2.) What is the lifetime of that state, and what portion of that lifetime is mutable vs. read-only?
Java-style OOP can control #1, but makes no distinction between reads & writes. C++ const correctness and the val/var distinction in Kotlin/Ocaml/ES6/etc. can make that distinction, but only C++ forces you to think in terms of lifetimes. Rust borrowing probably comes closest to answering all 4.
I'm still waiting for a language feature that lets you say "This field of this struct is only mutable while this module is running, and once it completes it will never be changed", though. A lot of data structures are initialized in passes and then passed along as constant data: you construct the basic object structure with nothing but a few IDs, then you compute extra fields as necessary, then you're done and the object is never written to again. If you could keep the fields mutable during initialization (which may be a longer process than just constructor calls) and then seal them off once that's complete, you eliminate a whole class of bugs where a read-only client decides to mutate a field that should only be mutated from designated initializer.
Why do you want to think of it as "the same" struct? It seems to me that the way to achieve this is having the partially initialised thing be a different type than the fully initialised thing, along with a linear type system that makes sure the partially-initialised one is consumed in the process of making the fully-initialised one.
This is likely cleaner than sharing a reference which is sometimes mutable and sometimes not. Rust of course allows for both mutable and immutable borrows, and that's a very powerful tool but it'd be complex if all you needed was field initialization.
The consensus across languages that's emerged is that initialization shouldn't be a group effort, with references escaping in various stages of partial initialization. Be it RAII in C++ or functional record constructors, the common theme is that initialization should be a single operation that can succeed or fail as a whole.
class Symbol {
@init(constructor) originalToken: Token
@init(constructor) position: SourceLocation
@init(Parser.parse) scope: SymbolTable
@init(Parser.parse) declaredType: Type
@init(TypeChecker.typecheck) inferredType: Type
@init(DataFlowAnalyzer.computeLiveness) usages: List<Expr>
}
And then the type system carries an extra bit of information around for whether a reference is fully-constructed or not, much like const-correctness. Fields marked with @init can only be written on a reference that's not fully-constructed. There's no compile-time enforcement for initialization order, though it'd be pretty easy to do this at runtime (convert them to null-checks or special not-initialized sentinel values). Newly-created references start out with the "initializing" bit set, but once they're returned from a function or passed into a function not in the list of legal accessors, they lose this bit unless explicitly declared.It's basically the same way "mutable" works (in languages that support it), but with a separate state bit in the type system and extra access checks within the mutating functions to make sure they only touch the fields they're declared to touch. You can fake this now by passing mutable references into initialization functions and then only using const, but it's a bit less specific because many classes are designed to be long-term mutable through 1-2 specific public APIs but also need to be mutated internally for deferred initialization, and lumping these use-cases together means that deferred fields can be touched by the public API.
Clojure, in 2020, is very very different from the Lisp of 50 years ago and is modern in many respects, even compared to other popular languages.
In proper OO state is hidden and insofar as it causes issues it does so in a way that is handled by the object rather than the program overall.
Also treating code as data isn't a feature of OO, it's an old idea starting with Lisp and metaprogramming I suppose and it's present in functional languages as well. It basically just means that the language itself is a first class data type within the language.
One problem is that every OOP advocate has a different perspective on "what the entire point of OOP is". Another problem is that irrespective of what any OOP advocate believes is OOP, the body of code that is commonly understood to be "OOP" is plagued with abuses of inheritance (I'm not even convinced there are legitimate uses), mutable state, the "banana with a reference to the gorilla holding it with a reference to the entire jungle" problem. OOP advocates are quick to say "that's not real OOP" in a very no-true-scotsman fashion, but that aspirational statement isn't comforting to people who have to deal with these problems day-in-and-day-out.
> Also treating code as data isn't a feature of OO, it's an old idea starting with Lisp I suppose and it's present in functional languages as well.
The parent specified that their objection was about tightly binding data to code, not about treating data as code.
It's true that bad OO code suffers of different problems than bad FP code, and both are different from bad procedural code. But there is no guarantee that an organization which produced bad OO code would have produced good FP code, just as there was never a reason to imagine (though many did) that an organization that produced bad procedural code would produce good OO code if forced to switch to OO.
In general, there is no solver bullet. We can absolutely replace the problems specific to bad OO code with problems specific to bad FP code if we move from OO to FP, but we cant be sure of any other outcome.
More likely than not, we need to change more than the style of code we build if we want to take an org that has produced bad code and make it produce good code - we should change incentives, testing, goals, timelines etc.
And just to be clear, you're absolutely right that saying 'that's not good OO code' is no-true-Scotsman and not helpful in any way, other than pointing out that it is possible to produce good OO code (some people doubt that, I think).
The difference is that FP, procedural, data oriented, etc are all pretty reasonably defined. No one seems to agree with what OOP is, and any objection to the kind of code that is typically considered "OOP" is that "that's not real/good/etc OOP code". It's a no true Scotsman.
When I talk with OOP proponents and point to issues about inheritance or the gorilla/banana/jungle problems, they say that these things aren't part of OOP. I think when you deduct from OOP these kinds of warts, you end up with something that looks almost indistinguishable from data oriented programming (something like what you would write in Go or Rust) and increasingly something that resembles functional programming specifically now that many OOP languages have support for first class functions and more functional abstractions in their standard libraries.
So if there is any truth in the "not true OOP" argument, I think it's that "good object oriented programming" is just another spelling of "data oriented programming".
One is the Alan Kay camp, which usually defines OOP as primarily message passing, with Smalltalk as a beacon of what they consider good code. This is a very vocal group, but I think that it is extremely obscure in the industry.
The other group is mostly focused on SOLID and design patterns. I would say that Java's Swing is a good example of a usable library based on this style of code, inheritance-heavy but still principled.
I believe that this style of code is most reliant on garbage collection to actually be reasonable. Otherwise, you end up caring about ownership for every little object and it rapidly becomes a nightmare. I believe your banana/gorilla/jungle problem is related to this mostly.
For myself, I do believe that implementation inheritance is usually to be avoided, but I think that other OOP concepts, such as encapsulation and interface-style extensibility, using constructors to establish invariants, are frequently useful, especially in the high-level architecture of a program or module. I do prefer some kind of support for sum types and multiple dispatch, rather than traditional virtual dispatch.
Some of those things still apply in non-OOP style programming, but OOP certainly has a larger cognitive load for many nontrivial applications.
Keeping code and state more decoupled actually reduces cognitive load in many cases. In some of those, there is less encapsulation, but at least IMO OOP overshoots for how much encapsulation is productive.
Now, I am not religious about this and regularly approve OOP designs in reviews. But my general approach is to prefer modules and packages of functions for behavior, plain structures for state, and only dip into OOP style encapsulation when it seems the extra encapsulation and bundling is worth it. A class modeling a mutex or other resource would qualify there.
If actual usage of paradigms is an indication rather than belief then it doesn't look like functional programming really is intuitive, it tends to be very useful in certain niches.
Nitpick, but I'm not sure this is the correct plural.
As you may very well know (but just in case, and for others), the "system" in ECS doesn't describe ECS as a whole, but another part of the model - you have entities, attributes of those entities, and systems that operate on those attributes.
That said, I'm not sure what I'd suggest instead. "Entity component system systems" is obviously terrible. Just not pluralizing probably works, here? or "towards the entity component system pattern"?
... shrug
What there actually is in game development is a definite wish for a simpler language than C++ and dislike of many of the newer parts of it.
And that’s the fundamental problem with OOP. State can’t be encapsulated. It’s highly radioactive. State should be kept to the boundaries of your application, where it interfaces with the real world, and minimized as much as possible. The core should be immutable.
And, if you have code that is for working on a particular kind of data, why is binding it to the data problematic?
I don't agree at all. It's much more dangerous to separate data representation from the code that manipulates the data. I can't tell you how many bugs I've found and had to fix in large codebases (most of which are procedural-code-in-"OOP"-languages) that result from some horrific evolving arrays-and-records structure not recording intent and validity constraints properly (since that would require, you know, code). Instead, each interaction with the Holy Data Structure requires the code to go to confession, recite ten Hail Marys and five Our Fathers before receiving the Almighty's state-changing grace, mercy and peace; failure to do so will result in immediate confinement in purgatory, and possibly damnation.
I'm not being hyperbolic albeit a bit facetious about this. "Data" itself is the problem: it is subjective, and so requires a lot of tinkering for a programmer to grok. That tinkering transforms it into information, but only in the programmer's head, which is less than ideal since it's outside of the program. If that information fades away, it must again be rebuilt (or worse, be short circuited so a hotfix can go out to prod).
> and that statefulness is fine to freely sprinkle throughout your program.
I agree much more with this, with the condition that shared statefulness sprinkled throughout is the problem. An object should be treated with the same respect we would show a VM or even a person: decent ones would not tolerate having an external entity reach in and manipulate things like memory, nor would they be so care-free as to depend on and trust literally everything they are told. I think the big problem here is that few or no OO languages are powerful enough to allow for this kind of arrangement to be the norm. But it is a valid criticism and should be taken more seriously by OO language designers.
EDIT: I wanted to add one more thought, which is that when I'm working on a new feature or new codebase, I tend to model the problem first in a procedural style. Then I rewrite it as objects. I've had very little luck trying to do objects first (though I think this is just me), because decomposing the problem space is just hard to do de novo. But once I understand the problem, it's easier to see the constraints and issues.
Also, certain code that doesn't need to worry about things like noisy channels (e.g. Dijkstra's shortest path algorithm) is probably a good candidate for non-OO code. OO is good for the large amount of software that interacts with people across multiple machines and environments, rather than isolated from everything in just a single machine.
This is grevious error, IMHO, that leads to nothing but problems: endless boilerplate code, lots of "interfaces" that essentially do nothing (that couldn't be done directly with said data), and debugging nightmares.
I much prefer to work with systems where code and data are separate and never the twain shall meet.
There are various legends, and they mostly conflict with each other.
Homoiconicity is about having the same syntax for both structure and data.
First class functions means there is no conversion, again at syntax level, between a function and a value.
Mainstream languages all have a way to see code as data, it's function pointer in C, delegate and lambda in C# and first class functions in Python or JavaScript.
My code became simpler, easier to understand.
Of course it wouldn't get the approval of any software architect or any design pattern fanatic, but I can take any code I wrote 3 years ago, understand it on the fly and edit it with confidence. There are few bugs and they're never a nightmare to find.
Now if I have to modify the code I was writing before my KISS revelation, when I was reading too much about OOP and design patterns then the headaches start. Often I'm left wondering why oh why I had to use so many levels of indirection to solve what is a simple problem.
Still, finding the simplest-possible solution to complex problems is often challenging in itself, especially considering what is "simple" or "intuitive" can be highly subjective. Engineers are often uncomfortable thinking about these problems because the domain is shifted somewhat away from the realm of logic machines and towards psychology and UX, in which they have little to no education or experience.
I worked on a mobile app once that really tried to force one of the fancier design patterns — it required something like six files to handle one screen (because, of course, it's best practice to separate your view, view model, controller, etc.) However, a good part of the org didn't understand the nuances of what code should go where, and found it absurdly tedious to manage all of these files to build one minor screen. Velocity plummeted.
I think a good design pattern allows you to scale as complexity increases. Let folks start simple, then provide a clear path to a consistent design pattern that handles more complexity as it's needed.
---
Modelling system state and effects (not data) is a good application. The article criticizes the following statement originally from the Oracle Java docs:
“Objects are key to understanding object-oriented technology. Look around right now and you’ll find many examples of real-world objects: your dog, your desk, your television set, your bicycle … Software objects are conceptually similar to real-world objects.”
I call this "Kindergarten-OO": It attempts to simulate things in the world as stateful Objects, which often should be modeled as plain data with generic data manipulation tools.
System state and effects however are _inherently_ stateful and effectful. File systems, DB/network connections, peripheral devices, that sort of thing. It makes sense to apply OOP here because you want to model these things with state-machine behavior and configuration/constructors in mind.
---
Also there is an interesting move away from "traditional" OOP by modern languages like Go and Rust. They emphasize set-like composition instead of hierarchical inheritance and implicit/user-defined interfaces/traits to both enable common abstractions/naming as well as increasing flexibility.
Both of these things essentially lead to simpler code and IMO address one of the original OO ideas of data-less programming (AKA I only care about this type of behaviour, not the whole thing) but improve the paradigm by rejecting inheritance and explicit (rigid) interface declaration.
If OOP provides the footgun to shoot yourself, OOP is at fault. Overall the article reads as "OOP has no problems if is used correctly". Of course that's true. I have seen well designed OOP programs, but the majority was not. The author should ask the question, why OOP is applied the wrong way so often.
well, if universities started by teaching something else that "Cat" inherits "Animal" in 2020 ...
It turns out this translates to general purpose programming exceedingly poorly. It spends your valuable class hierarchy budget on the wrong things, while discouraging spending it on the right things.
On the other hand, OO turned out to be a useful gateway into polymorphic programming (not the only one, but a useful one), and to be a useful organization principle in real programs when used differently. Instead of Cats inheriting from Animals, you have interfaces for things like Iterators, which aren't real world objects at all, and you have objects that encapsulate certain operations, or certain patterns, or other much more abstract things that have no concrete physical referent. This style can work fairly well, especially if you don't overuse inheritance.
The simulation argument has really faded in the past 20 years, but even now, you get some crossfire between people attacking that formulation vs. people defending the modern version, and they're talking about such different things they might as well not even be talking about the same paradigm.
And as is typically the case, education is a lagging indicator, and even fairly recently I've seen it training people on the simulation model, even though by the 2010s it was pretty clearly useless in practice.
I can't tell you how many times I've seen a project go full onion-pattern with a web service that basically just recieves HTTP requests and makes a few itself and returns some data. Instead of a HTTP request handler that fetches data, perhaps does some mutation and returns it, there's a whole "service layer", a domain model, data fetching abstraction. To change something you have to navigate the winding dependency graph and fight off all the deamons. That's not the pit of success, they're the catacombs of failure.
We should design codebases so that future developers fall into the pit of success, even if it means we haven't been able to show off our knowledge of software architecture etc. The best pattern you can ever apply, is the one that makes change easiest. If you make change hard, the code becomes artificially stale -- I call it 'calcified'. Coupling makes code hard to change.
Thinking about problems through objects, in my experience, encourages people to make 'one model to rule them all', for example in a retail app, you might have a "Product" which ends up having product information, media assets, pricing, promotions, retail and stock keeping information etc which in a peice of software undergoing 'rapid development', multiplies the likelihood of coupling. Which is where Domain Driven Design steps in and tries to preach. Thinking about your problem in terms of data, or events can often help you find the right concepts to create your abstractions around. Another good heuristic is to think about caching, can you cache pricing for the same length of time you can cache product information, for example?
Bit of a ramble, but explains why I tend to shy away from objects, except for where I have a bonafide business problem that needs to be modelled -- with this, the models are only used to make the code make sense to developers and help arrive at the result they produce, rather than the models being the result themselves.
> Software can be considered "general" if it can be used, without change, in a variety of situations. Software can be considered "flexible", if it is easily changed to be used in a variety of situations.
We frequently forget that these are both valid paths to the same end. We tend to laser-focus in on generality, at the expense of flexibility.
And for small problems like the one described, it is usually much easier to go for flexibility at the expense of generality. And I think we should.
Learning it starts to train you to look for these opportunities, and eventually you start thinking "what if this class becomes a foundational class? I want everyone working on it to have a great time. I don't like being cursed out for not predicting the obvious future", and so now there's a bunch of onion-y stuff added.
A reasonable way to OOP is: 1. Understand your domain and work hard to match its structures. 2. Delegate and separate where there's an unquestionable benefit, not for the sake of it. 3. Likewise for inheritance hierarchies. 4. Don't create entities unnecessarily.
Every single decision should have a clear practical rationale. SOLID on its own is not a rationale - it's a guide, but to use it you need to understand what a concern is from the domain POV, not from the code POV.
The most common (but not only) way each side argues its point is to take some contrived example and show how philosophy X does it well, and how philosophy Y does it terribly. These examples are often ranted about by the "Y" proponents because "it's a bad problem choice designed to make Y look bad".
Yes. It is. Because X and Y were not designed to solve the same problems. If you pick an example Y is good at, most of the time X will be bad at it. So if you're doing something like that, use Y. But if you're doing something X is good at, then pick X
Why would you intentionally shoot yourself in the foot to use a framework, language, philosophy, etc. that was never intended to be used for that thing when there's something that was? Stop arguing about who's better at what. X and Y are good at different things. That's why they're _different philosophies/languages/technologies_.
Someone comes along and says "oh, you could do this much better with Haskell and functional programming".
How can you argue that the comparison involves an "X and Y" that are not designed to solve the same problem? Advocates for languages and programming styles are generally doing so on the basis that there is a broad class of software development problems/projects that will benefit from the use of their preferred "X and Y".
Sure, there are some DSLs that are clearly not intended to be compared with (say) C++. And there are some overall programming styles that clearly suite certain kinds of software much more than others (e.g. the absence of an event loop somewhat changes everything, as does high level distributed parallelism).
But OOP isn't an example of such a thing.
Because Y is Haskell and your project has soft-realtime scheduling constraints.
But if Y were Rust, point taken.
The problems come in when someone reads some over-stated screed about design patterns, pure functional programming, etc. and then decides that they have to throw every other tool in the toolbox out, and follow The Right Way™ in every possible way even when they're spending most of their time on problems of their own creation rather than their job. The various philosophies being debated will change over time but that mindset is remarkably stable.
As soon as you want to do something that's not already perfectly built in, everything becomes a mess of impenetrable hacks. Autogenerated Django migrations are unstable and often need to be edited to to be correct or make any damn sense.
It also encourages to people to blur or just straight up stomp on the line between ORM objects - tightly coupled to your database - and business-logic objects (entities, usecases, whatever). It's the Django recommended approach, called "fat models" and it leads to ridiculous levels of coupling and impenetrable ball-of-mud code.
Just write SQL and marshal it into business objects. I'm actually quite fond of doing this with the SQLAlchemy Table & Query APIs, they do most of the marshaling for you.
In Go I just write the raw SQL.
More generally regarding OOP: I have never seen any good come from more than one layer of inheritance.
Once upon a time I worked with a lovely Perl system (!) (yes! Perl!) that used Rose::DB. The main parts of Rose weren't really special in any particular way; the part that was good was that you were provided with rather effective tools for running your own raw SQL queries if you needed to.
Of course the bigger problem with ORMs in OOP isn't raw SQL management. It's the fact that the naive approach to OOP and the approach all the frameworks give programmers looks like "active record pattern" and "fat models". The active record instances can be found all over your code, usually leaking some encapsulation violation wherever they're used, and the fatness of the model usually means that the models also accumulate various concerns from parts of the app that don't belong on the models. Many programmers who approached OOP in an ad-hoc manner aren't even aware of patterns like "entity-component-system" and sometimes have strange ideas of what you're supposed to call a "service".
> I have never seen any good come from more than one layer of inheritance.
Good OOP is compositional and may have a lot of "has-a-strategy" and "implements-an-interface" patterns but usually only a very few "inherits-from" patterns. OOP tools like strongly-typed systems are there to bring you clarity and safety as you move data from your "query XYZs", "plan some XYZ operations", "execute XYZ operations", and the actual system interfaces to perform the operations.
And I agree with you on inheritance. One level max for production code, one additional level for tests that need to change a method here or there to get them to work. That's it.
If you need composition at the DB level, that is what Views are for.
User.banned.posts.flagged where we want all flagged posts from banned users.
Posts.flagged.today where we want all flagged posts from today
User.created_today.posts.flagged where we want all flagged posts from users created today
There's probably some room for limited compositional SQL, but there are ways to get that without a full ORM, given your type system is expressive enough.
> In Go I just write the raw SQL.
But in general I agree with you about ORMs (I do not use them). I'm just pointing out that when the easiest/most popular library to reach for to protect yourself against injection is an ORM, it's understandable to use it.
[0]: https://docs.spring.io/spring-data/jpa/docs/current/referenc...
If you're going to stand up a business in a week or a month, Django ORM and its migrations are going to get you a pretty darn good solution without spending hours and hours thinking about your RDB design and handling many-to-many relationships "by hand".
I've never seen a business go bankrupt because of the DB, but I've seen plenty never make it to market because all their money was burned in development. This is stupid and a great example of how shitty engineering can doom a project.
Most important thing is to determine if people are actually gonna buy your app, if you can make a business out of it. Like it or not, marketing is the most important part of the business.
Businesses are confronted with different problems at different times. Insisting that everything be solved/designed perfectly up front is silly.
Use the ORM for perfectly built in things, but also use raw SQL when it's not.
Generally we start with Entity Framework Core, because it's well known and let's us compose and execute queries really easily (I should add we always use a "code first" approach; the actual database is created carefully by us, not the ORM).
For some apps, you need to do more complex stuff, such as use composite keys, duplicating rows, etc - while this might technically be possible with EF Core, it's horrible in practice. Also in some cases you just can't coax EF to generate a performant query. In these cases we either add a view, or we add a micro-ORM into the mix, like Dapper - you basically write the SQL yourself, but Dapper helps marshal data from the database to your classes.
What is your background? C++ combines interfaces and abstract classes, so this may be the issue you're running into. An interface is an abstract class with no instance variables or method implementations. The trouble with inheritance lies in overriding the superclass's method implementations.
(C++ style) OOP encapsulation is different. Only member functions can modify the "struct" (object) data (unless you did something crazy like make your data members public). If the data is in an invalid state one of the member functions did it. Nobody else had the ability to do so. Your debugging got easier, because there's a much smaller set of places where the problem could be.
Now, true, you could do that in C, with the structure declared only in one C file (and no header file), and all functions declared file static except the "public interface". But C++ make that the normal way to work, not something that you had to go out of your way to do.
Inheritance: If you have a problem where it adds value, use it. It's a tool, not a religious dogma (either for or against). You could argue that most people, when they think they have a problem where inheritance adds value, are mistaken. You could even be right. But never use it? It's always the wrong choice? Get outta here. I'll use it when it helps - when the shape of the problem calls for it.
This involved running FORTRAN programs on what was then considered a large supercomputer (256 nodes.) Asked what he thought about any programming technique that cost a factor of 2 in performance and he told me it was a non-starter when he ran jobs that took a month.
Back when Hadoop was popular, the "cutting edge" of the thing was the system that deserialized and serialized data and some of the major ways to improve performance are: eliminating copies, eliminating memory allocations, etc.
I told Rich Hickey the same thing I was told, that even though the performance difference isn't much in the grand scheme of things, people who have high performance needs are going to dismiss whatever he brings to the table if it costs them a factor of two.
Interestingly, Hickey responded the same way the professor did: just as the professor dismissed the value of the 2x slower but more maintainable system, Hickey just seemed to dismiss the value of "2x faster".
I see this a lot where people just don't communicate, even when they pretend they are.
The position that x2 "isn't much in the grand scheme of things" is a reasonable position. Note the "grand scheme" qualifier. Also the implicit here is "we trade performance for ---". The professor was making a sensible statement about tradeoffs when considering 'general programming approaches'.
Your last line also reads like a potshot at Rich Hickey.
I think the actual failure of OOP is that it did -not- result in the desired outcome of addressing the "software crisis" [1] and the significant imbalance between the scale of required (new) software and available human resources to write these systems. A subset of programmers can use OOP to build very effective systems; the rest shoot themselves in the foot. That is the failure of OOP.
But I am curious if any other software methodology prevents underskilled developers from shooting their own feet. AFAIK the answer is a definitive 'No'.
Years ago I thought "people are fundamentally good."
Now I listen to the fire and brimstone preachers on the radio and every so often it gets to me.
I wouldn't quite say "people are born in sin" but it does seem that people can't really handle computers or carbon-containing fuels or other technologies. So often I meet somebody who is said to have 'good social skills' or who 'cares about people'. Somebody will have a talk with them and tell you that they felt like they were 'listened to' but when I talk with the 'good social skills' guy an hour later about the conversation he had that left somebody impressed it is clear the 'good listener' dubbed over what he heard in his mind with what he wants.
He never gets challenged over this, but this is the problem I see. People talking past each other, not listening. I know one person who can listen to people argue for an hour and repeat back what they said in great detail, but a lot of "high performers" seem to make it through life because their bluff never gets called.
---
The model where software development starts by "determining the requirements" may itself be a "bad smell".
On some level it is all about 'satisfying the requirements', but there is something to say for putting the integrity of the system first.
For instance, say you want to build an airplane that carries a huge radar, such as an AWACS plane. If you were going to build a plane from scratch you would be overwhelmed with "non-functional requirements" such as being able to take off and land, not crash because of ice, etc. You would be driven batty by the people who want to argue with reality about those requirements (e.g. Boeing executives that couldn't reconcile the asked-for-by-customer not retraining the pilot with the 'not crash' non-functional requirement.)
If you have any sense you buy an off-the-shelf plane and stick an antenna on top and you skip the psychoanalysis. You get this
https://en.wikipedia.org/wiki/Embraer_R-99
By "design reuse" you reuse all sorts of validation, testing and experience. Many software projects go at it entirely wrong, getting into the "let's design a whole new airframe" approach.
Having said that, “2x improvement” can also be read as “an order of magnitude improvement (base 2)”, which does become more meaningful in general as x gets large.
If you're just passing around raw structs or arrays, it's way harder to manage blocks of data.
When they're immutable they're not objects, they're just data. There's no need for encapsulation because if you can't mutate something then you can't break its invariants. There's no need for identity or references, there's no message-passing...
As for structs, you could qualify the pointee of a pointer argument as `const` but this is as rare in C as the equivalent is in OOP languages.
This isn't an OOP issue either.
If you want to treat OOP as a tool to use when it's appropriate to the problem, good - but as you gain experience you'll find that that actually just means "never".
There are two things worth mentioning, though.
1) the best pragmatic programming recommendation is "semantic compression" by Muratori [1]. Incidentally, I find it to be an effective "takedown" of the sort of dogmatic oop that anyone criticizing oop is criticizing
2) Muratori might disagree with this, but for me the whole point of a new class (type) is new invariants [2]. If you need a new invariant, you make a new type. Sticking to this principle has helped me keep classes small and focused without really thinking about other things like "single responsibility". There are a few exceptions (eg you might need a new type to work with a particularly clunky API, like some Map Reduce ones), but it's really that simple.
As a bit of an aside, the worst code I have written hasn't been purely OO nor purely functional. It's been when I've mistakenly tried to add new ideas to existing code that break the paradigm.
But I don't get how "semantic compression" is any different from the more commonly termed DRY (Don't Repeat Yourself)?
In Muratori's world they're following best practices like YAGNI and DRY and that allows you to refactor code at the right time
Agreed, but is that OO?
From [2]:
> Class invariants are established during construction and constantly maintained between calls to public methods. Code within functions may break invariants as long as the invariants are restored before a public function ends.
That just sounds like a race condition with extra steps.
Beyond that, I'm not sure I understand your question.
Unfortunately, this "bastard OOP" is the more popular variant, with many "bastard OOP" examples from the late 90s and early 00s running around textbooks, polluting the minds of student programmers at the time. This article does a good job poking fun at it, with "class Dog extends Animal", which is complete nonsense.
Actually learning SOLID principles, as well as the historical 80s style of OOP, goes a long way towards fixing problems.
Just let it rest and move on. It's just a waste of time for everyone involved.
Otherwise, whether or not a procedure is a function is about as clear cut as you can get. It either is or it isn't, it's not subjective.
Definitely.
If it returns a single value then it's a function. If it does not return a value, it's a procedure. (Pascal)
All procedures are functions (C)
All procedures and methods are functions (Swift)
A Function procedure is a series of Visual Basic statements enclosed by the Function and End Function statements. (Visual Basic)
...
[EDIT]: To clarify I am not advocating for one or the other. I made another post in this thread talking about this a bit. GP's comment is able to be applied to basically any technology, language, or philosophy. OOP and FP are just the two "heavyweights" in this.
If you're following this paradigm, for all your core objects they:
-Will be immutable, meaning calling methods on them will not change their state. (except for potentially some transparent optimizations, eg in-memory caching)
-Should not have methods that have side effects (i.e. purely functional)
If this is the case, then choosing whether your classes are objects or simple data structs is only a matter of code organization. ie do you want objects to carry around their methods with them, or have those functions live separately.
If you want to share method implementations among objects, you have the option of using inherited methods/types for a more OOP solution, or the typeclass pattern for a more FP solution. (Though the typeclass pattern has significant syntactical baggage in Scala)
"OOP is good when you use the object oriented structure as a tool to organize your code and data to solve a problem, but not when you try to wrap your solution or your conception of the problem itself around OOP."
It's 2020, and after 50+ years of programming language research I think it's safe to say that there is no "one language to rule them all" or "one programming paradigm to rule them all." Different approaches seem to excel in different areas.
OOP seems to excel when the problem involves modeling systems with discrete parts and/or where there is a lot of state to manage. In other words I'd consider OOP for a "state-rich" problem domain. I am not saying this can only be done with OOP, just that it's a viable choice. There are multiple approaches to most problems.
I'd also say that bad code tends to develop different forms of badness under different paradigms. Bad OOP code is massively over-engineered, verbose, and slow. Bad procedural code is spaghetti. Bad functional code is impenetrable "write-only code" that can only be understood by its author (maybe). Bad code in multi-paradigm languages tends to have all these forms of badness.
Because of this, many game engines are built in a way that allows high throughput for processing the state of hundreds, perhaps thousands of entities in the world. Our guiding paradigm could be described as an entity-component system; another word used to describe it has been "data-oriented design." Here's a talk about it by the guy who was our engine director at the time[0]. It is not object oriented, and it seeks to unshackle itself from many of the issues with OOP as a guiding principal.
Absolutely! But when OOP was initially sold to the masses it was marketed to be that one solution to rule them all, OOP was the buzzword of the day and it stayed like that for some time to come. It is only fair that the king was dethroned to make room for equally good/competing paradigms.
I don't think OOP is bad but I've seen codebases where it become needlessly more complicated and complex than it should have been.
The author is absolutely right in claiming that OOP is not bad if certain things are avoided when: objects are exactly not paralleling the real world, avoiding inheritance when not coding a framework/library, not creating objects unnecessarily, not overusing design patterns, etc. But since it was heavily marketed those things could not be easily escaped from and the OOP gurus kept on adding to the list. OOP is not bad but it deserve its bashing for the hype it took the world over with.
I agree it is a tool but since its definition (or multiple definitions because it appears there are many conflicted OOP philosophies out there) is not very clear and it leads to a lot of needlessly confused ways of using it.
If it weren't so hyped in the first place maybe it would have evolved in a more harmonious way and it wouldn't be the subject to bashing now. I personally can use it for my projects, but I am in no way inclined to save it from bashing, it did bit me multiple times when my intuition was telling me otherwise. I guess what goes around comes around.
Why is a paradigm like FP necessarily bad for this? Not all FPs are pure like Haskell, and some exist specifically for the purpose of managing state in sane ways (for example which tolerate concurrency without introducing bugs).
I've read here on HN the idea that FP is appropriate when you can think of your program like a pipe (data comes in, data goes out). To me, significant amounts of state would be antithetical to that. Is that view mistaken?
AFAIK it is theoretically possible to model any program that way, but it's only convenient for some problems.
In my experience (20 years of OO programming and 5 of fp)... Almost all programs are better modeled as data transformations (recursive or otherwise). One time I did come across a data structure where this did not work, but I was not convinced that the layout of the data structure itself was wisely designed (it was tied to a legacy Django ORM layout).
Data is immutable.
Also, I didn't even read TFA. However, I think it's only fair since it's a medium post that requires login.
I also don't think trying to define OOP is fruitful. One can resort to textualism, going back to Alan Kay's definition for example, but this is as bad in programming (how does this definition relate to current practice?) as it is in law.
Also, as an aside, I wouldn't call DRY a 'rock solid foundation' of programming. People do some weird contortions to make code DRY that end up causing my harm then good. Separations of Concerns is far more important. DRY is fine when applied within the scope of a feature, but not cross features. Otherwise you end up playing wack-a-mole when you fix a bug in one place only to make a new one somewhere else.
Seriously why does anyone even publishes there?
In fact I'm already biased against the author just because of his choice. If he can’t even bother not using Medium can I really trust he spent the time to think through what he wrote?
I suppose it's weird to be young enough that you're first exposure to OOP is it being taught in an academic environment. It must make it seem like it was dreamed up like some kind of formalism and spewed into your brain.
But before OOP languages and everything that you describe, people using plain old procedural languages were already coding in an OOP style because it's obviously beneficial. Languages later came along to formalize patterns that people had used for years. For a more modern example you just have to look at the Linux kernel which includes a lot of OOP principles even though it's all just C.
This is the opposite conclusion of Brian Will's Object-Oriented Programming is Bad video.
Since it forces you to think about everything as a real-world "object", OOP often leads to philosophical debates like
Should a Message send() itself?
Should a Sender send() Messages?
Should a Receiver receive() Messages?
Should a Connection transmit() Messages?
With more and more abstract OOP constructs with more and more interaction with the rest of the program, knowing where to put the relevant code becomes more and more difficult. FizzBuzzEnterpriseEdition[1], while it is satire, is quite similar to a lot of large Java codebases that have been through the wringer of alternating cycles of OOAD and code rot. Try to find the right bit of code to edit if you wanted to add a new feature to FizzBuzzEnterpriseEdition. Not very easy.
---
[1] https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
Procedural programming does not, at all, require global variables to make it work.
I'm not a big fan of procedural programming at scale because it almost always seems to assume mutability (by default) and shared state when dealing with concurrency, but let's set those aside.
You use structures to contain what a naive programmer (or a prototype) would use global state for. Instead of:
player_t current_turn; // a global
You do: struct game_state {
player_t current_turn;
}
And pass that game_state value or reference around. The global state can easily be minimized or eliminated from most procedural programs. I've done this quite often as part of improving older programs.While it's possible to do this kind of thing, it's busywork that properly designed programming languages are perfectly capable of handling well on our behalf.
The main feature that OO added to this was dynamic function dispatch via inheritance. The specific function that gets called at runtime depends on the type of the struct.
Inheritance has proven powerful but often results in confusing code. The development of interface-based (non-inheritance) dynamic dispatch in COM and Java, and later embraced by Go and Rust, shows that we can get what is arguably the primary benefit of OO with a flat structural approach.
Complaining about OOP requires an entire object-oriented software stack to post your argument.
So I'm not saying that there aren't fair criticisms of object-oriented programming. But "the case against OOP" is not proven by real numbers and repeated experiments. It's the most successful programming paradigm in history. The evidence for the success of OOP is more overwhelming than for any vaccine. Yet we're stilling debating boogeyman like mercury in vaccines and inheritance in OOP.
Re-writing hackernews in a functional stack might be trivial. But what about the web browser, the GUI environment, the OS kernel? OOP based software stacks are everywhere. And there is no need to re-write anything because it all works fine.
OOP isn't super terrible, but it does mix some good ideas with bad ones. Newer languages tend to not be fully "OOP" but do include some of the better ideas from it. OOP isn't the end-goal of programming, it's a stepping stone.
Same applies to functional programming by the way; a lot of non-functional languages include various features pioneered in functional programming languages.
No, but success brings out nothing but contrarians. You don't get an article on hacker news saying everything is fine and working well even if that is the reality.
You can't make money selling alternative medicine by claiming that medicine works. Newer languages, in my opinion, are going backwards in a lot of ways out of fear of ideas that shouldn't be feared.
Firefox is being rewritten in Rust, which is not oop. Linux kernel is in C, which is not oop.
Eclipse SWT, a very succesful java GUI, uses composition over inheritance.
But I think the biggest problem with OOP is that it encourages a spirit of "eager complication". Take getters/setters as a classic example. Occasionally that pattern can be useful, but standard practice in Java is to never ever create a simple public field. You always create a private field, with a getter and setter, under the assumption that later you'll need to put some special logic in there.
We can't just create some plain functions, we need a Helper Class™. We can't just pass in a function's dependencies as arguments, we need Dependency Injection™. We can't just use a union type, we need a Visitor Pattern™.
I think some of these habits were built in a world before certain simplifying language features were widely available (union types in particular are only recently entering the mainstream). Programmers were traumatized by the limitations of Java and the ensuing complexity of their projects, causing them to enter future projects already bracing for the worst. Simplicity is assumed to be impossible, so you just go ahead and barricade the windows with some up-front complexity in hopes of flattening the exponential complexity curve down the line.
But the world has changed since then. There's a reason multi-paradigm languages are becoming so popular: because implementing real projects without creating out-of-control complexity requires having a wide range of tools at your disposal. In this new world, I think it's really important that some of these patterns get unlearned.
I'll leave you with this Paul Graham post from 2002 about OOP Design Patterns: http://www.paulgraham.com/icad.html
> When I see patterns in my programs, I consider it a sign of trouble. The shape of a program should reflect only the problem it needs to solve. Any other regularity in the code is a sign, to me at least, that I'm using abstractions that aren't powerful enough-- often that I'm generating by hand the expansions of some macro that I need to write.
After spending 6 months on a modern react project without any classes I had forgotten about Dependency Injection™ in OOP land and gotten used to just doing dependency injection by passing in parameters in a function.
I'm really happy React is introducing functional programming principals to new engineers. Obviously though we still find ways to over complicate things (aka using Redux in a 4 file project with only 5-6 pieces of state to manage) but humans will always find a way to do that :)
Local variables in methods are private and encapsulated. Languages had this before and after OO. Plus modules/namespaces/headers/etc.
OO languages added classes with fields, which are variables shared among methods (whether they are declared with the private keyword or not.) The opposite of private/encapsulated.
If the 'private' keyword made a class's state sufficiently private I wouldn't need to worry about the difference between StringBuilder and StringBuffer, and I could pass java.util.Date without fear.
That's not true. Structs and other mutable, structured data objects existed long before OOP. The differentiating factor was that no functions/methods were able to have varying levels of access to those fields; everything was public all the time.
> The opposite of private/encapsulated.
I don't think that's fair. OOP added "private variables that survive past the end of a function", if you want to get pedantic, but I think there's plenty of added value to having this functionality in your toolbox. And yes, technically closures can accomplish the same thing, but most mainstream languages did not have them yet when OOP was on the rise and even then, using them to achieve "lasting, private, mutable state" is, IMO, one of the few cases where the OOP way of doing things is actually more ergonomic and expressive of intent than the functional way. OOP is fundamentally about state, so when you really have to manage some state, it is often the right way to do things. The problem comes when you assume preemptively that you need state in the first place.
That said, there are some general policies that seem to make sense, namely the alignment of your object models with the shape of the business problem you are trying to solve. Perhaps your problem domain has the idea of a Customer, but there are really 3 flavors of customer. Many developers would automatically do base/derived here, but in some cases it makes sense to have these as entirely distinct types. Making this determination one way or another is at the very core of the art of software engineering. These decisions have higher order impacts that are impossible to anticipate or understand until you personally go through hell a few times.
Proper management of state is another major concern. In context of OOP, I find it best to centralize state along independent verticals of business functionality. Models like CustomerCheckoutState, UserSignupState, etc. When you centralize all of the state you wish to mutate as part of one business activity into one object instance, it becomes really easy to manage. E.g. serialize your state to database and back every time the user takes an action. You can also leverage the scoped injection functionality available in many DI frameworks to pass a per-user-action state instance into all relevant business services and UI components.
If you can keep your business fact modeling & stateful domains under control, the rest will likely fall into place.
Including key use cases such as being able to use the provided components "out of the box" and also being able to customize them, i.e. "like this component, but with these differences".
[1] https://docs.oracle.com/javase/8/javafx/api/javafx/scene/con...
[2] https://developer.apple.com/documentation/appkit/nsbutton
[3] https://ci.apache.org/projects/wicket/apidocs/org/apache/wic...
Yes, it works, and yes, there are great systems built with OOP patterns. The same can be said of C++. That doesn't mean they're good, or we should choose it as our tool moving forward.
GUI frameworks like NS/UIKit and CoreData aren't good. They're quite bad, actually. That's why we are moving on.
It's just that the pendulum has swing so far away from OOP in our zeitgeist for enough time that the new audience is feeding a new wave of "X is actually good!" We can see the same with PHP-related HN submissions right now.
Inheritance is terribly overused, and tends to lead to code spaghetti and testing nightmares. Hierarchy is a mental trick we pull on ourselves to make sense of the world, but it rarely works out nicely in systems.
Object inheritance does that well. Finding the parent is easy and is done in a consistent way. The deletion process is done in a consistent, if not ideal, way. That's useful.
If we had proper syntax and semantics for getting a reference back to the owning object, one of the use cases for inheritance would go away. Languages have "this" or "self", for accessing the current object, but lack "owner", for accessing the owning object. If a language offers single ownership, you should be able to find the owner easily.
(Multiple inheritance is just a mess. Most, if not all, of the use cases for that are better done in other ways.)
And more towards how to name libraries, what functionality to put functions into and how to name my files.
OOP is just a tool to organize code. People give it too much thought sometimes. I did too.
Once you get to application code there aren't a lot of is-a relationships but inside operating systems, libraries, and frameworks it does come up legitimately.
The other benefit of inheritance, that often goes unmentioned, is the ability to fix bugs in other products. I've had to inherit from some library/framework class to fix a bug in that technology -- it might be a rare situation but it's absolutely invaluable to have that option.
1. When I have something that is something else with minor modifications that my and everyone else's lives will almost always be better when if I solve the reuse issues with some composition and the is-a problem with an interface.
2. That when I inherit from a class I don't control to fix a bug in that class the fix is both very fragile and usually very short lived. Either way I created a problem for myself later on down the line.
I don't disagree that sometimes the framework or library you are using give you no other choice than inheritance. In those cases using inheritance is your only and therefore best option. However I don't consider the framework to be better for it. I consider the framework to worse off for it.
I'm not sure how that's better -- you're just implementing inheritance with more steps.
> is both very fragile and usually very short lived.
It is and I know you'd make that point but it's better to be fragile and short lived than completely impossible. I have a least one of these hacks that was completely necessary that has been in place for years.
Particularly, Java's AbstractMap is a thing of beauty in the amount of code it saves you if you need to make your own map data structure for whatever reason. One method override and you have all the Map functionality.
Otherwise, I agree that it is an Anti-pattern like 99% of the time. Young devs tend to reach for inheritance almost out of instinct whenever they have code they want to share. It's a hard habit to beat out of college grads (Honestly, some of our senior devs haven't learned that lesson :( )
It may not be the best way of abstracting above procedural programming, it's not suitable for everything, it's easy to use poorly, but it's there and it does have benefits and can exist peacefully along side other paradigms.
These directly relate to some of the core features of C++ like namespaces, virtual functions, and templates.
I think this perspective makes the differences between C++ style object orientation and Smalltalk style object orientation understandable.
Vocal proponents of languages will be on the more intelligent end of the spectrum to begin with, so this economic / structural "advantage" of OOP isn't as apparent, especially since such advocacies revolve around idealism and the hidden biases of it paying their bills.
They're doing singleton service patterns in Spring Boot.
F# and Scala also have both OOP and FP features - something else nice to see would be the same project written in both OOP and FP styles in the same language.
Anyone have any links to repos or blog posts along these lines?
The author is in desperate need of an actual understanding of OOP, an understand of what OOP looks like in practice, and the most basic understanding of other programming paradigms (or at least a rudimentary understanding of what’s meant by procedural, structural and functional programming.)
I also really liked C#’s “single inheritance plus interfaces”. That was also an eye opening moment.