5

OOP makes state reads and writes implicit. For instance, in Python:

class Foo:
    def bar(self):
        # This method may read and/or write any number of self.attributes.
        # There is no way to know or limit what self state this method
        # accesses and/or modifies.

Compared to:

def bar(qux, baz, flux):
    # This function's only inputs are qux, baz, and flux. Its only output:
    return trax

The latter seems far easier to read, maintain, test, and reason about.

Are there any solutions for this issue? I'm especially interested in how you'd solve this problem in mainstream languages like Python and C++, though pointing out any tool or language that solves it can be helpful as well.

  • 4
    Your problem has MUCH more to do with immutability than it does with OOP versus Functional programming. An Immutable OOP will give you the same readibility and testibility as your functional example, probably. However, basic OOP is taught in exactly the problematic manner you describe, so I definitely see why you think its bad. – Graham Oct 28 '19 at 18:22
  • @Graham not quite. Immutability gives me an answer for the question "which attributes of self does this function write to", since it forces the answer to always be 0. However, it does not address the other question: "which attributes of self does the function read". – Dun Peal Oct 28 '19 at 18:51
  • 3
    @DunPeal Any of them - and that's fine. It just says that it needs a whole object. In exactly the same way, you cannot answer the question which of the parameters, or which parts of their values, the function in your second code snippet does access. – Bergi Oct 29 '19 at 2:03
  • @DunPeal, which parts of qux does bar in your second example read? Even if qux is a primitive type like an integer, it is still made up of multiple bits. And if you don't care, why do you care about which parts of Foo get accessed in the first example? – Bart van Ingen Schenau Oct 29 '19 at 11:29
6

That's not quite right.

I mean, you're right that explicitly declaring a method's inputs and outputs is very good. I think a language that actually declares it's types is better than your Python example even.

But you're not right that OO's are implicit. In an OO world the reads and writes are constrained to the instance the method is on. And since data should be private, you know that the state changes are limited to that class. And since good OO constrains classes to be focused on a single responsibility, there tends not to be state that is visible to a method but not a dependency of that method.

There's some question about how well that works in practice, but the same question applies to functional or imperative programming models as well.

tl;dr - OO limits the scope of state changes by literally limiting the scope of state.

| improve this answer | |
  • "In an OO world the reads and writes are constrained to the instance the method is on." Sure, but the objects themselves can become very large and complex. Effectively, self becomes a global state of sorts. It's certainly better than having all the state thrown in the global scope, but you will have some of the same problems as self grows in size and complexity. – Dun Peal Oct 28 '19 at 17:19
  • 8
    if objects become very large and complex, then you're doing bad oo. Just like if you made a whole bunch of global state in a functional language. No amount of process can make up for bad programming. – Telastyn Oct 28 '19 at 17:26
  • The limiting case is the god object: just shove all your globals into a class and call it OO. It is very clearly no such thing, and using it as an example of flaws in OO style is entirely to miss the point – Useless Oct 28 '19 at 17:59
  • 1
    @Bergi - which is no different from a function acting on its (mutable) inputs or calling some dependency. – Telastyn Oct 29 '19 at 2:37
  • 1
    OOP done right is a bag of hidden data that sends messages to another bag of hidden data when you send messages to it. – candied_orange Oct 29 '19 at 3:12
2

OOP mitigates this particular problem with encapsulation.

When calling a method (from the outside) you don't know what what internal attributes may be read and modified. But in OO you shouldn't know or care.

More generally, the "unit" you reason about and test is the object, not the function. So internal attributes are like local variables inside a function: You don't care about them when calling the function, you only care about observable behavior and input/output.

When testing objects, you don't test by inspecting the internal state. That would indeed be cumbersome and fragile. Instead you test the behavior of the object through its public interface.

I really dislike foo/bar examples, so lets take a more realistic example. Lets say you have an ordered dictionary:

let dict = OrderedDictionary()
dict.Add("car", "voiture");
dict.Add("horse", "cheval");
print dict["horse"] --> cheval
print dict[0] --> voiture

This dictionary could be implemented in multiple ways, e.g. a linked-list of key-value pairs, a hashtable combined with an array and so on. It could even change strategy based on the number of items. The point is that you don't care as long as it works.

Now consider if all parameters had to be explicit:

dict_h = HashTable()
dict_l = Array()
dict_Add(dict_h, dict_l, "car", "voiture")
dict_Add(dict_h, dict_l, "horse", "cheval")
print dict_by_key(dict_h, "horse") --> cheval
print dict_by_index(dict_l, 0) --> voiture

Here it is explicit that e.g. dict_by_key only uses the hashtable and not the array. But the price for this explicitness is really steep: You push complexity and implementation details to the clients which spreads it all over the program and makes it much more difficult and risky to change the implementation. (Never mind that a Hashtable in itself would consist of multiple attributes)

Functional languages typically solve this by using record types which may contain multiple fields. But then you are back to the square one, that you don't know exactly which of these fields aRE read or modified by a function call.

| improve this answer | |
1

This is normally not an issue, because in the real world methods would not (should not...) be named bar or other meaningless names but would express the semantics of the service that an object provides. How it implements this service is none of your business as a user of the class, it may be a pure function or a complex algorithm that keeps cached results, or it may delegate to an external service, or whatever. You may rightfully assume that a method name() will not return a different value each time you use it, while balance() may return different values depending on the deposit() and withdraw() operations that were performed.

The developer's responsibility is to pick good semantics and good names (and preferably document them in an easily understandable form) and to avoid nasty surprises for the user. This is actually not much different from non-OOP programming...

| improve this answer | |
  • Even if you work in a business area which allows clear and concise semantic names for every method (very big if), what you describe doesn't seem to solve the problem of testing. If I want to test a method, I'd still have to hunt down all its input states across N lines of code, find ways to inject them (difficult in many languages), and if I mess up and/or miss anything - my test will be broken, at best missing some error conditions. – Dun Peal Oct 28 '19 at 17:35
  • 1
    I mean, that's sort of true, but it's mostly just as true for pure functional code. You still need to inject every possible combination of input arguments. If the problem is that it's hard to set up your object state, then you wrote a bad object which is hard to test. The intrinsic complexity is the same. – Useless Oct 28 '19 at 23:54
1

You're right in that the classic designs of stateful OOP and pure functional programming are somewhat at odds. There's ways to mitigate this however:

One approach is to use Immutable objects and return a new object with every method call that alters state. This works up to a point, and if you use shallow copying it's not even all that inefficient, but it gets pretty cumbersome to implement.

An extension of that approach is to return new objects only where "observable state" changes, otherwise return the same object with its state changed -- basically a fluent API. In a pure functional language, that pretty much limits you to caching and local calculations, but if you're just going for pragmatism in a stateful OO language, you can give yourself a lot more wiggle room.

For example, consider a "builder" pattern where you create an object then update it with a sequence of methods: If you're not using that builder anywhere else except for its final result, you can still mutate it with every method call and still test it in a clean way by recreating the builder with the same sequence of method calls. Obviously this only applies when every method call is deterministic and doesn't depend on outside data that can change underneath it (like randomness or database fetches)

Determinism is the operative word here: overall, you're never going to squeeze referential transparency out of a stateful OO language, but if your methods are deterministic in how they update internal state, then you can still test any sequence of calls with confidence, and compose any number of those calls into a utility method.

| improve this answer | |
-2

There is no "implicit reading/writing" in OOP.

If a read operation happens, it happens because I wrote explicit code to read it.

If a write operation happens, it happens because I wrote explicit code to write it.

I write methods that don't modify the state of an object without reason, but make exactly the modifications that I need to achieve what the method wants to achieve.

You gave one quite meaningless example with nothing whatsever in it. Try creating a real world example with some complexity.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.