But why?
There's a lot of object-hating going around. Its silly. Use objects to encapsulate functionality, they are good at that and everybody understands what it means.
But why?
There's a lot of object-hating going around. Its silly. Use objects to encapsulate functionality, they are good at that and everybody understands what it means.
You can encapsulate functionality without objects. There is nothing that you can do with an object that you can’t do with a closure, and the closure version will be 1/10th the size and have 1/10th the semantic programming language elements. That is why you see ‘object-hating’ all over the industry. It hasn’t lived up to its promise.
Objects aren’t simpler, inherently. It’s just what you know, today, and you’re unwilling to acknowledge your biases.
The reason information hiding is/was advocated was for reduced coupling. But in reality I find that this coupling becomes implicit which is arguably worse. But this is just my opinion.
If it does have internal state, then the answer it returns depends on the value of that internal state which means it's hard to verify that the answer returned is correct since you have to know the internal state of the object.
You test a class as a user by calling its methods in the desired order (per requirements and documentation) and you check that the results you get are correct. If the results are correct, the class fits your use case. If they are not, then you're either calling it wrong or it has a bug. Either way, no need to know the internal state.
This is my point. The order of calling the methods matters, because it manipulates opaque internal state. Since this ordering matters, the caller is implicitly dependent on this internal state as it must know the order to call the methods in to get the desired result.
So it's very relevant to the outside world in my opinion and nothing is truly hidden. It becomes part of the public API whether you hide it or not.
Think of a List class. Let's say it has methods to add, remove and retrieve elements, and it has a method to retrieve the size.
Let's say the internal implementation is an array with a fill pointer.
Obviously, before you can usefully call list.get(7), you must have called list.add(T) at least 7 times (assuming no deletes).
Do you need to know the the state of the backing array and the value of the fill pointer? Do you need to know whether there even is a fill pointer, or a linked list underneath (obviously, the performance would be different so you would need to know at some point)?
However, when it is hidden in the OOP sense it makes it much harder to reason about the behavior of a function without reading the source code. Because there is this internal "hidden" state that you do not know exists.
But a referentially transparent function is much easier to understand. For a given input, you get an exact output.
Testing is limited when you do not have the ability to fully control state, and understanding can be limited when information is hidden. The impacts of this are determined by the information being hidden, its relationship to the function's behavior, and the documentation of that behavior relative to the calling context.
Limitations on the ability to fully control state are orthogonal to the method of information hiding (opaque function parameters, internal object state, etc).
This is not an OOP/functional discussion, this is a discussion about the tradeoffs inherent in information hiding. And let's be perfectly clear here: These are tradeoffs, not black and white clear wins in either direction.
It's been a useful discussion though so I appreciate everyone's input.
I feel like it's difficult to talk about this without examples. The library I like thinking about when I think of passing around in a state variable is the lua C api.
https://www.lua.org/pil/24.1.html here is an example.
https://pgl.yoyo.org/luai/i/lua_State here are docs for the state variable's type.
This is pretty object oriented except that it's not using C++'s syntax sugar. You can't really do much with L except pass it into other lua "methods". The discussion up until your comment seems to be about the difference between using the syntax sugar and passing around a state variable yourself.
Information hiding and these other implementation details are kind of seperate. In my example you can't just configure L exactly how you want by messing around with it or inspecting its state directly. You have to go through accessor "methods".
I think if the argument is that OO promotes information hiding I can agree with that. I'm not sure about the point about the internal state becoming a part of the API though. APIs are contracts that can be met with different implementations right? If your implementation details are public then your contract is huge and inflexible.
Since using this API makes an implicit constraint that it is using a stack underneath the hood, why not just expose the stack for inspection? What advantage does keeping it an opaque blob have? I agree you don't want code manipulating this stack (although if it is immutable as in FP this isn't an issue), but since the external code already knows it is a stack and relies on that fact then it is part of the public API already.
I believe David Parnas introduced it in 1971 to mean that a program's design was sliced along shared units of concerns (things that vary together) rather than "steps in a flowchart".
https://prl.ccs.neu.edu/img/p-tr-1971.pdf
I believe what you are trying to convey is called "data abstraction", as for example used by Reynolds, 1975;
mentioned here: https://www.cs.utexas.edu/~wcook/papers/OOPvsADT/CookOOPvsAD...
explained here: https://link.springer.com/chapter/10.1007%2F978-1-4612-6315-...
Tests.h
#define private public
#define protected publicWhen you test opaque objects, you're generally not trying to test individual state transitions, you're trying to test that the class's interface adheres to the external promises it makes.
That said, many OOP languages have solutions specifically for when you actually do need to test those internal state transitions. C# for example has the "internal" keyword and allows you to declare friend assemblies, so you mostly get your cake and eat it too, at the cost of not hiding the code from yourself as the module implementer.
Whether you make it private or not hidden state leaks into the output since the output is dependent on it.
I don't want to worry about external methods modifying state, I don't want to deal with constructors, I don't want to deal with instantiating state or modifying state before I call my function.
My function just takes a state and returns a new state. Simple.
But it is - the programming language is now repeatedly quizzing you on something you've already told it.
method(state){
return doSomething(state)
}
Or this: class A:
otherThingThatMutatesState1()
otherThingThatMutatesState2()
otherThingThatMutatesState3()
method(){
this.state += 1 // or some other mutation
}Also, pieces of data that hide their internal structure and just expose an API to modify that data (Objects) are a very useful construct, and used in all languages with first-class mutation. Some languages have special syntax for this case (e.g. C++, Python, Go, OCaml), some don't (e.g. C). For example, no language that I know of exposes a mutable List type with a public count of elements. Neither does any language I know of expose a mutable List type as a closure.
I'm noting this because i think that number kinda detracts from your main argument, with which i agree completely. I think even if the code would remain roughly the same size, the removal of OO would still be worth it, since, as you mention, is much less jargon and conceptual baggage. And if i can achieve the same result with a much simpler conceptual model and language, then all the better :)
Closure and function return function can be used as substitute, and except for inheritance (which should be avoided too), I haven't found any use case for using class class. Example:
function MyList {
let data = [];
let add = (item) => data.push(item);
return { add };
}So for every list you have an additional allocation for the add function. It obviously continuously to get much worse as more "methods" are inevitably added. That's extremely inefficient for something that's used often and it's not something JavaScript engines can optimize away. The benefit is also extremely negligible since the closed over values are still implicit in the call to add.
This seems pretty central to your point, but a factor of 10 is also a very strong claim. I'd like to see sources.
Passing around state via variables is no more tedious than the extra syntax for class definitions, constructors, member variable access, extra semantics related to objects, extra keywords related to visibility, etc.
So instead of doing objects, you're doing closures that act like objects?
Closures - functions with data
No, I'm using using as-plain-as-possible data structures for my state, that I pass to functions that operate on it and return new state. Simple, composable, easy to test, easy to pass the state data elsewhere, serialize it or whatever else I might want to do. I'm not arguing against using classes or objects, but often its unnecessary and hiding mutable state in an implicit "this" variable is, in my opinion, something that often gets in the way of simplicity, as it often tends to imply mutable objects when in my opinion mutable data should be a careful decision rather than the default. Not necessarily of course, but even when you're operating on a constant "this", I feel that hiding the data that its acting on internally is still often not the right choice, especially if the object itself isn't constant/immutable: then its a coeffect instead of an effect, and if you're using truly immutable objects, then its really just a minor syntax difference to write obj.foo(bar) instead of obj(foo, bar). Object systems still carry around a whole bunch of other functionality that you may not want or need, but by using objects, a reader can't know if you are or not without reading the code or documentation. If its data and functions, it isolates the places things can happen somewhat. Classes and objects have their place, but I feel they shouldn't always be the default choice, just because.
That's my take at least, and a big reason I switched from Python and Java to Clojure, but to each their own :)
Often the best things to do in an OOP language is to remember that you don't have to encapsulate everything, and sometimes it's just about bundling data/state.
I may have highlighted the wrong point that I was responding to. I was making a point about the behavioral equivalence of objects and closures in response to this:
There is nothing that you can do with an object that you can’t do with a closure, and the closure version will be 1/10th the size and have 1/10th the semantic programming language elements. That is why you see ‘object-hating’ all over the industry. It hasn’t lived up to its promise.
If you're using a closure where it would have made sense to use an object, it's likely that the closure has the same problems to solve with encapsulation, reasoning about hidden state, etc. that make many languages' object related syntax complex, but instead of dedicated object syntax sugar you're solving it with dedicated closure syntax sugar.
Of course in reality its not that bad because you can also use classes as much or little as you want and can use them simply for single dispatch, or namespacing, if you wish.
I do find that in practice syntax and features matter because they push you to think in a certain way and it can be hard to break away from that. For example, I write a lot of Clojure and really like functional languages, but when I use Java, Python or even C++, its all too easy to slip into the OOP mindset even when I set out to just "do functional" in those languages. Maybe that's my failing and its certainly not OOP's fault, but I think in practice (at least from observation) it seems most people are like this.
But yeah, I don't actually disagree with you on what you said.
All you need to do is define a function and call it.
Literally, again I urge you to take a look at the following example, it's waay more simple than OOP:
Except that it's actually more tedious implementing your own bootleg object. Then managing the scope, namespace and pointers. When they could all be contained inside a language construct guaranteed to follow the rules.
How do you prevent memory “leaks” due to the banana referencing a forest problem? With JavaScript libraries returning a function pointer, the returned function pointer would often have a closure that contained a lot of irrelevant state. There is no easy way to reason about the leak, there is virtually no way to work out the cause at runtime (debuggers can help, but a problem with increasing memory usage certainly is not easy to diagnose why). Even when really conscientious about the problem, it is seriously hard to avoid the problem or inadvertently create a closure e.g. many programmers are unaware that arguments may not get GCed if you add an event handler within a function (closing over arguments and maybe other variables).
Objects have their problems, but the memory leak problem with JavaScript closures is insidious and very very hard to fix.
But no one is going to write a big industrial project in assembler today, because it's the wrong abstraction for the job.
Likewise with objects. They make it easy to switch conceptual levels in a domain in a way that projects don't. They take a bit longer to code, but with careful coding you can reuse them ad lib.
The OP is making the point that OOP is the wrong abstraction if there's no reuse. And that's perfectly true.
But there are situations where you want to say "Make a thousand of these items which respond dynamically and somewhat independently to their environment but which still support some kind of top-down management..."[1]
You're going to have a bad time trying to do that with exclusively with closures. Of course you can probably make it work - but that doesn't mean you should.
[1] E.g. particle and/or crowd simulation in games/CGI.
OO couples behavior with state. Sometimes that's exactly what you want (e.g. containers). And sometimes you just need data, functions and namespaces and all 3 are available in C++ outside classes.
If my state has some invariant, I want to be sure that I can trust it. I don't want to worry that someone will mess with it, breaking the invariant, or accidentally pass in stale state.
If that's all handled automagically by the object, it's out of sight, out of mind. Add in a get_state() and set_state() method if you need it as an escape hatch or for testing.
So yes, object-hate characterises the popular but tiresome anti OOP sentiments going around.
The context pointer for an encapsulated set of functionality, need not be passed around. It adds little or nothing. In a language full of encapsulated functionality, it stands out like Chekov's Gun. I'd argue, there better be a damn good reason to deviate for normal practice. And not just 'I prefer the explicit'.
I've seen monstrous state variables passed around to "pure functions" at least as much as I've seen monstrous god objects; the problem is around complexity and data flow design and has very little to do with function vs object.
Sadly objects are also used in an attempt to hide data (because direct access to data is considered dirty in OOP).
Now, consider a non-trivial OOP class hierarchy. You have an object that needs to mutate another object's data (an object that is completely unrelated to your initial object). In order to solve this you have options:
- completely rearchitect your code so that you take this use case into account
- find the shortest path between these two classes and add setters to each class
- hack the code and call it "technical debt"
First one is not feasible, second one is unmanageable (complexity increases dramatically), third one is usually chosen.
Speaking from experience, OOP is most of the time the worst tool for the job.
Yes breaking encapsulation willy-nilly is a bad thing. So don't.
I don't doubt there are bad programmers out there. Blaming the hammer for a bad carpenter is foolishness.
Btw objects are used for like 5 different things. Choose what works for your problem space.
Call it whatever you want, the requirement is for one class to produce a side effect in a completely unrelated part of the code (happens all the time). This doesn't fit with the current architecture, and there was no way of predicting it beforehand. Now what? Rewrite everything? What happens if you're in a department of 30 developers? Tell them "hold on while I go back to the drawing board to create a clean design"?
OOP isn't a hammer, it's duct tape, and carpenters are taught from school that duct tape is the main way of doing carpentry. It's duct tape because it ties data to functions in a way that makes it very difficult to extract or reuse that data somewhere else.
When OOP is your religion you don't blame the bad carpenters, you blame the master priest.
https://news.ycombinator.com/item?id=23338700
You will see that it is far more concise and intuitive to do it with a function.