Restrict Mutability of State
When it is not necessary to change, it is necessary not to change
What appears at first to be a trivial observation turns out to be a subtly important one: a great many software defects arise from the (incorrect) modification of state. It follows from this that if there is less opportunity for code to change state, there will be fewer defects that arise from state change!
Perhaps the most obvious example of restricting mutability is its most complete realization: immutability. A moratorium on state change is an idea carried to its logical conclusion in languages that embody a pure expression of the functional paradigm, such as Haskell and Clojure. But even the modest application of immutability in other programming languages and paradigms has a simplifying effect with architectural implications and benefits.
Immutability makes it easier to reason about state. If an object can’t have its state changed when your back is turned, that’s one less thing to track, one less thing to worry about, one less thing that needs to be remembered, and so one less thing that can be forgotten or overlooked. If an object is immutable it can be shared freely across different parts of a program without concern for aliasing problems or synchronization surprises.
Programmers often assume thread-safety is necessarily associated with locking. This assumption comes from focusing on what is being locked rather than appreciating what locking is supposed to protect something from. You don’t use locks because you wish to prevent concurrent access to an object by other threads; you use locks to prevent concurrent access to an object whose state may change. What matters here is the possibility of change. Without change, there is no need to lock.
An object that does not change state is, therefore, inherently thread-safe and free to access — there is no need to synchronize and guard against state change if there is no state change! An immutable object does not need locking or any other palliative workaround to achieve safety.
A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in. In a multithreaded environment, the lack of understanding and the resulting problems are greatly amplified, almost to the point of panic if you are paying attention.
Depending on the language and the idiom, immutability can be expressed in the definition of a type or through the declaration of a variable. For example, Java, JavaScript, and .NET’s String class represents objects that are essentially immutable — if you want another string value, you use another string object. Immutability is particularly suitable for value objects in languages that favour predominantly reference-based semantics.
By contrast, the const qualifier in C and C++ and, more strictly, immutable in D and constexpr in C++, constrain mutability through declaration. Such qualification restricts mutability in terms of compiler-enforced access rights, typically expressing the notion of read-only access rather than necessarily full immutability.
Perhaps a little counter-intuitively, copying offers an alternative technique for restricting mutability. In languages offering a transparent syntax for passing by copy, such as C#’s struct objects and C++’s default argument passing mode, copying value objects can greatly improve encapsulation and reduce opportunities for unnecessary and unintended state change. Passing or returning a copy of a value object ensures that the caller and callee cannot interfere with one another’s view of a value.
But be aware that an approach reliant on copying is not recommended if the passing syntax is neither easy nor transparent. If programmers have to make special efforts to remember to make a copy, such as explicitly calling a clone method, they are also being given the opportunity to forget to make a copy. Far from being a simplification, it becomes tedious and error-prone, a complication that is easy to overlook, a bug waiting to happen.
In general, make state and any modification to it as local as possible. For local variables, declare as late as possible, when a variable can be sensibly initialized. Try to avoid broadcasting mutability through public data, global and class static variables (which are essentially globals with scope etiquette), and modifier methods. Resist the temptation to mirror every getter with a setter.
Encapsulation is important, but the reason why it is important is more important. Encapsulation helps us reason about our code. In well-encapsulated code, there are fewer paths to follow as you try to understand it. Encapsulation isn’t an end in itself; it is a tool for understanding.
— Michael Feathers, Working Effectively with Legacy Code
The relationship between immutability and encapsulation is often overlooked. For (counter)example, a common code smell is methods or properties that return references to collections used as private representation. Not only does this expose and tie callers into a dependency on the private representation choice, it also grants them inappropriate — and often unintended — write-access to state. In addition to traversal and query, they can now modify the collection content, breaking any invariant protection that encapsulation was supposed to offer. That no-nulls and no-duplicates guarantee? No longer a guarantee. Anyone can insert nulls and duplicates once you’ve invited them in!
Never ever invite a vampire into your house. And why? Because it renders you powerless.
Instead of handing out the whole collection, which allows others to undermine an object’s integrity, consider offering an iterable or streamable view of the elements. This takes different forms in different languages and libraries: Iterator or Stream in Java; IEnumerator, IEnumerable, and LINQ in C#; iterators and ranges in C++; iterators, iterables, and __iter__ in Python. By restricting callers to views, they can look but they can’t touch and, therefore, can’t break.
Much code that we consider complex is considered complex because of the mental highwire act we perform when trying to understand what (the hell) is going on. The more that things can change — and the more that changes depend on one another — the harder it becomes to reason about them correctly and confidently. Thinking about code should not be a circus performance. The name for code we can’t reason about? Unreasonable. Immutability makes code more reasonable.
When it is not necessary to change, it is necessary not to change.
— Lucius Cary
Restricting mutability of state is not, however, some kind of silver bullet you can use to shoot down all defects. The resulting code simplification and improvements in encapsulation nonetheless make it less likely you will introduce defects, and more likely you can change code with confidence rather than trepidation.