Business objects (domain objects, value objects, transfer objects) are logical structures that may benefit from having some of their program state locked down. Although this is less flexible in terms of what a developer can do with the object, it does give the developer more overall flexibility in making changes to the codebase. That is, if a developer can be coerced into using an object correctly, he or she can close tickets quicker without fear of side effects.
Immutability is one technique that can bring objects under control.
A Lack of Documentation
This blog post was inspired by watching seasoned and talented developers create bugs by putting objects into unknown states. Documentation is hard to come by and comments are only as good as the last update. When under the crush of closing tickets, a developer is pressed to understand an object based on his or her experience. This can result in a sensible object usage that survives the closing of the ticket, but may result in another bug (or more) down the line.Immutability keeps an object consistent throughout its life. It's also a pattern that creates new objects for new object states. Immutability is based on the principal of information hiding and is an enforcement technique that make the developer conscious of what's acceptable with an object and what isn't.
Consider this Java POJO representing a transaction
public class MutableTransactionPOJO { private Long id; private Double amount; private Date date; // related fields
private Boolean posted;
private PostedStatusType status; private Date postedDate; // end related fields
The rest of the class consists of a default constructor and setters and getters for each of the fields.
The fields id, amount, and date may be known when the object is first created. A later posting operation will set the others. An experienced developer can probably guess from my naming convention that "posted", "PostedStatusType", and "postedDate" are related. There may be a validating class to help, but if I rely on the caller to do this
MutableTransactionPOJO mt1 =
new MutableTransactionPOJO( ); mt1.setId( 1L ); mt1.setAmount(123.12d); mt1.setDate( today );
And then require the caller to do this
mt1.setPosted(true); mt1.setStatus( PostedStatusType.SUCCESS ); mt1.setPostedDate( today );
I'm requiring the developer to make six setter calls, potentially putting the object in an invalid state. One could just set posted to "true", close a ticket when the object appears successfully in a "posted" list, but introduce a bug when another area of the code checks on postedDate.
"A Lack of Documentation" means that a developer needs to rely on a Wiki or a comment to steer their object usage into a valid state. Immutability starts with the object in a valid state and doesn't worry about transitions, preferring to return solid new states. Pulling the posted fields setters and providing a composite setter like "updatePostedFields" can be a middle ground, but it's not quite as controlled as the pattern driven off constructors.
The Immutable
To turn the MutableTransactionPOJO into ImmutableTransactionPOJO, I make each of the fields final.private final Long id; private final Double amount; private final Date date; private final Boolean posted; private final PostedStatusType status; private final Date postedDate;
This means that the values must be set in a constructor. So, I provide two constructors with a strong requirements heritage: one constructor for new transactions, one for posted transactions.
The new transactions constructor provides suitable defaults for the unused posted fields. The constructor is limited in the number of arguments to discount the ones not used yet (the posteds) and chained to the other constructor to reduce duplication.
public ImmutableTransactionPOJO(Long id, Double amount, Date date) { this(id, amount, date, false,
PostedStatusType.NEVER_POSTED, null); }Rather than calling setters on a new transaction to make it posted, I provide this second constructor. Notice, that I'm taking the opportunity to perform a last-ditch validation on the input. This final check is intended for developers rather than end users. I use separate validation classes to give structured feedback to callers (mobile, desktop, web, other ejbs) for presentation to the end user.
public ImmutableTransactionPOJO(Long id, Double amount, Date date, Boolean posted, PostedStatusType status, Date postedDate) { if( postedDate == null &&
status.equals(PostedStatusType.SUCCESS) ) { throw new IllegalArgumentException("postedDate cannot be null for PostedStatusType.SUCCESS"); } this.id = id; this.amount = amount; this.date = date; this.posted = posted; this.status = status; this.postedDate = postedDate; }This is followed up with the usually getters, BUT NO SETTERS.
Usage
Immutability changes the pattern of usage of the objects. This is a common snippet of code that iterates over a list, conditionally modifying objects. If the date of a record matches "today", fields are updated reflecting the posting status.for( MutableTransactionPOJO mt : mtlist ) { if( mt.getDate().equals( firstOfTheMonth ) ) { mt.setPosted(true); mt.setStatus(PostedStatusType.SUCCESS); mt.setPostedDate( today ); } }All three fields must move in lockstep.
In an Immutable example, I iterate through a list and still expect posted information to be set. However, I do this by creating a fresh object, based on the existing one and supplemented with new values.
ListIterator<ImmutableTransactionPOJO> iterator = transactions.listIterator(); while( iterator.hasNext() ) { ImmutableTransactionPOJO t = iterator.next(); if( t.getDate().equals( firstOfTheMonth ) ) { ImmutableTransactionPOJO newT =
new ImmutableTransactionPOJO( t.getId(), t.getAmount(), t.getDate(), true, PostedStatusType.SUCCESS, today ); iterator.remove(); // take off unposted version iterator.add(newT); } }Note the interaction with the ListIterator. I need to use ListIterator instead of Iterator because I'm manipulating the Java Collections List to remove the now-stale unposted object in favor of the posted object.
Java 8 Streams
An Immutable iteration can also be performed using Java 8 Streams. This example uses a filter() to establish the posting criteria and follows up with a map() that will call the constructor with the existing values and new values.List<ImmutableTransactionPOJO> tlist = transactions.
stream().
filter(t -> t.getDate().equals(firstOfTheMonth)).
map(t -> {
return new ImmutableTransactionPOJO(
t.getId(),
t.getAmount(),
t.getDate(),
true,
PostedStatusType.SUCCESS,
today);
}).
collect( Collectors.toList() );
Not For Every Case
Some components like those mapping to a database table or a UI component may need access to each field or property. Those flexible components may rely on something like a service class to help with information hiding. You don't want to lock things down so much that classes are duplicated just to re-provide mutability. Also, if your company has a ton of JavaBeans-styled code, I'm not recommending running roughshod on the codebase.But for cases where a developer simply wants to use an object correctly but lacks good documentation, Immutability can provide guardrails to keep the tickets flowing and the side effects to a minimum.
No comments:
Post a Comment