Smarter ValueObjects & an (even more) elegant Builder
Value Objects (VOs) are prevalent and needed in traditional Java programming. They're almost everywhere -- to hold information within a process, for message-passing, and various other areas.
Apart from having getters
and setters
for the properties, on several occasions, there's a requirement for these VOs to implement equals()
and hashCode()
. Developers usually hand-write these methods or use the modern IDE templates to generate them. This works fine initially or until there's a need to update the VOs with one or more additional properties.
With an update, the baggage that comes with new properties includes:
a new set of
getters
andsetters
,updates required to
equals()
,hashCode()
, and,update required to
toString(),
if needed
This is, of course, cumbersome, error-prone, and the simple VO soon starts looking like an airplane cockpit!
Google's AutoValue framework is a smart approach to address this issue. With just a couple of annotations, almost all of the "junk" is done away with, and the class becomes smarter -- any future property updates, including getters
, setters
, as well as equals()
*, hashCode()
** and toString()
are all handled automagically!
The VO then just looks like a basic set of properties of the given type, like so:
import com.google.auto.value.AutoValue;
@AutoValue abstract class CartItem { abstract int itemCode();
abstract int quantity();
abstract int price();
static CartItem create(int itemCode, int quantity, int price) {
return new AutoValue_CartItem(itemCode, quantity, price);
}
}
Note the default presence of a static factory method create()
, as suggested in Effective Java [Bloch, 2017], Item 2.
The use of this annotated VO would be no different from a typical one. For instance, the CartItem
defined above would have a simple invocation like this:
@Test
public void create() throws Exception {
CartItem item1 = CartItem.create(10,33, 12);
CartItem item2 = CartItem.create(10,33, 12);
assertEquals(item1, item2); // this would be true } Apart from the default support for a static factory, AutoValue also supports Builder classes, within the VOs. Armed with this knowledge, let's take another jab at the example in my previous post on Builders. We continue with the same Cake example and add the required annotations and modifiers. The updated version of the class would then be:
import com.google.auto.value.AutoValue;
@AutoValue abstract class Cake { // Required params abstract int flour(); abstract int bakingPowder();
// Optional params abstract int eggs(); abstract int sugar(); abstract int oil();
static Maker builder(int flourCups, int bkngPwdr) { // return builder instance with defaults for non-required field return new AutoValue_Cake.Builder().flour(flourCups).bakingPowder(bkngPwdr).eggs(0).sugar(0).oil(0); }
@AutoValue.Builder abstract static class Maker { abstract Maker flour(int flourCups); abstract Maker bakingPowder(int bkngPwdr); abstract Maker eggs(int eggCount); abstract Maker sugar(int sugarMg); abstract Maker oil(int oilOz);
abstract Cake build(); } }
Observe that:
the member
Builder
class (namedMaker
here) just needs to be marked with@AutoValue.Builder
annotation, and the framework takes care of everything elsein the parent class, we could also have had a no-arg
builder()
method but we specifically want to have only one way of building this class -- with the required paramsas shown above, the optional parameters should be set to their default values since we want the flexibility of choosing only the relevant optional params. [With non-primitive members,
@Nullable
can be used.]
Just to complete the discussion, here is an example of the ease with which this new builder
can be invoked:
@Test
public void makeCakes() {
// Build a cake without oil Cake cakeNoOil = Cake.builder(2, 3).sugar(2).eggs(2).build();
assertNotNull(cakeNoOil);
// Check that it has 0 oil assertEquals(0, cakeNoOil.oil()); // default
// Make cake with oil Cake cakeWOil = Cake.builder(2, 3).sugar(2).oil(1).eggs(2).build();
// Obviously, both the cakes are different assertNotEquals(cakeNoOil, cakeWOil); // valid
// Another cake that's same as cake w/ oil Cake anotherCakeWOil = Cake.builder(2, 3).sugar(2).oil(1) .eggs(2).build();
assertEquals(cakeWOil, anotherCakeWOil); // valid
}
There are many other fine-grained things that can be done while using AutoValue, like specifying getters
for specific properties or customizing toString()
, etc.
It's impressive how AutoValue facilitates writing and static factory methods and builders quickly -- taking the headache out of defining and updating VOs.
[Full implementation of the abovementioned example is here.]
Further reading:
Project Lombok also addresses the VO update issue, along with other things
* Effective Java [Bloch, 2017], Item 10 ** Effective Java [Bloch, 2017], Item 11