EXPEDIA GROUP TECHNOLOGY —ENGINEERING
Lenses in Java
Improve your functional programming with Lenses
States are a common source of coupling which can glue the layers in a codebase together. Many components depend on such shared state and when we need to modify the state, we need to touch upon at multiple places probably spanning multiple logical layers. This causes code litter at multiple places.
This is where Lenses come to the rescue.
Read on to know more about the magic of Lenses…
What are Lenses?
Immutability is the core concept of functional programming languages and this idea has found its way into object oriented programming languages like Java too. There’s no denying the fact that immutability offers a lot of benefits along with it but do come with few disadvantages. Some of the most pressing ones are:
- allocating lots and lots of small objects rather than modifying ones you already have leading to a performance impact
- cloning objects and collections is tricky
These issues can be addressed by using Lenses.
Lenses keeps the balance of mutability in the immutability world. It decorates accessors and mutators of an object in a functional style providing the capability to access and mutate data in an immutable manner. It hides the complexity of cloning behind the accessor and mutators.
So, if you have a complex object which has composition of various degree of hierarchy of objects and you expect that this hierarchy might need to be manipulated or even accessed. Lenses will help you to achieve it without introducing redundancy and repetition in the code.
Adding on to that, Lenses prevent the snowball effect of mutation in immutability universe and keep them localised to the section of code which was actually modified. It generalises the accessors and mutators with the addition of composition capability.
Lens implementation examples
Before we jump into the implementation of Lens design pattern, let’s come up with few example beans that we’ll help us showcase the power of Lenses.
Our example classes model a movie booking system and for the sake of simplicity store only one movie booking for the User
.
Classes User
, Booking
, Show
and Movie
are connected through composition and are an ideal example to demonstrate Lenses.
Let’s have a look at the fully functional Lens pattern.
Here, we can see how the getter is a simple function that receives a whole object and returns a part of it. And, with the setter, we can create a new whole object with the new part that we have passed to it.
Lens usage
A simple Lens usage could be like below:
As we see in the test above, the get
of our Lens passing a User
gives us its username
and using the set
of our Lens, passes it a new name and returns the complete object with the changed name.
You might think that, the get
points to one thing and set
modifies the other. But that’s not the case, so let’s continue and find out what rules are followed by the Lenses.
Lens rules
Lens provide a convenient and easy way to mutate the state of the code. This convenience has been possible because of the rules that Lens pattern adheres to. Let’s take a look at the rules.
1. Set after get
Statement: Updating the object with what was received does not change the object
assertSame(userNameLens.set(user, userNameLens.get(user)), user);
With this rule, we should see that the set
and get
focus on the same part of the object.
2. Get after get
Statement: Updating the object and then receiving should return what was updated
assertEquals(userNameLens.get(userNameLens.set(user, "newName")), "newName");
In this, first set
of the Lens is executed which returns a new User
with new name. When we get
the username via the Lens for the new User
, we should get the new name.
3. Set after set
Statement: Updating the object twice should return the last updated object
assertEquals(userNameLens.set(userNameLens.set(user, "newName"), "newName2"), userNameLens.set(user, "newName2"));
In this, first the internal Lens set
is executed which updates the user name to newName
and then the outer Lens’ set
is executed which updates it to newName2
which is also seen in the final result.
Lens composition
As stated before, Lenses can be composed together adding a tremendous value.
This is the real power of Lenses.
We can compose multiple Lenses together which results in a new Lens. The new Lens will be able to look into the object hierarchy deeper than which could be done from individual Lenses. With the composition of Lenses, the depth of the object graph can be reached in a simpler way and then mutate/fetched easily. Let’s try to understand it through an example:
In order to create a composite Lens, we create a function f that accepts two Lenses and combines them to create a new Lens. So, assume there’s a Lens 1 that talks to type A and B (where B is part of A following the composition principle in OOP), and another Lens 2 that talks to type B and C (where C is part of B). The function f will return a new Lens that talks not only to types A & B or B & C individually but also to type A & C going deep into the inner levels.
Let’s see composition of Lenses in action.
Final thoughts
Lens design pattern is an easy way to introduce the getters and setters in an immutable and functional way. With the benefit, there’s a risk of anti-pattern too where it could break the immutability concept if not used carefully.
Please find executable code with tests at https://github.com/liquidpie/lenses-java