MapStruct and Immutability
MapStruct is a pretty popular and well-maintained framework for object mapping. To summarize: it is capable of generating the mapping code among objects in different layers of an application.

About MapStruct
MapStruct is a pretty popular and well-maintained framework for object mapping. To summarize: it is capable of generating the mapping code among objects in different layers of an application.
A typical scenario is when we do mapping from ugly SOAP objects to well-formed domain objects, and maybe from domain objects to REST DTO objects and vice versa.
This excellent framework makes this job a pancake. In short, using annotations one can define all the non trivial mappings (non trivial = the name of the field differs in the source and the target object), add custom logic for doing tricky mappings (requiring call to the business logic for example), etc. Those, who have not heard of this great framework, a visit to http://mapstruct.org is strongly suggested.
Immutability
Life is very happy with MapStruct as long as someone is not thinking about writing immutable code.
In the mutable world basic mapping instantiates the target object, than calls setters on the created instance. This is definitely something that does not fit into the picture when it comes to immutable objects.
Immutability and builders
Luckily when we move into the immutable universe, we soon realize, that we need builders to be efficient.
As a basic scenario we can use builders to initialize and instantiate the immutable objects. We might use a builder with an original object and a builder consumer to instantiate a new object, copy all the properties, update the ones using the consumer, and build an updated copy.
Those few lines above are more than enough reasons to dive into builders.
Builders for the help?
Since builders themselves can be mutable objects, it is a natural decision to choose them as the targets for the mapping when using MapStruct. The "only" problem is, that this way the code gets contaminated by .build()
calls in order to get created a normal domain object based on the configured builder.
Another issue might be that a possibly not that experienced teammate might commit code (hopefully only for peer review), where she or he has cheated the immutability by sending around builders to achieve "easier" solutions, thus breaking the teams dream of immutability.
Abstract mappers
Luckily, in MapStruct a mapper does not necessarily needs to be an interface. The framework gives quite a lot of flexibility, and one of the goodies is that a mapper class itself might be defined as either an interface or an abstract class.
This is great news for us struggling with immutability. This gives us the option to define an abstract mapper, meaning, that we can
-
Define an abstract method, which is going to map from the target class to the builder. It is hidden form the outside world by being defined as package private, but allows MapStruct to pick it up, and auto-generate the mapping code.
-
Define a second method, which is not abstract. It is public, and simply calls the mapper method defined above. Based on the return value it either calls
build
method (if the returned builder is not null), or returns null (or maybe an Optional, if we like that better).
An additional benefit is, that as the builder is returned by the abstract method, we have a natural place to customize our object mapping prior to calling the build
method.
An example
Let us define the following hypothetical SoapUserObject, and its counterpart (User).
package no.itverket.soap;
public class SoapUser {
private String firstName;
private String lastName;
// 11 digits: private, 9 digits: company, otherwise unknown
private String id;
public SoapUser(String firstName, String lastName, String id) {
this.firstName = firstName;
this.lastName = lastName;
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
package no.itverket.domain;
public final class User {
private final String firstName;
private final String lastName;
private final UserType type;
public User(String firstName, String lastName, UserType type) {
this.firstName = firstName;
this.lastName = lastName;
this.type = type;
}
public static class Builder {
private String firstName;
private String lastName;
private UserType type;
public Builder setFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder setLastName(String lastName) {
this.lastName = lastName;
return this;
}
public Builder setType(UserType type) {
this.type = type;
return this;
}
public User build() {
return new User(firstName, lastName, type);
}
}
}
User is immutable.
In addition we have a tiny little enum class for the user's type.
package no.itverket.domain;
public enum UserType {
UNKNOWN,
PRIVATE,
COMPANY
}
If we follow the recipe above, we get to the following mapper definition:
package no.itverket.mapping;
import no.itverket.domain.User;
import no.itverket.domain.UserType;
import no.itverket.soap.SoapUser;
import org.mapstruct.Mapper;
@Mapper
public abstract class UserMapper {
abstract User.Builder soapUserToUserBuilder(SoapUser soapUser);
public User soapUserToUser(SoapUser soapUser) {
User.Builder builder = soapUserToUserBuilder(soapUser);
return builder != null ?
builder.setType(getUserType(soapUser))
.build() :
null;
}
private UserType getUserType(SoapUser soapUser) {
UserType userType = UserType.UNKNOWN;
String id = soapUser.getId();
if(id != null) {
switch (id.length()) {
case 11:
userType = UserType.PRIVATE;
break;
case 9:
userType = UserType.COMPANY;
break;
}
}
return userType;
}
}
If we define the following utility class and interface:
package no.itverket.mapping;
import java.util.function.Function;
public class MappingUtils {
public static <B extends ObjectBuilder<R>, R> Then<B, R> ifNull(B builder) {
return factoryFunction -> defaultValue -> builder == null ? defaultValue : factoryFunction.apply(builder);
}
@FunctionalInterface
public interface Then<B, R> {
OrElse<R> orElse(Function<B, R> factoryFunction);
}
@FunctionalInterface
public interface OrElse<R>{
R orElse(R result);
}
}
package no.itverket.mapping;
public interface ObjectBuilder<R> {
R build();
}
and do a little modification in the User class's builder:
public final class User {
//...
public static class Builder implements ObjectBuilder<User> {
//...
@Override
public User build() {
return new User(firstName, lastName, type);
}
}
}
than we can write the mapping method as follows:
@Mapper
public abstract class UserMapper {
//...
public User soapUserToUser(SoapUser soapUser) {
return MappingUtils.ifNotNull(soapUserToUserBuilder(soapUser))
.then(builder -> builder.setType(getUserType(soapUser)).build())
.orElse(null);
}
//...
}
This reads far better.
Benefits and conclusions
Following the recipe above, we managed to make MapStruct and immutability live together in harmony using builder classes and abstract mapper classes. However, going immutable still involves a lot of manual coding if we use bare java. One should really consider using a framework, like immutables to make life and easier, and the codebase smaller.
Tailoring the returned object
In addition, since we govern what happens in the mapping method, which calls build
, we can tailor the return type for our needs. Instead of returning null we might return an optional to explicitly tell our client that the object to be returned returned may not be present.