Dysfunctional programming in Java 2 : Immutability
From dysfunctional to functional : functional programming with immutable objects in Java!
In the previous installment of Dysfunctional Programming in Java we saw how introducing laziness could simplify and make our Java code more robust. We started with a relative simple data class and examined some of it’s hidden complexities — at least one of which we fixed by introducing a functional lazy data-structure. In this article we will investigate another core functional concept — immutability.
The class currently looks like this :-
Even with a relatively simple class, it’s mutability may cause us some problems.
Happy families are all alike; every unhappy family is unhappy in its own way. — Tolstoy: Anna Karenina
An analogous statement in terms of software design may be
Good designs are all alike; every bad design is bad in its own way.
Mutability imposes no design constraints on developers, meaning engineers are large free to sculpt mutable imperative programs how they see fit. Mutability will not guide us to good designs, but it sure as hell is possible to build bad ones with mutable Objects.
Let’s look at some common problems clients of this class may design themselves into. For example it is perfectly possible to write factory code like this :-
What is wrong with this code you might ask? The code base you are working on right now may look kind of like this ( — run, get away now, while you still can 😜).
- Mutable Objects allow us to mutate them as input parameters
Most of the time, most of us agree we shouldn’t do this. Most of us probably think that we don’t do this, but if you take 5 minutes to check your current project, my guess is there will be plenty of examples right there.
Take a look at how the variable metadata is used. It is declared and then instantiated via a No-Argument constructor. None of the fields have been set. No additional reassignments are made to that variable. Then it is returned from the createDataFileMetadata method. Only one of two things are happening.
- We are returning an empty DataFileMetadata instance (really unlikely — how says you can’t have Theorems for free in Java?? 😛😂😛)
- It is being mutated inside both methods where it is provided as an input parameter (😱🤢😱).
Mutating the contents of an input parameter is a really bad practice — not least because it can change the semantics of a method from those it had when client code that calls it was created. I.e. if we were already using the writeDataFile method in DataFileWriter elsewhere, that code will also be impacted by the parameter mutation. Mutability makes reuse harder / more risky (who knew! 😲😳🤯)
In this case, the most likely change we are introducing is that the writer will store the File reference to the location the contents were written on the metadata Object.
Another reason this is a bad idea is that it doesn’t really help or guide us away from writing code like this (⏬⬇️⏬) :
The void return type in this method means we don’t have to return a DataFileMetadata instance (because we can mutate in place). So there is nothing to alert us that we’re creating execution paths that we can completely fall through.
Perhaps the right thing to do is throw an Exception ( — or — engage in some more functional style error handling to be shown in depth in a future article in this series). The absence of controlled error handling here, means that NullPointerExceptions elsewhere — when client code expects the customerId and type to be set are likely!
2. Mutable Objects allow us to distribute the Object creation logic throughout our code base
We can see from the example code that our Object is instantiated in one method, and then most likely populated in two others.
This particular anti-pattern makes it harder to figure out when our Objects are fully initialized and where additional initializing enhancements should be made in the future. It can make it very difficult, when taken to extremes, to be confident about the current state of our Object even during initialization. In large applications this can lead to excessive defensive programming. For example null checking on every method, this in turn can lead to hidden production errors (if the policy is to in some way attempt to recover from a null value, this is much more likely to hide another problem than a fail-fast approach would).
3. Mutable Objects don’t play well with multi-threaded code
Limiting ourselves to the example at hand, if either or both of the I/O Bound services called inside our method were to leverage threading (now or in the future), we would open ourselves up to race conditions.
Code calling createDataFileMetadata may attempt to read from the DataFileMetadata Object returned, before either service had finished writing to it (inside writeDataFile / attachCustomerInfo).
[Functional Concept] Immutability : How to make Java Objects Immutable
Now that we’ve had an examination of the reasons why we should want to make the Objects in our application immutable — let’s investigate the how. Much like swapping the the sides on which you fold your arms (try it now! fold your arms and then swap which hand rests higher from left-right or vice versa) — if you are used to creating mutable Objects in Java only, the mechanics of creating Immutable ones can be difficult to grasp initially.
Recap what our DataFileMetadata class currently looks like :-
On the necessity of constructors that accept arguments
The first thing we can do is make all of the fields final
Once we do so IntelliJ or Eclipse will inform us that variable x has not been initialized. We need to define a constructor that passes in the initial values for the uninitialized values. Note that, even though the field contents has not been loaded from disk we can define it fully, and finally where we declare it.
We can replace the Lombok @Setter annotation with an @AllArgsConstructor instead.
That was the easy part! A pretty common pattern used in creating mutable Objects in Java is to write something like this..
Often, the actual population of the fields can be spread out over a significant forest of complex conditional and looping code. The disentangling of which is probably the largest stumbling block to adopting immutable objects.
The key point to notice though is that the fields are very often set only after our variable has been declared and initialized. In fact, if we rely on constructors without arguments all our Objects must populated in this way.
With immutable Objects the key insight is that we need to do the oppostite. We need to make all of our fields values available prior to declaration and initialization. A good technique to help you build confidence is to declare all field values as local (ideally final) values first.
We can refactor our initialization code above to declare all of the field values before Object initialization.
By declaring the values for our fields before we instantiate our Immutable DataFileMetadata Object we make it’s declaration simple, but the challenge remains — how to populate those values?
[Functional Sub Concept] Expressions are your friend
Declaring customerId is straightforward in this case ( — it was already available in scope, so we can simply resuse that declaration). On the other hand extracting out type (String) and output (File) is a little more awkward. They were defined in the middle of an if / else statement.
An expression evaluates to a value. A statement does something. Statements represent an action or command e.g print statements, assignment statements. Expression is a combination of variables, operations and values that yields a result value.
When fields and values are declared final it is much cleaner to declare and initialize them with an expression that evaluates to the required value. The code below uses a statement to initialize a pre-declared immutable field. This spreads the initialization logic over a wider area than is necessary.
It’s much cleaner to use an expression (a method or function with a return type) to initialize i
An alternative method of creating an expression is to inline the logic with the ternary operator
For Immutable Types : Make field data available before Object creation
Move the variable declarations up! (That’s up as in up towards the top of your monitor, well above the new keyword!)
Making updates to our Immutable DataFileMetadata Object
We now have an Immutable DataFileMetadata class. Along with some general idea of how to instantiate Objects to work with it ( — define the values for it’s constructor parameters well above the new keyword). But how do we make changes to it?
It’s immutable, so the answer is we can’t. The trick is to create a new Object instance that copies the values we wish to keep while replacing the ones we’d like to remove with the new value.
If we define a DataFileMetadata Object for a customer that hasn’t not yet provided their final contents we could override the output location like so
We can simplify the code with Lomboks @wither annotation
Then the calling code becomes
If mutability fails enforce constraints on our liberty to produce bad designs, the inverse is true of immutability. The harsh, harsh constraint of immutability — gently guides us in the direction of more robust and correct code.
If we make DataFileMetadata immutable, it isn’t possible to mutate it as an input parameter, it’s difficult to work with it in a partially constructed state & we can share it safely across threads,
Refactoring to immutable
Now, armed with a knowledge of and a toolkit for immutable Objects lets refactor DataFileService to use the Immutable DataFileMetadata Object instead.
One of the first steps we will need to take is to change the signature of the attachCustomerInfo method on the UserService. This relied on accepting a mutable DataFileMetadata class and on being able to change its values. Instead this method now needs to return a value, and we can use that to update our immutable DataFileMetadata Object by creating an entirely new copy.
We need to refactor the writeDataFile method in exactly the same manner.
Following the technique discussed above of making sure that the data for the fields of our immutable Object is available pre-instantiation we can refactor the createDataFileMetadata method
With the code refactored and working with Immutable types it is possible to reduce it to a single line, by removing the intermediate references :-
Next up : Functional composition
In the next article we will continue to evolve the DataFileMetadata class, and explore the possibilities for enhancements offered by making more full use of the Eval datatype used to manage the contents internally. We will explore functional composition in more depth.