This doesn't speak to what I consider some of the most dangerous parts of OOP, which include the assumptions that tightly binding data and code is helpful, and that statefulness is fine to freely sprinkle throughout your program.
"Tightly binding data and code" allows you to maintain complex invariants via code, and to abstract away from the specifics of any single implementation. Statefulness per se is quite manageable; what's not manageable is shared, mutable state that isn't isolated to a single, modular, highly cohesive "unit" of code that can be understood in its totality. The real issues with OOP have to do precisely with things that break these principles, viz. implementation inheritance.
> what's not manageable is shared, mutable state that isn't isolated to a single, modular, highly cohesive "unit" of code that can be understood in its totality
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.
You can have an address struct, and functions that manipulate it in controlled ways (like always updating the state when the ZIP changes), in every language, whether OOP or not.
It’s an OOP technique, yes, but functions operating on structures predate OOP and are used in practically every program, so they’re not enough to claim that a program is written in OOP style without the term losing all meaning.
For me, true OOP requires some sort of inheritance and/or runtime method dispatch, at least.
OK, I'll go along with that. The description given doesn't contain a full "implementation" of what OOP is considered to be.
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?
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.
This is generally accurate, but I want to highlight the awesomeness of opaque structs. In C you can declare a struct in a header file, but not its fields ie struct point;. Then in the corresponding c file you can write out the whole definition ie struct point {x: float; y:float;};. Any code that includes the header can pass pointers to struct point (or pointers to pointers, etc) but can't directly pass points around because the compiler doesn't know their size.
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()).
Isn’t this the way C++ operates under the hood, with the “this” pointer hidden?
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.
This might sound stupid but I have ONLY ever known OOP in the Ruby / C++ / Java sense before but this idea blew my mind as such an interesting and different way of thinking about it and I can't tell if it is just my own bias but my initial thought is that it seems like such an additional level of mental overhead that you would always have to carry around on top of everything else you already have to think about. I assume that becomes easy with time?
It's the opposite - you can spend less time worrying about the details because you know they're opaque. It's like how in Java it can actually be easier to work on a generic datastructure (e.g. a tree) than a more concrete implementation (e.g. a tree of integers) - the generics force you to cleanly separate the structure-specific parts and the value-specific parts and not get them mixed up.
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.
For what it’s worth, with ADTs and non-exposed constructors you can achieve the same result (i.e. only the creating module can actually _build_ such a list but other modules can read the data).
"Abstract data types and non-exposed constructors" is enough to give you object-based code, which is not too far from a definition of "the good parts" of OOP. Though some could quibble that "object-based" doesn't include sensible ideas such as composition, interface inheritance, delegation etc.
Classes can be used as modules, as namespaces, as datatypes, for encapsulation, etc. The problem is they're not particularly great at those things, and those concerns often become conflated.
Sure, there are usually equivalent mechanisms in all languages, because it is a good pattern. And yes, especially in OOP purism, it can also go over board, but there are absolutely use cases where tightly binding data and code is very useful (whether you do it with private members, ADTs, forward structure declaration, name mangling etc is less relevant, I think).
I think people in general are pretty OK with the data hiding part of encapsulation. Scattering application state across objects as part of encapsulation, and polymorphism and inheritance are the source of a lot of debate though. Bob Nystrom wrote the best description of the polymorphism vs. pattern matching tension I've ever read [1], and I think problems w/ inheritance are all pretty well known (method resolution, programmers becoming taxonomists, etc.)
Actually my response would be that you just have a plain-ish vanilla list of `Component`, and a function `TotalCost(List[Component]) -> float`, which is implemented as, approximately, lambda arr: sum(x.cost for x in arr). This maintains the invariant without state.
There's a potential performance cost here, but that usually doesn't matter a whole lot.
That's true - but where do you put the function? In a complex system I can end up with loads of these totalling functions scattered about. I might have gross and net versions of the total and, given human nature, they get created near the use of them. The variants then get duplicated.
The objects give you a modular structure which means you can see the interface.There is a disconnect when you have to shift to the object - but you can also see when to refactor.
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.
That's true, although it raises a question: is the maximum you're willing to spend a trait of your bill of materials (I'd argue no), or of some other thing, for example your overall project. In that case you could have a method that validates your BOM against your project requirements.
A good example would be a complex state machine, where you rely on the fact that the state is only modified by the state machine code to make sure it remains well-formed, and the control-flow itself heavily relies on the state being well-formed (think of a TLS session, for example).
But then again, what you want there is modularity, which OOP provides, but is also well-supported in other paradigms.
You don't need OOP to do anything! But in the state machine example, it provides a coherent and readily understandable metaphor (object modifies its own private state; callers can tell it what to do but not how) for the task at hand. Isn't that the goal of a programming paradigm?
I was about to write exactly this. In 2014 I wrote an essay that has been discussed here on Hacker News several times [1]. When I re-read it now, parts of it seem very subtle, parts of it seem to be matters of opinion, but what jumps out is how really dangerous it is to tightly bind data with behavior, especially because this then leads to the problem of initiation (which I devote a lot of time to talking about in the essay).
[1] Object Oriented Programming Is An Expensive Disaster Which Must End
Thank you for this. I am so tired of this exchange:
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.
> 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?
Is the problem better than 80% with other paradigms though? Even after controlling for "only smart programmers bother working in less popular paradigms"?
A typical microservice tightly binds code and data, and it communicates via messages. So when a microservice is useful, it's an example of binding data and code being useful.
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.
Eventually data has to be bound to code. Otherwise you'd have apps where you either have functional code running and other apps that are displays of raw data.
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.
Declarative programming is also an option, where data is directly encapsulated into state, and programming entails describing the state transitions. SQL is a basic example of this, but it's also generally possible in other languages (eg template metaprogramming in C++). Prolog is a really cool language that operates solely on this principle.
Yes, it does seem like OOP and functional are on opposite sides of a spectrum. OOP is all about having and controlling state, while functional is about being stateless. I think my own code would benefit from being more functional, but that's hard to do in a OOP infrastructure.
Tightly binding code and data works as long as a useful abstraction is created. Keeping the structure of a person and how to interact with that person together is a fine paradigm. Usually the issue with OO is that the abstraction isn't there and it is just half the functions to interact with the person and/or there is an expectation that certain fields are manipulated by calling code instead.
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.
I think the key differentiater is that OOP (in some versions) wants to hide the data and not just implementation details.
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.
Hash maps hide lots of internal state though. I think the difference is hash maps have a clear, battle tested API and its obvious what should be promised by the interface and what should be internal implementation.
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?
It really isn't, as OOP languages don't traditionally provide robust mechanisms around immutability. And why would they? The core design of OOP is to wrap mutable state behind methods which control that mutation. OOP was in many ways a reaction to C; shared mutable structs are harmful, so let's invent privacy.
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.
Hash maps also don't (usually) have external effects.
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:
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.
I think the problem is that your example is too contrived and you can hide implementation details by just having a function giveFinanceDepartmentARaiseInDollars(company, dollarAmount) that exhibits the same issues as the “object oriented version”.
More generally the issue of global mutable state and keeping your own sanity as a developer is a problem across paradigms.
The real problem with my example was that I modified the strict instead of returning a new one.
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.
Creating a new value only gives you a stale data problem if you had pointers to the old value. You don't have to have pointers like that, it's a choice.
To take an obvious example, you might load a value from a database (company or employees, say), create a new value (with the higher salary, say), then write the new value to the database.
I think the point being made here was that alternative approach don't eliminate the difficulties of global state. Simply using immutable object doesn't make all your problems go away, it makes them different. I think it makes them better, but let's not pretend that all the challenges of dealing with state can be eliminated.
> using immutable object doesn't make all your problems go away, it makes them different
Yes, and here is a concrete example of the kind of 'different' problems you get: nested immutable structures, say `a.b.c.d`, become harder to update than simply `a.b.c.d = newValue`. The functional solution for this is 'lenses' (and other 'optics'). My favourite Kotlin framework that I use in my day job provides lenses for HTTP - URL query parameters, request and response bodies and so on - which work very well.
'the data' is a vague concept. Usually, a hash map doesn't expose, for example, the bu met array, or the length of the bucket array. Sure, you can ask the hash map to give you the contents associated with a hash key, but you can't access the entire data structure that the hash map represents.
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.
A hashmap wouldn't hide the data. But say, a Customer object might. A purist OO might insist that you shouldn't have a GetName() method because any code which needs the Customer's name should be method on Customer.
This particular example is probably a bad illustration, with all the recent rage about PII and privacy. In some cases (such as this) you want to make sure data doesn't leak all over the place and anything accessing it follows a strict path.
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.
The data is accessed through an interface, a set of functions associated with the data structure. You don't dig through internal memory structures to get the data - you call these functions. That is what "hidden" means in this context.
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.
Almost all hashmaps hide the internal references/pointers to the backing arrays. That's implementation state just like any other internal data. It's just so mundane, well executed and common that you forget it exists.
Separating functions and state has nothing to do with the type system. Otherwise how could you put types on the parameters to static functions? Check out F#, OCaml, or Haskell for plenty of examples of how you can have strong static types without mixing state and functions in a scope together.
>Otherwise how could you put types on the parameters to static functions?
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?
It is important to note the difference between state and mutable state as I believe you are conflating the two here. There is no reason that you cannot have structs.
You should have data structures, but not mutable state or reference identity (two sides of the same coin). Having a bundle of structured data with functions is great. Having those functions secretly change other data somewhere else in your application (you wanted a banana but got the gorilla holding the banana and the whole jungle) is not great.
OOP was a (too) big response after an era of semi-wildness.. I believe nobody mastered the whole cake and it was rarely applied fully. Most of it is simply mental logistics convenience (most related methods are one dot away) and of syntax. car_start(&car) is noisy; car.start() a tiny bit less so.
So what is your preferred solution for programs that intrinsically need to deal with a lot of state ? Global variables ?
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.
Ideally you want to have all your state in one place, the "single point of truth", in tightly modeled data structures, keeping it as lean and immutable as possible. The program is then just a function of the state, consisting mainly of self-contained functions, that take arguments and produce a result, without causing side effects. Realistically, that doesn't mean there are no classes or objects with internal vars, but these are more of ephemeral nature, and can be created predicatively and deterministically from your centralized global state. In the best case you can have complex applications with tens of thousands LoCs that feed from just a few dozen state variables.
Essentially, yes, but in a way that sounds more careful than what you saw in that Fortran program. If it absolutely has to be stateful, like with a db connection for example, then there's no getting around that, but what you can do is (1) not bury it in a bunch of stateful objects that didn't have to be stateful, which can cause cascading errors when either one of them misbehaves and also makes it difficult to isolate any one stateful component for testing or re-use, and (2) use special wrappers around it that will make the statefulness easier to deal with. (For examples of (2), see Clojure atoms and the Clojure library mount: https://github.com/tolitius/mount )
More concretely, for every bit of state in the program I'm interested in two big questions, each with two subquestions:
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.
> 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.
You can do this, but unless all the intermediate types are auto-generated by the compiler, it's going to be a lot of typing (of the keyboard type). Many structs that require multi-pass initialization have many, many fields. Each field that isn't late-initialized is going to be duplicated for each following pass. That's the type of maintenance headache that compilers are meant for.
If you have higher-kinded types and a working record system you can do it by transforming your existing types. Even in more limited languages you might be able to make it manageable with phantom types, e.g. Java's "type-safe builder pattern".
Pure functional languages are capable of this, although they likely aren't modeling it as you envision. Pure record constructors let you do perform arbitrary mutation on the stack (e.g. calling other functions to build field values), but once the constructor is called the returned reference is immutable.
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.
It's the consensus among languages, but I've got a pretty large collection of application code that does need to partially initialize in passes, and there's also a fairly long list of escape hatches in languages (eg. lateinit in Kotlin, friend in C++, builder pattern in Java) to let you do this. Languages are supposed to capture common patterns; I'd argue this is a common pattern that isn't captured all that conveniently by existing language mechanisms, though a number have tried.
That would be really cool, but I think enforcing that is undecidable. My gut tells me that having that language feature at compile time is the same as the Halting Problem.
In its full generality it's undecidable, but there are probably restrictions you can build into the type system to make it decidable. I was thinking something along these lines:
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.
I suspect that the ideal language (at least for human use) is one that is just shy of Turing completeness. Once a program compiles and an initial configuration phase has passed all loops would be bounded and all recursions would be guaranteed to terminate. Except for the case of programs that aren't supposed to terminate for which there would be a single construct allowing wrapping everything inside one and only one infinite loop.
(2) sounds very similar to the way I have typically handled db access in C++. However the kind of state I was referring to is more about the internal computational data used by the program than about external resources.
You might want to consider examining another programming language for clues here. Perhaps look to a language that has a more modern approach to state such as Clojure.
My understanding is that Clojure is basically lisp implemented inside the Java ecosystem. I'm not sure how that makes it "modern" given that the only higher level language older than lisp is the original version of Fortran.
That like saying Go/Rust/Kotlin are just Algol, what’s so modern about them? Lisp isn’t a language anymore, it’s a category of languages and it’s members are rather different from each other besides some superficial syntax style. Common Lisp, Scheme and Clojure are three very different languages, certainly more different than Ruby is from Python IMHO.
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.
Clojure has quite a few specific libraries. As far as I know, one of these implements a lot of persistent data structures - data structures that make it efficient to expose an API that returns a copy of the whole structure for any mutation (by sharing all of the unmodified substructure behind the scenes).
Yeah imo law of demeter shouldn't apply for namespace organization of static, globally relevant functionality. The amount of dots in that case is irrelevant to me because it doesn't really imply changing context in the same way. more like drilling down in a phonebook than calling your neighbor and telling them to call Bob for you
The entire point of OOP is to encapsulate state within objects and to have objects communicate through messages, that is to say to 'program without a center'. Global or interconnected state is if anything a violation of OO principles.
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.
The problem with lookibg at bad X code and concluding X is bad is that X can be anything.
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 problem with lookibg at bad X code and concluding X is bad is that X can be anything.
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".
I think that there are two groups that try to claim OOP.
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.
I think you miss the point. The behavior and state of an object in OOP is the aggregation of the code and state of its classes (including ancestors), mixins (if applicable), and members.
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.
I'm not sure you're responding directly to the point the parent made. Even if state is encapsulated, objects are mutable. Many people believe reasoning about programs in which lots of mutable objects coordinate is harder than some of the alternatives (e.g. pure functions, immutable data etc).
depends on the domain. In anything that has say, a very structured 'data flow' in one direction, like in finance or accounting or versioning immutability works well as a concept. In other domains like say, game development it feels forced and out of place.
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.
Is it a wild simplification to suggest that the inner loop of every game is the state now, some processing at the current time step, and a new state to be rendered?
game development seems to be moving away from OOP and towards entity component systems and data oriented design. i've heard arguments that OOP's killer application is desktop guis, which i could buy, but those become less and less relevant daily. (caveat: my experience is entirely in back end and low level software and anything i say about front end or ui development should be highly suspect)
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"?
I think the point was that game development is an inherently stateful domain, which ECS recognizes (if anything, ECS is more stateful than OOP, not less). In contrast, using a pure FP language would probably create a lot of friction with many common styles and even algorithms used in games programming (think about how ugly and out of place actual quicksort looks in Haskell).
You can still program in a data oriented manner using OOP. It is after all just about watching data access patterns in view of the hardware constraints and organizing along those lines.
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.
entire point of OOP is to encapsulate state within objects
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.
I think "tightly binding data and behavior" is a less-ambiguous way of phrasing the pitfall. Obviously code goes great with data; no one wants to work with strings as raw buffers all the time!
If your program has statefulness (worse, has to have statefulness), then it seems better to control where it is (encapsulate it), rather than letting it run all over the place in the program.
And, if you have code that is for working on a particular kind of data, why is binding it to the data problematic?
> This doesn't speak to what I consider some of the most dangerous parts of OOP, which include the assumptions that tightly binding data and code is helpful,[...]
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.
Agreed. The biggest problem with OOPS lies in its original premise: that code and data should be the same thing!
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.
I'm pretty sure what was meant by 'code is data' is that a lot of OOP training encourages you to "bundle the code and the data in an object". People take that to heart, and...bundle code and data into objects. Whether or not those functions and that data really belong in the same object is an afterthought, if considered at all.
I'm not sure "code is data" necessarily means "homoiconicity", but it could mean "first-class functions" (or at least that's what I think of when I hear the phrase; maybe I'm wrong); however, I'm not familiar with anyone who argues that these are fundamental OOP features (perhaps Smalltalk had first class functions, but for a very long time the most popular flavors of OOP did not).
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.
I understand the differences between homoiconicity and first class functions and even that modern languages often support them, but that support is typically an artifact of a functional (and not OOP) influence with the exception of function pointers in C whose origins im not sure about except that it doesn’t come from OOP. The point is that despite the confusion about “what does OOP mean, really”, there are no prominent arguments that suggest that homoiconicity or first class functions are defining characteristics.
I think both conceptions are important. On one hand, some code is naturally related to certain data and hence we should accordingly design the program. On the other hand, complex and distributed programs have code which must be dynamically bound to different data. OOP focuses on only one aspect but does it very well. How to adequately support the opposite aspect is not clear. At least there are many ways how this can be done.
Since I accepted I was no Einstein I embraced the KISS principle.
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.
Einstein himself would approve this philosophy. As he once said, "Everything should be made as simple as possible, but no simpler."
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.
An idea I carried over from mechanical engineering is the idea of degrees of freedom. If your solution has fewer degrees of freedom than the problem, it doesn't work. If it has too many it's unconstrained and requires hacks to keep it from doing something terrible.
The problem with "simple" is precisely the problem of removing what is not necessary. In most commercial software design, we tend toward making designs so flexible we make them tougher to reason about than necessary. One of the other problems in software is that software that is good is also the code that is easiest to understand and therefore replace. This doesn't happen so much with things like houses evidently.
This so many times over. OOP encourages clever implementations, I believe. That is, when I and others use it, I often think about how clever it is. Until I need to debug it and fix it. Then I start cursing.
Amen. I think the mistake is to reach for the complicated patterns first. Rather than refactoring later when it’s obvious they are needed to simplify your code.
I like the theory behind app architecture, but I haven't seen a complex design pattern work well in practice (unless the team is small, and everyone has a solid understanding of how it works).
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.
OOP can be useful and simple like that. Typically when it is applied to things it solves well.
---
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.
In my opinion, a big thing many developers forget, especially those that are obsessed with design patterns and code rules, is that simple machines to solve complex problems are actually harder to design than convoluted ones. Leave a designer to their own devices, they immediately start spitting out way to many levels of indirection, it takes real thought to collapse those levels of indirection to something simple. Many less-experinced devs think that if they keep slapping on more layers, hide the problem more, more abstraction, they will reach some sort of inception-like base-case nirvana where the complex problem will magically become simple. But they are running the wrong directions. Simple, powerful, abstractions that make great tools great are generally pretty thin. They don't mask the actual problem, instead they provide good hand holds on it. C is a decent abstraction of assembly because it simplifies it, not because it hides it. All that being said, OOP, like many patterns, is perfectly fine if it makes a good abstraction. There is no one great pattern to rule them all.
> The solution to the fragile base class problem and other inheritance hangovers is surprisingly simple — don’t use it
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.
Personally, I think this is a large part of the problem with the whole "Is OO Good?" debate. OO was initially thought to be a solution to essentially simulations problems ("Simula"[1] - the first OO language - it was right in the name!), where Cat inheriting Animal makes sense.
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.
"The pit of success" is a great guiding principle when it comes to the design of codebases. Forget about "design patterns" and just focus on _how do I allow developers to focus on creating things that are adding value, rather than faffing around with extraneous things_.
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.
For large interwoven software systems this complexity seen in the form of service layers, directory services, distributed quorums, etc. is often necessary to achieve availability guarantees. I think that the problem is that engineers are over eager to design these sorts of things and implement them before they're actually necessary. And they do it badly and make life shitty in the process because they still don't fully understand the requirements for such a system. OOP just happens to be the language feature they use to make a mess. It would still be no fun even if they used a straight procedural language like C.
Adding on to this, there's an important distinction described by Parnas back in the 1970s as
> 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.
OOP is conducive to premature optimizations. It hints for you to find the right abstraction for future problems, because that's where the productivity gain supposedly is. Instead of rebuilding cars from scratch every time (in the future), I have an interface.
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.
The onion anti-pattern is Separation of Concerns taken as mythology, not practicality.
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 problem I've always had with these back-and-forths between various programming philosophies is that the very nature of the argument exposes the reason the arguments don't work.
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_.
So let's say I've decided to sit down and write a digital audio workstation. I've got to pick at least one programming language and at least one general programming style. Let's say I pick C++ and (somewhat by implication) a generally OOP style.
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).
The other related observation I make is that our field suffers from dogmatic thinking. A methodology is a tool, not a holy calling which cannot be altered: if it's working for your team, it's good; if not, adjust to something which works better.
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.
The author is right about ORMs. They are ridiculously over engineered solutions. (My experience is largely with Django and SQLAlchemy ORMs)
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.
> As soon as you want to do something that's not already perfectly built in, everything becomes a mess of impenetrable hacks.
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.
I like certain aspects of Django, but Rails got models and migrations better. Writing raw SQL for everything gets old, and I'm someone that loves SQL when it's the right call for the job.
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.
I get that, and I'm comfortable writing raw SQL but as the system grows larger and requirements change I inevitably run into the fact that raw SQL doesn't compose. For me the query builders hit that sweet spot of mapping almost one-to-one to raw SQL, while giving me composition when I need it.
Or you could just create a View for flagged posts and join on that as appropriate, no ORM needed.
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.
Always prescribing raw sql is dangerous: You typically want something managing your sql queries if there's user input because scrubbing out SQL injections is hard and it's not something you should be 'rolling your own'
I was just reacting to this, which is confusing given your response.
> 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.
Having used Django's ORM, SQLAlchemy and JPA (via Spring Boot), it depends on the ORM. E.g., JPA's automatic query generation[0] is very convenient while still allowing plenty of options for handcrafted SQL, if needed. I'm the kind of person who enables logging of ORM generated SQL so that I can see exactly whats going on under the hood; so far, many hours saved with very few complaints. This assumes a well defined database model - if that has issues, all bets are off.
Of course the biggest gain of ORMs is initial dev speed.
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".
Yeah, why spend hours on the most important part of the application and likely the most important part of the business when you can gloss over with with an ORM and not worry about designing the database until it comes back to bite you in the ass when you can no longer grow your business because of the incredibly stupid thing you did which is not designing your database. Frankly, those businesses deserve to fail if this is the kind of idiocy that drives their decisions. But hey, we got the app going really quickly before going bankrupt.
DB design is not the most important part of a business nor the application. This is a very misinformed view point.
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.
None of those things can even remotely compare to the importance of the database design for most software businesses, especially ones just starting out. Most of them can also be addressed later in the company's life unlike a bad database design.
This is a pattern I've used before with dotnet, and I think it works quite well.
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.
The author flatly says "do not use inheritance." Without inheritance and polymorphism, what is left in OOP? If you look at the author's 4 pillars, the only thing left is encapsulation (and "Oversimplified Hot Takes") It seems like the author himself has proven why OOP is bad. You can have encapsulation without OOP. People were doing it in C 40 years ago. And people are doing it in Go right now. No one would call Go an OO language, yet it has what the author admits is OOP's only good feature.
You can have polymorphism without inheritance. A single class can implement multiple interfaces, and multiple classes may implement the same interface.
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.
Is OOP just syntactic sugar once you remove bad practices? I don't think so, but there's definitely an argument there. But on the issue of inheritance, what I'm trying to say is "not a good idea for the average business developer, but still a great tool for careful, ambitious developers designing the OO frameworks that business developers will use."
Yes, you could do encapsulation in C. You'd have a struct. But the problem is, any function in the whole program could modify the data in the struct, possibly placing it in an invalid state. When the struct wound up in an invalid state, you had to look at the entire program to find out who did it.
(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.
I never said encapsulation was easy or obvious in C, just that it's nothing new or inherently OOP-based. I also never said that you should never use inheritance, I was just quoting the author, who, in his "defence" of OOP admits that OOP's only unique feature should not be used at all.
IMO, OOP just puts too many footguns at your disposal, the most dangerous one being mutability (how normal is it that our methods mutate instance variables?). The larger a system grows the more mutability will make it even harder to understand and control. It may be tolerable in small-scale embedded software, but outside of that we should make use of the better alternatives that modern hardware affords us.
I knew a professor who worked on Density Functional Theory which can be used to derive many of the properties of materials from first principles. (e.g. "Does Iron Conduct Electricity?")
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.
HPC obviously prefers higher performing approaches, but how is this anecdote relevant to general programming community?
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
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.
Yes, HPC is certainly a separate domain as much as embedded is. You cannot transfer (what I would call) conventional wisdom and expect a reasonable outcome or a working system at all.
Part of the communication problem is that “high”, “performance”, and “needs” are all relative, which makes the combined phrase super-ambiguous. If you reach for a thing that’s advertised as “2x faster” because it means you’ll get your answer in one month instead of two, I think reasonable people would agree that’s a reasonable optimization in that specific case. If you reach for it simply because it’s advertised (perhaps not reliably) as “2x faster”, that’s more of a premature optimization if you don’t know along what axes, or under what conditions, that speedup obtains.
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.
Clojure isn't supposed to be everything for everyone. Did Hickey really dismiss the value that 2x faster had for the professor, or was he just not interested in that particular use case for himself?
If that's his attitude then Hickey is right not to bother. Suppose Hickey puts in months of efforts and gets it so that you can apply the same technique with only a 1.2x performance cost. The professor is still going to dismiss it for the same reason.
Calling mutability an OOP issue doesn't make sense to me. You could certainly have immutable objects. You can event have immutable object inheritance. Maybe you just want a language where fields are immutable by default.
If you're just passing around raw structs or arrays, it's way harder to manage blocks of data.
> Calling mutability an OOP issue doesn't make sense to me. You could certainly have immutable objects. You can event have immutable object inheritance.
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...
Obviously mutability is not an issue in OOP alone. Note, however, how I said, “how normal is it that our methods mutate instance variables?”. A well-skilled programmer can always choose to not use the footgun a language hands them. But those are not the only people to consider.
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 might be because almost all languages that support OOP are mutable, which goes all the way back to Smalltalk and CLOS, and becomes popularized with C++ and Java and then the dynamic languages like JS, Ruby and Python.
Article doesn't even make an argument. If you're not modelling things with objects, and you're not using inheritance, in what sense are you doing OOP at all?
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".
Pretty good takes here in the comments (shout out to references to "pit of success")
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.
> If you need a new invariant, you make a new type.
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.
It seems to me that the definition of OOP is so unclear as to make the term useless. It seems to me that lots of really bad ideas were spread under the guise of OOP. But now OOP advocates are shifting the definition so that OOP either excludes are doesn't explicitly advocate those bad ideas. I have to wonder: what exactly does OOP still advocate?
There's this "bastard OOP" where "ellipse isa circle" going around which completely doesn't work in practice. But there's "true OOP" that follows SOLID principles (particularly LSP) that actually makes a lot of sense.
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.
Might I suggest, when posting Medium premium stories to Hacker News it would be best to use the "Share Friend Link" link so people can avoid the pay wall (it doesn't affect author earnings). That seems to be what it is for: promotion outside of Medium itself. (This only works for the author so doesn't help others, but I wanted to mention it to raise awareness)
IMHO if a programming philosophy leaves so much room for interpretation and misunderstanding, rants and counter-rants, needs evangelizing to the "dumb masses" what it "actually" means, and all of that hasn't been settled after more than half a century, then it has turned from a philosophy into a religion (or rather, a cult).
Just let it rest and move on. It's just a waste of time for everyone involved.
The only real contention seems to be whether "functional programming language" should describe languages that merely facilitate functional programming (e.g. lisp), or only to those that enforce it (Haskell.)
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.
in as much as there is controversy about what OOP means, yes -- OOP has an established definition, same as FP. The argument about "what it means" comes from the arguments between OOP and other philosophies. People often will purposefully (or accidentally) misrepresent "what it means" to strengthen their own point (or through misunderstanding of the topic). The same happens with FP compared to other philosophies.
[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.
After working in the Scala, which blurs the line between FP and OOP, daily for years, I feel like the choice to use OOP or not tends to boil down to how you'd prefer to solve the Expression Problem [1] for your domain. (if you follow the paradigm of "Functional Core, Imperative Shell")
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)
That sounds snarky. Not sure if that was intended or not, but regardless I read your comment as:
"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.
I program video games; I would consider running a real-time simulation as a "state-rich" problem domain. And the constraints of running real-time simulations mean that some implementations are not acceptable. Things are not generally allowed to be slow if they can be done in a way that is fast.
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. And the kinds of data structures you get in an entity-component system are probably easier and more natural to use in a language such as Haskell than a typical OOP object-graph-of-mutating-objects would be. Not that anyone's programming their ECS in Haskell.
> 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.
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 about OOP being marketed that way. I remember that time. Rejecting it utterly and making it an "anti-buzzword" is equally irrational. It's a tool. Use it when appropriate.
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.
> In other words I'd consider OOP for a "state-rich" problem domain
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).
Which FP languages do you think would be good for managing state?
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?
That's a good way to put it. I've thought of it as "FP is good if you can think of your program as a single possibly recursive data transform, even if it's a very elaborate one."
AFAIK it is theoretically possible to model any program that way, but it's only convenient for some problems.
That's really not true for most working fp-languages. For example Julia has support for fully mutable datastructures, which, you might expect for a language designed for high performance scientific computation first.
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).
i would make a distinction that data and state are two different things and argue that a lot of the mess people create with OOP is due to confusing the two. data is what exists externally to your program while state is strictly internal to your program. inputs and outputs of your program are data and not state by definition. applying OOP principles to data is an unmitigated disaster and is the source of most of the problems associated with bad OOP design. data should be modelled as plain structs and arrays. data should be trivially convertable to and from json. there should be no object/relational impedance mismatch because objects are not data. data should not be bundled with code. there should be no hidden information in data. there should be no inheritance in data (use composition instead). on the other hand, many of these principles are quite useful for encapsulating state in order to provide higher level abstractions. it makes sense to hide the internal implementation details of containers, synchronization constructs, database client libraries, and other abstractions, and to allow for multiple implementations of those abstractions which can be swapped out for different purposes. introducing a new abstraction should not be taken lightly and the vast majority of programs should not be introducing their own abstractions. if the abstraction isn't something you would put in a library and make use of in several other programs, it probably shouldn't exist. in this sense, i think it is fair to say that the article author's stance is that "OOP isn't bad but you probably shouldn't be using it", and that this is a perfectly reasonable statement to make. a lot of die-hard OOP fans would argue that this is not "real OOP" but ironically their dogmatism and inability to deal with ambiguity has done more to fuel the anti-OOP movement than anything else.
If there is anything in CS which should be renamed simply because people have completely incorrect assumptions from the name, it's OOP. Everyone has someone different they think about it, and almost none of it has anything to do with late-binding, encapsulation, or message passing.
I believe OOP was successful largely because of Conway's law. OOP itself is a hierarchical (inheritance) type system that mirrors hierarchical social systems in business. It's very "top down" in its orientation, and also allows teams to tightly control the information they are dealing with.
Also, I didn't even read TFA. However, I think it's only fair since it's a medium post that requires login.
I find the premise of this kind of article is flawed. Not because some of what the article says is incorrect, but because there is no such thing as OOP. OOPs as practised in Java, say, is very different from that in Rust (which the article mentions as having a "slimmer set of object-oriented features"). As a result trying to make generic statements about OOP doesn't really give much value IMO. You need to discuss specific well-defined programming practices to make progress.
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.
Ruby implements the Kay concepts under the hood, and erlang is arguably a rather faithful implementation of Kay objects (though the code one writes doesn't semantically reflect that and it was developed independently for a completely different set of reasons - fault tolerance)
So you fix OOP by taking away the things that make OOP, OOP?
That just sounds like procedural programming with extra steps.
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.
I knew a programmer who was all in on DRY and it was the heart of many stupid debates. Not only did result in a crazy inheritance and dependency tree, there were tons of many useless functions. Like having "logger.log(obj.read_stream())" in a few places would be "WET" the API got cluttered clutter with crap like obj.read_and_log_stream()
I wish I could read this but it’s hosted in Medium and no, I won’t create an account.
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?
OOP is a crazy academic-esque idea that happened to stick because it works at scale. When you have hundreds of programmers working on a codebase, it's useful to constrain people with UML diagrams and rigid hierarchy. Otherwise, cowboy coders hack up something the other 90% of coders can't understand. This is why languages like Scala and Haskell haven't taken off outside of a few small-team-focused niches, like data science.
> OOP is a crazy academic-esque idea that happened to stick because it works at scale.
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.
True, a lot of OOP is stuff that you might develop on your own, if you were writing a lot of code back in the procedural days. But computer science has been with us since the beginning of computers, and certainly OOP as we have it today, with all of it's formalism, probably started back then in computer research institutions like MIT and Stanford.
In my opinion the "killer feature" of OOP at scale, more than hierarchy, it's compartmentalization of code.
Class hierarchies tend to have issues at scale, but OOP makes people agree on "where should we put this block of code" in a way that's "intuitive" and scales.
> OOP makes people agree on "where should we put this block of code" in a way that's "intuitive" and scales.
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.
Usually when I see people rant against OOP, they're not ranting in favor of FP (or even better, in favor of something like Scala that combines OOP and FP) but rather in favor of a return to procedural/top-down programming. There are very good reasons that procedural programming was abandoned a long time ago and the need for global variables to make it work is one of the big ones.
> There are very good reasons that procedural programming was abandoned a long time ago and the need for global variables to make it work is one of the big ones.
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.
That's basically a hand-rolled implementation of a state monad that supports real mutability. The Haskell equivalent is the ST monad.
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.
I had the same thought: he's doing OOP without using the "class" keyword. Well-written C code is actually very object-oriented this way, it just doesn't take advantage of the syntactic sugar of C++.
This is a common misunderstanding. Structural programming like this is clearly similar to what we think of as OO. But it existed before OO and is still heavily practiced in non-OO languages such as C.
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.
This is only one thing that OO languages provide. My point, though, was that global state is not inherent to procedural languages as you claimed. And if all you're getting from an OO language is a bit of sugar and single dispatch, you're not getting much.
It's the most successful programming paradigm in history. I feel like being anti-OOP is like being anti-vax. It's so successful and ubiquitous that it's success becomes invisible and so people only focus on the failures or misuse.
Complaining about OOP requires an entire object-oriented software stack to post your argument.
That doesn't make any sense.
Being an anti-vaxxer is simply stupid, proven by real numbers and repeated experiments.
Meanwhile, there are a lot of fair criticisms to OOP. Of course, a lot of them arise due to the fact that the skill floor for software development is quite low nowadays, but then again if we were all that smart, we'd just write C and C++ at the speed of light for everything.
P.S: Rewriting hackernews in a functional stack is trivial
There are plenty of potential side-effects for vaccines as well. The last time I had one, I had a relatively unpleasant reaction.
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.
That it's successful doesn't mean it's also a good idea. To quote Dennis Ritchy: "C is quirky, flawed, and an enormous success". I feel this could probably be paraphrased to OOP as well.
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.
> That it's successful doesn't mean it's also a good idea.
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.
Obviously being against the common trend is always going to get more attention, but it makes no sense to assume that those people are wrong strictly because of that.
The fact that vaccines are widely used doesn't make anti-vax stupid. Anti-vax is stupid, because a lot of real research has gone into vaccines, and they really do work. The arguments anti-vaxers use are not based on reality, and can easily be proven wrong. The same can NOT be said about programming paradigms.
There is no proof that OOP produces more elegant, simpler, higher quality software with smaller programming effort than other paradigms. OOP isn't proven to work; it's proven to be an attractive choise
Isn't there proof? I mean there are plenty of alternative programming paradigms -- some that have been around since the 60's and still promoted as alternatives today. Where is that success to compare?
Show me that an alternative to OOP is statically more successful and I'll switch tomorrow.
It seems like you really want it out to be merely popularity rather than being simply successful. That's not a rational position.
To the downvoting-without-commenting crowd: The GP's claim is that OOP is "the most successful programming paradigm in history." Responding that there exists a single website that happens to be written in a non-object oriented language, which by all accounts is the only remotely useful thing written in that language, is not exactly a scathing rebuttal of the claim. It's not even relevant, honestly.
It has become popular to hate OOP so now people find every opportunity to discredit it. But yeah, every tool has a purpose. Don't like it in your workplace? Make a fact based case against it and do something about it.
PSA: If you want people to read your tech posts, don't write them on Medium. Medium requires logging in to view their content now, so like Pinterest, Quora, etc they are dead to me. And I know I'm not alone.
Given that the post is ranked relatively highly on Hacker News at the moment, it would appear that there are people who are still reading their tech post even though it is on Medium :)
The best thing I can say about OOP is that it allows for private state that only certain logic is allowed to know about. In some cases, you just really need that.
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.
> 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.
I just inherited a TypeScript project that made heavy use of Dependency Injection™ by using a 3rd party library.
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 :)
> The best thing I can say about OOP is that it allows for private state that only certain logic is allowed to know about.
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.
> OO languages added classes with fields, which are variables shared among methods
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.
I'm saying you can have private mutable state if you stick to method variables. If you take a method variable and promote it to a field (private or not), or return it via a public method, you have made it more public - that's what I meant by "The opposite of private/encapsulated".
Overcomplication isn’t the fault of OOP—or any paradigm. It’s what happens when any programming technique gets a lot of traction. It’s easier to make things complicated than simple. And people tend to use ideas blindly, without understanding the context in which they’re meant to be used.
OOP is basically a blank canvas. It is always up to the artist to bring structure and reason to it. There is never one perfect answer for every case.
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.
People love to hate on inheritance and suggest that there are no cases where its use is warranted. But inheritance absolutely is used, and successfully, in many frameworks. For example GUI frameworks [1] [2] or server-side web frameworks [3]. People have been writing code in frameworks like this for ages and as far as I can see they worked well.
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".
Just because there's examples of successful usages doesn't necessarily mean that it's "good" or even a model for how we do things moving forward. Picking that specific example [NS|UI]Button, they're dreadful to work with. Honestly. Customizing them is difficult to impossible. It's often easier to just compose them into a different container because the extension points couldn't possibly be designed in a way that gives you all the flexibility you need.
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.
Not very compelling. Your point that people have been able to leverage something to build something applies to everything. Doesn't mean it's good nor that there isn't a whole rest-of-the-continuum of things that are better. Look at all the things humans built before 21st-century tooling. So what?
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.
There is a lot that is problematic with OO, especially if it is adhered to with dogma. The worst idea and the one I've experienced most issue with, is the `extends` keyword in Java, and more generally, the concept of inheritance.
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.
One point I've made occasionally is that few languages support backpointers well. When B is a member of A, B often needs a back reference to A. Rust does that badly, because it doesn't fit well with the ownership semantics. C++ with move semantics does that badly, for much the same reason The same thing applies when A allocates and owns B. It's hard to get a clean, safe, reference back to A that cannot result in a dangling pointer, especially during deallocation.
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.)
I think React's decision, when faced with a desire to track state alongside code, to partially reimplement objects inside of functions rather than just saying "know what, this new feature requires class syntax, deal with it" (and in fact excluding class-style syntax from the new features) is the perfect sign of where the functions-vs-OO pendulum is.
It's been forever that I have used Inheritance in OOP. Perhaps Python has spoilt me, but 99% of my thought process automatically pivots to composition when thinking in terms of OOP.
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.
I have difficulty maintaining a stance in the OOP vs. FP debate. Like, all I see in my day-to-day is OOP, so I know what good, production-ready OOP looks like, but I've never an equivalent for FP. Would be nice to see a before/after article, i.e. an OOP app refactored into FP, talking about why the latter is better.
I respectfully disagree that frameworks and stdlibs for languages like Java and .Net are better due to use of Inheritance. In fact I struggle to come up with a single occasion where use of Inheritance was not an Antipattern. The exception I suppose is when you have no other way to do polymorphic interfaces.
Lots of things in software development have an is-a relationship. The only time inheritance is an antipattern is when it's used for code-reuse and it isn't modeling an is-a relationship.
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.
Is-a relationships are best handled by interfaces. If you don't have those and only have inheritance then that is the exception I mentioned above. In those cases I would argue that the base class should be composed entirely of abstract methods and the depth of the inheritance hierarchy should be no greater than 1.
If you have something that is something else in almost every way except for one or twos details inheritance is far superior than interfaces.
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.
I'll have to take your word for it that such things exist but experience so far has taught me that:
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.
> if I solve the reuse issues with some composition and the is-a problem with an interface.
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.
I've seen a few clean examples where it works really well.
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 :( )
I'm no expert, but the mental model of an interface is so simple. The inheritance model I liked in school, but practically the payoff just never seems to show up.
OOP is perhaps not the best abstraction for distributed computing where you want maximize data flow and minimize distributed state. The slowdown of Moores Law for a single core/node means performance improvements will distribute computing among a large number of such units.
I've never really gotten the hate towards OOP. It's always logically made sense to me. It follows along the path of abstraction and encapsulation computers had been moving steadily towards. It just takes the concept of a function, packages it together with some kind of data, keeps the internal workings separate from the rest of the program and allows one to reuse or extend those small pieces.
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.
A little off-topic, but when thinking about the reasoning behind OOP, in particular how it shows up in C++ and the C++-influences in other languages, I think it is worth looking at how large-scale C projects (OpenSSL, Gnome, etc.) organize their code:
long prefixes in front of function names
complex structs with lots of function pointers
complicated macros for code generation
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.
I still think procedural and OOP code requires 10-15 IQ points (for some theoretical "good" measurement of IQ) than FP.
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.
Apart from Rich Hickey's "Simple Made Easy" I think Joe Armstrong summed-up best the inherent problems with OOP as it exists in the most popular languages: "The problem with object-oriented languages is they've got all this implicit environment they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."
Something I'd really love to see is a GitHub repo containing different projects written in both an OOP style (e.g. with C# or Java) and an FP style (e.g. with F#, Scala or Python).
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?
If this article is to be taken at face value, the case against OOP is understated...
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.)
FWIW I’ve been a python and javascript dev for a long time. I’ve spent this year learning Rust and I’m kind of in love with the whole “it’s just a struct and here are some associated functions” thing. It feels so simple and clear.
I also really liked C#’s “single inheritance plus interfaces”. That was also an eye opening moment.
Just to mention, Brian Will also has a video called OO programming is good* [0]. I don't think we should take these literally, he presents what he things are some bad traits and good traits of OOP.
As usual. People have criticism. Counter critics say get smarter. The problem is that we are dealing with largely unproven concepts. We have no facts, not even meaningful empiric insights. It is a battle of religions but it should be an academic challenge.