Maintaining state is the main cause of complexity and headaches in software development: without a careful consideration of state, our projects will inevitably become impossible to understand. In fact, various development techniques and programming styles are mainly there to handle state in a responsible way: for example, monads, as used in functional programming, are often employed for this very task. A good general way of managing state is trying to make it immutable, either through the use value types, that is, types which instances are passed around with deep copy semantics, or simple immutable objects, which have reference semantics but because they’re immutable their state is fixed.
Unfortunately the processes we usually represent in code are all but immutable: the state of any running software is probably going to change as time passes, as a result of foreign interactions, a.k.a., side effects. This means that something, somewhere has to mutate, and to be more precise we can say that the information stored in a certain object is going to change: actually, an information by itself is a constant value, but from time to time that object is going to store different pieces of information, some are going to be new, others are going to be outdated. So there’s no escape from mutation: the point is to mutate responsibly, that is, to not be reckless with our mutable objects, and treat them in a special way so that we can still reason about our code and easily understand the state of our system at any given moment.
The already cited techniques based on monads represent a good way to solve the problem, but monads only really work in a functional programming context, something that’s not everyone’s cup of tea, and to be really productive while using them you need to think in terms of monads, and treat them as primitive objects. I don’t want to talk about monads or functional programming in general, my focus for this article is analyzing mutability in software, while identifying common techniques that can be leveraged in any programming language/paradigm to improve our code. In particular I want to talk about 3 specific problems that arise while working with mutable objects, and 3 solutions to address them. To quickly summarize, my suggestions will be the following:
- be idempotent;
- react to change;
- proceed one-way only.
Let’s see each one of them in detail by developing a concrete example: an object that represents a payment process, which at any moment can be in various states, like possible, started, working, succeeded and failed.
To mutate an object from the outside we usually tell the object to do something. If we truly want to respect the object-oriented paradigm, and in particular if we don’t want to violate encapsulation, we shouldn’t make assumptions about the internal state of a certain object after a certain method call; still, we cannot help but making assumptions about the way our entire system is going to work after a method call, otherwise it would be impossible to reason about code. But if our changes are incremental, it’s going to be really hard to understand the system even after a simple method call. Not all changes are created equal: if the resulting system’s state is affected by the previous one, before the change takes place, then it’s going to be basically impossible to reason about the system after the change, because in our reasoning we should take into account all the possible conditions the system was in. An incremental change, for example a method call to increase a counter, will overload our mind of conditional paths and will cause bugs.
For example, for our
PaymentProcess we don’t want to be able to generically advance the payment state: we want to advance it to a particular stage, with a clean method call that should be idempotent, that is, calling it 1 time, 2 times or 100 times has to be the same. This also means that if more objects call the same method, it will be like if only one object called it, and this will completely remove a running cause of bugs: multiple, uncoordinated interactions of the same type with an object. Idempotence is a simple concept, but it’s extremely powerful: with it, we don’t need to keep track if a particular operation has already occurred (thus saving some state), but to properly implement it we need the correct semantics: while delta operations are very frequent in real life (think about increasing the volume of a TV set) and are usually harmless, they can be dangerous in software development, an in general it’s important to understand the what feels natural in real life is not necessarily a good thing in software engineering, or engineering in general.
Also, notice that while an idempotent method call will always have the same result on a particular instance, it doesn’t mean that will have the same result on different instances - more on this later.
React to change
Reactive programming has been big the last few years, and that’s mostly thanks both to the popularity achieved by functional programming in OOP circles, and to the diffusion of architectures based on microservices. But the idea of being reactive in software development has been around for much more than that: for example, the observer pattern has been there for decades, and the basic underlying idea is that of connecting objects by establishing relations between them in which a state change is automatically propagated to a collection of observers. It is responsibility of an observer to react to what just happened, while the observable object doesn’t really care about what the others will do. This will allow us to achieve the following:
- a nice decoupling between a mutable object and other objects that for any reason are interested in its mutation; no custom interface will be needed, just a simple fixed method (like
next) to pass around the new information;
- a more declarative code style, in which our method calls will only describe the intent of connecting one or more objects to a chain of reactions (and functional reactive programming is particularly good a that).
About our payment example, whatever object is interested in a change of state, like for example UI objects the will show different views to the user based on the current payment stage, should really subscribe to our payment object to receive signals about its mutation: the payment object itself should then send these changes to the observers. Notice that I used the word send and not broadcast: the change of state of a particular object should usually not be treated as an all-encompassing event. Events are really about application wide changes (like a phone call during the execution of a mobile application): there are many ways to implement our observables and observers, the point is simply to have a clean, fixed API to emit an information, and ReactiveX could be a nice starting point to get an idea about the possible APIs.
Proceed one-way only
This might be controversial but it’s extremely important. Consider the following case: our payment process object has idempotent methods and a reactive API, so a view controller object can keep a UI updated and send back user interactions to advance the process. But while the process is in “working” stage something goes wrong and the process fails: not a problem, we actually considered the “failed” stage, so the
PaymentProcess is updated accordingly. But we would actually like to retry the payment, and of course update everything accordingly in our reactive chain. What should we do? Set our
PaymentProcess back to “working”? That would be possible, but it’s going to make everything more complex, because all the observers should in theory take into account the fact the this is not the first attempt to pay (the UI for example could show a “retry” message instead of the usual one).
In general, we want to solve a state problem, we don’t want to distribute it to multiple objects.
Another case could be a complete stop of the payment process (the user could cancel it, for example, or the session could expire): we don’t want to bring back the
PaymentProcess instance, forcing all observer to do some kind of cleanup. In any case, a process that goes back and forth makes reasoning about code really hard, because it makes the future more complex: all the objects that depend on a mutable object are going to be easier to implement if we can assume that the mutability will only proceed in one direction. Of course this is not easy to implement, but in this case it’s really natural to think about it: if the milk is running out in the evening, I cannot expect the bottle to magically refill for the morning, so I should start thinking about a different breakfast. Processes that are invertible usually require some form of external, high-level coordination, and from an architectural standpoint the idea of an object that mutates in any possible way might seem harmless: but at the implementation level this will cause problems, and bugs are in the implementation, not in the architecture. While implementing new features, refactoring, debugging or simply studying a codebase we reason about the code, not the architecture, and coordinator objects are classic jack-of-all-trades at the architecture level: but unfortunately, an implementation that requires external coordination is going to be more complex and error-prone, so it’s better to be clear about the mutation path for an object.
In our particular case, we could consider one or more “retry” stages, or a single “retry” stage with an associated object that represents the number of retries. And about the possibility of going back to the beginning, in that case we should really discard the whole process: the process owner, that would have likely activated the various accessory objects (like the ones for the UI) should listen for an “abort” stage, that would make it kill everything and start the process anew. There might be performance concerns: to mutate objects is usually faster than recreating them, but again, it can be managed and it’s better to have cleaner, more understandable code from the beginning, so that it’s going to be easier to make fine calibrations in later stages, than to create a mess in the name of early optimizations.
One final question: if the process is one-way only, what happens if some object tries to set it to a previous stage with a method call? Absolutely nothing. The ignore-if-impossible semantics is actually clean and reasonable, and we don’t need errors or exceptions to handle: even if this has to be taken into account at every method call, the important thing is to be clear about it, and the alternative is much worse. This whole article could be summarized in the phrase “clear semantics are what makes mutability bearable”. In this particular case, the fact that a method call is ignored or not doesn’t change our reasoning: I’m not going to make assumptions about the state of
PaymentProcess after the call, because I’m actually reacting to its changes in a separate context.
We considered a bunch of possible problems to think about when implementing mutable objects, and some options to confront them. Of course these ideas are purposely very general: every problem is different, every codebase is unique. But applying even just one of these techniques will most likely result in more clear and understandable code. I also didn’t talk about application-wide mutation, because that’s really an architectural thing; my goal is to raise awareness about the concept of mutation and variable instance properties in every single class. We should always avoid mutation when possible, and concentrate our variable state in a few, carefully controlled places; but if we’re not careful, even a single mutating class in a multiple class project could wreak havoc, and force use to debug and fix a system that we thought was clean and well structured.