Not sure I understand what this means. Could you give an example?
But then again, what you want there is modularity, which OOP provides, but is also well-supported in other paradigms.
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.
There's a potential performance cost here, but that usually doesn't matter a whole lot.
Yes, but this is not possible in general. For instance, what if you want to endow your List with a "MaxTotalCost", set at creation. You can't ensure that your list respects that invariant, other than by encapsulating it behind some custom interface.
[1]: https://craftinginterpreters.com/representing-code.html#the-...
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()).
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.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.
State and zip code always change together. Having had to fix code that passed 5 variables around for an address (which needed to go to 6 for “address 2”) I replaced it with an address object.
For me, true OOP requires some sort of inheritance and/or runtime method dispatch, at least.
Can we at least agree that limiting operations on the structure to the code that controls that structure is following the definition of encapsulation, which is what I should have said anyway?
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.
[1] Object Oriented Programming Is An Expensive Disaster Which Must End
http://www.smashcompany.com/technology/object-oriented-progr...
A: OOP isn't so bad, actually.
B: But what about all these terrible messes?
A: Oh, that's not a problem with OOP,
they're just doing it wrong.
I'm sorry, but when 80% of the industry (and 100% of new grads) are "just doing it wrong", it isn't helpful to no-true-scotsman the critics.The only way I see out of this endless game of semantics is for someone to canonize the tightrope of practices which are "OOP done right", and then give it a different name.
Our industry has a naming problem when it comes to practices, especially when the practice is subtle and complex but its name is simple and trendy (maybe it's not specific to software, IDK).
Dev teams are mislead into thinking that they can deduce the practice from the name (and maybe a 1h training with a self-proclaimed expert in the practice).
This is how we got aberrations like: - "TDD" devs writing their tests afterwards - "Agile" teams with 6-month release period - "SCRUM" masters that act as classic managers
New names referring to "good/right/trendy" practices are doomed to get claimed by "80% of the industry", in a way that doesn't respect the original practice.
Alan Kay recently said that maybe he should have called it "Message-oriented programming" ( instead of OOP ). While not having "object" mostly eliminate the risk of the "one-class-per-real-world-object" antipattern, we might as well had ended up with other anti-patterns based on wrong interpretation of what constitutes a "message".
This is why I think changing the name of any practice is inefficient. The misunderstanding isn't accidental, it's systemic (Moreover, renaming a practice would probably create even more confusion)
Names that refer to common mistakes wouldn't get claimed ; no team is going to proudly explain to their customer that they're using the "big ball of mud" antipattern, the "scrum but" team organisation, or the "debug later" dev practice.
So maybe we should better canonize OOP antipatterns?
Though when a microservice mutates something, it's typically persisted in a database or similar (and, you'd hope, not sprinkled throughout its code). But, yeah, a bunch of communicating microservices are a lot like communicating objects - for better or worse. Same with Erlang-style actors.
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.
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.
If you're going this far, you're essentially throwing away typing as well.
Are you actually just opposed to private fields?
Object methods are just sugar for this, so whats the difference to you? When you say "separate" do you mean you just want it in different files? Different curly braces? Different namespaces?
Is it inheritance and virtual functions?
For example, a hash map data structure hides the details of how the hash map is implemented, but it doesn't hide the data. In some versions of OO, the object should not only hide the implementation details but also the data. Any function that needs that data MUST be a method of that object. That's where things go haywire.
Devs aren't clairvoyant so when we're making a new type we might sometimes get this wrong. But this is a fundamental issue with type systems in general and not OOP, is it not?
There arent many hash map methods that will actually modify the data.
Standard OOP, on the other hand, seems to encourage such behavior. So, for example, a Company object could contain a bunch of Department objects, each of which contained a bunch of Employee objects.
These people objects could also be referenced in areas outside of the company object.
If I called a company.giveFinanceDepartmentARaiseInDollars(10000) function on the Company object, it would have an unpredictable impact on the Department/Employee objects. It may also have cascading effects in other parts of the application that may not be clear.
On the other hand, if you represented this as a company hashmap, with the keys the department structs, you would have to do something like the following instead:
company["finance"].employees.forEach(x => x.salary = x.salary + 10000); company["finance"].base_salary = company["finance"].base_salary 10000
This contrived example shows some of the pros/cons. OOP allows us to hide a lot of behavior/changes, which may mean less repetitive code. OTOH, it hides a lot of behavior/changes.
More generally the issue of global mutable state and keeping your own sanity as a developer is a problem across paradigms.
My “functional” code wasn’t actually functional.
If I had written that correctly, by returning a modified copy using a map instead of modifying the strict in a loop, your example would also be covered, because the function you’re describing would not modify any of its inputs, but instead would return a new copy. And the function wouldn’t have any side effects either, so it would once again be completely clear what’s happening.
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.
Here are some links along the same lines:
* _Aggregate Roots_ in DDD limit pointers, https://martinfowler.com/bliki/DDD_Aggregate.html * Guidance on where to mutate, _Functional core, imperative shell_, https://www.destroyallsoftware.com/screencasts/catalog/funct...
> 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.
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.
What OOP offers on top of modules is extendability and polymorphism.
And these two capabilities are possible to achieve in various ways.
That is the mechanism for immutability that is traditionally provided. It is easy to opt out of that protection.
Java and C# are getting records. They're a convenience but are in no way opposed or counter to OOP.
Where things would get silly is if you tried to make all state internal. But that's what some purist OO approaches would do for business objects.
By contrast, in an 'extreme procedural' style, you would have a struct that is an array of buckets and an integer, and functions that can take such a struct and return information from some particular place inside it. In normal use you would just use the function, but if you're passing your hash map to someone else, there is no longer a guarantee that the length field still matches the actual buckets, or that objects are still arranged into buckets based on their hashes.
Anybody modifying Customer object would hopefully be in the right mindset regarding personal information, but once it gets out as a string from GetName or GetEmail, all bets are off, it gets into all kind of logs.
Anything security related is the same.
A better example might be a linked list. With information hiding, you manipulate the list through functions like push and pop. Without information hiding, you manipulate the pointers directly. For instance, I've worked on C codebases where linked list manipulation isn't even hidden behind macros.
I used to see that type of approach in some complex Fortran simulation programs where every function referenced a huge common block. I really don't think that leads to easier to manage solutions.
1.) Who can access this state, and which of those accesses allow writes vs. which are read-only?
2.) What is the lifetime of that state, and what portion of that lifetime is mutable vs. read-only?
Java-style OOP can control #1, but makes no distinction between reads & writes. C++ const correctness and the val/var distinction in Kotlin/Ocaml/ES6/etc. can make that distinction, but only C++ forces you to think in terms of lifetimes. Rust borrowing probably comes closest to answering all 4.
I'm still waiting for a language feature that lets you say "This field of this struct is only mutable while this module is running, and once it completes it will never be changed", though. A lot of data structures are initialized in passes and then passed along as constant data: you construct the basic object structure with nothing but a few IDs, then you compute extra fields as necessary, then you're done and the object is never written to again. If you could keep the fields mutable during initialization (which may be a longer process than just constructor calls) and then seal them off once that's complete, you eliminate a whole class of bugs where a read-only client decides to mutate a field that should only be mutated from designated initializer.
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.
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.
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.
Clojure, in 2020, is very very different from the Lisp of 50 years ago and is modern in many respects, even compared to other popular languages.
In proper OO state is hidden and insofar as it causes issues it does so in a way that is handled by the object rather than the program overall.
Also treating code as data isn't a feature of OO, it's an old idea starting with Lisp and metaprogramming I suppose and it's present in functional languages as well. It basically just means that the language itself is a first class data type within the language.
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.
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.
Nitpick, but I'm not sure this is the correct plural.
As you may very well know (but just in case, and for others), the "system" in ECS doesn't describe ECS as a whole, but another part of the model - you have entities, attributes of those entities, and systems that operate on those attributes.
That said, I'm not sure what I'd suggest instead. "Entity component system systems" is obviously terrible. Just not pluralizing probably works, here? or "towards the entity component system pattern"?
... shrug
One problem is that every OOP advocate has a different perspective on "what the entire point of OOP is". Another problem is that irrespective of what any OOP advocate believes is OOP, the body of code that is commonly understood to be "OOP" is plagued with abuses of inheritance (I'm not even convinced there are legitimate uses), mutable state, the "banana with a reference to the gorilla holding it with a reference to the entire jungle" problem. OOP advocates are quick to say "that's not real OOP" in a very no-true-scotsman fashion, but that aspirational statement isn't comforting to people who have to deal with these problems day-in-and-day-out.
> Also treating code as data isn't a feature of OO, it's an old idea starting with Lisp I suppose and it's present in functional languages as well.
The parent specified that their objection was about tightly binding data to code, not about treating data as code.
It's true that bad OO code suffers of different problems than bad FP code, and both are different from bad procedural code. But there is no guarantee that an organization which produced bad OO code would have produced good FP code, just as there was never a reason to imagine (though many did) that an organization that produced bad procedural code would produce good OO code if forced to switch to OO.
In general, there is no solver bullet. We can absolutely replace the problems specific to bad OO code with problems specific to bad FP code if we move from OO to FP, but we cant be sure of any other outcome.
More likely than not, we need to change more than the style of code we build if we want to take an org that has produced bad code and make it produce good code - we should change incentives, testing, goals, timelines etc.
And just to be clear, you're absolutely right that saying 'that's not good OO code' is no-true-Scotsman and not helpful in any way, other than pointing out that it is possible to produce good OO code (some people doubt that, I think).
The difference is that FP, procedural, data oriented, etc are all pretty reasonably defined. No one seems to agree with what OOP is, and any objection to the kind of code that is typically considered "OOP" is that "that's not real/good/etc OOP code". It's a no true Scotsman.
When I talk with OOP proponents and point to issues about inheritance or the gorilla/banana/jungle problems, they say that these things aren't part of OOP. I think when you deduct from OOP these kinds of warts, you end up with something that looks almost indistinguishable from data oriented programming (something like what you would write in Go or Rust) and increasingly something that resembles functional programming specifically now that many OOP languages have support for first class functions and more functional abstractions in their standard libraries.
So if there is any truth in the "not true OOP" argument, I think it's that "good object oriented programming" is just another spelling of "data oriented programming".
One is the Alan Kay camp, which usually defines OOP as primarily message passing, with Smalltalk as a beacon of what they consider good code. This is a very vocal group, but I think that it is extremely obscure in the industry.
The other group is mostly focused on SOLID and design patterns. I would say that Java's Swing is a good example of a usable library based on this style of code, inheritance-heavy but still principled.
I believe that this style of code is most reliant on garbage collection to actually be reasonable. Otherwise, you end up caring about ownership for every little object and it rapidly becomes a nightmare. I believe your banana/gorilla/jungle problem is related to this mostly.
For myself, I do believe that implementation inheritance is usually to be avoided, but I think that other OOP concepts, such as encapsulation and interface-style extensibility, using constructors to establish invariants, are frequently useful, especially in the high-level architecture of a program or module. I do prefer some kind of support for sum types and multiple dispatch, rather than traditional virtual dispatch.
Some of those things still apply in non-OOP style programming, but OOP certainly has a larger cognitive load for many nontrivial applications.
Keeping code and state more decoupled actually reduces cognitive load in many cases. In some of those, there is less encapsulation, but at least IMO OOP overshoots for how much encapsulation is productive.
Now, I am not religious about this and regularly approve OOP designs in reviews. But my general approach is to prefer modules and packages of functions for behavior, plain structures for state, and only dip into OOP style encapsulation when it seems the extra encapsulation and bundling is worth it. A class modeling a mutex or other resource would qualify there.
And that’s the fundamental problem with OOP. State can’t be encapsulated. It’s highly radioactive. State should be kept to the boundaries of your application, where it interfaces with the real world, and minimized as much as possible. The core should be immutable.
And, if you have code that is for working on a particular kind of data, why is binding it to the data problematic?
I don't agree at all. It's much more dangerous to separate data representation from the code that manipulates the data. I can't tell you how many bugs I've found and had to fix in large codebases (most of which are procedural-code-in-"OOP"-languages) that result from some horrific evolving arrays-and-records structure not recording intent and validity constraints properly (since that would require, you know, code). Instead, each interaction with the Holy Data Structure requires the code to go to confession, recite ten Hail Marys and five Our Fathers before receiving the Almighty's state-changing grace, mercy and peace; failure to do so will result in immediate confinement in purgatory, and possibly damnation.
I'm not being hyperbolic albeit a bit facetious about this. "Data" itself is the problem: it is subjective, and so requires a lot of tinkering for a programmer to grok. That tinkering transforms it into information, but only in the programmer's head, which is less than ideal since it's outside of the program. If that information fades away, it must again be rebuilt (or worse, be short circuited so a hotfix can go out to prod).
> and that statefulness is fine to freely sprinkle throughout your program.
I agree much more with this, with the condition that shared statefulness sprinkled throughout is the problem. An object should be treated with the same respect we would show a VM or even a person: decent ones would not tolerate having an external entity reach in and manipulate things like memory, nor would they be so care-free as to depend on and trust literally everything they are told. I think the big problem here is that few or no OO languages are powerful enough to allow for this kind of arrangement to be the norm. But it is a valid criticism and should be taken more seriously by OO language designers.
EDIT: I wanted to add one more thought, which is that when I'm working on a new feature or new codebase, I tend to model the problem first in a procedural style. Then I rewrite it as objects. I've had very little luck trying to do objects first (though I think this is just me), because decomposing the problem space is just hard to do de novo. But once I understand the problem, it's easier to see the constraints and issues.
Also, certain code that doesn't need to worry about things like noisy channels (e.g. Dijkstra's shortest path algorithm) is probably a good candidate for non-OO code. OO is good for the large amount of software that interacts with people across multiple machines and environments, rather than isolated from everything in just a single machine.
This is grevious error, IMHO, that leads to nothing but problems: endless boilerplate code, lots of "interfaces" that essentially do nothing (that couldn't be done directly with said data), and debugging nightmares.
I much prefer to work with systems where code and data are separate and never the twain shall meet.
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.
There are various legends, and they mostly conflict with each other.