I have a java project that involves building some pretty complex objects. There are quite a lot (dozens) of different ones and some of them have a HUGE number of parameters. They also need to be immutable.
So I was thinking the builder pattern would work, but it ends up require a lot of boilerplate. Another potential solution I thought of was to make a mutable class, but give it a "frozen" flag, a-la ruby.
Here is a simple example:
public class EqualRule extends Rule {
private boolean frozen;
private int target;
public EqualRule() {
frozen = false;
}
public void setTarget(int i) {
if (frozen)
throw new IllegalStateException(
"Can't change frozen rule.");
target = i;
}
public int getTarget() {
return target;
}
public void freeze() {
frozen = true;
}
@Override
public boolean checkRule(int i) {
return (target == i);
}
}
and "Rule" is just an abstract class that has an abstract "checkRule" method.
This cuts way down on the number of objects I need to write, while also giving me an object that becomes immutable for all intents and purposes. This kind of act like the object was its own Builder... But not quite.
I'm not too excited, however, about having an immutable being disguised as a bean however.
So I had two questions: 1. Before I go too far down this path, are there any huge problems that anyone sees right off the bat? For what it's worth, it is planned that this behavior will be well documented... 2. If so, is there a better solution?
Thanks
2 Answers 2
Here is @9000 suggestion with a fluent setter that returns a new object:
public abstract class Rule {
public Rule shallowClone() {
// see http://stackoverflow.com/questions/5395501/shallow-vs-deep-copies-in-immutable-objects
}
}
public class EqualRule extends Rule {
private int target;
public int getTarget() {
return target;
}
public EqualRule setTarget(int target) {
EqualRule clone = super.shallowClone();
clone.target = target;
return clone;
}
@Override
public boolean checkRule(int i) {
return (target == i);
}
}
boolean result = new EqualRule().setTarget(1).checkRule(1);
Pros: Allows creation of immutable objects in a strongly typed manner (less human errors when using the POJO).
Cons: More human errors when implementing the POJO.
If you don't want to pass a mutable object into an immutable object (or vice versa) you can use a map.
public class EqualRule extends Rule {
private final int target;
public EqualRule(final Map<String,Object> config) {
this.target = config.get("target");
}
public int getTarget() {
return target;
}
@Override
public boolean checkRule(int i) {
return (target == i);
}
}
You can automate this using the Jackson library. It can inject data to immutable objects (final members) or fake immutability (private setter) automatically. See the @JsonProperty annotation in the Jackson library:
public class EqualRule extends Rule {
private final int target;
@JsonCreator
public EqualRule(@JsonProperty("target") int target) {
this.target = target;
}
public int getTarget() {
return target;
}
@Override
public boolean checkRule(int i) {
return (target == i);
}
}
final ObjectMapper mapper = new ObjectMapper(); //singleton
final ObjectNode node = mapper.createObjectNode();
node.put("target", 1);
// not as efficient since internally causes serialization/deserialization.
EqualRule rule = mapper.convertValue(node, EqualRule.class);
There is a side benefit that the conversion would allow you to make the code less vulenrable to schema changes and more backwards compatible. For example when converting from json to pojo you can handle or ignore unknown properties.
-
Hmm.. That looks like an interesting thing to do, but in this case it's not being serialized at all so I can't really justify importing a whole external json library. I have heard good things about jackson in the past before but it's not really applicable.scott_fakename– scott_fakename05/25/2013 22:06:53Commented May 25, 2013 at 22:06
-
Added more code to explain the tradeoffs vs passing a map to the cotr.itaifrenkel– itaifrenkel05/26/2013 07:38:48Commented May 26, 2013 at 7:38
Foo().setBar(5).addBaz(1).addBaz(2).build()
. Upon the finalbuild()
call, the builder object returns an immutable implementation of Foo, which can as well be the builder itself in frozen state.return this
is another pattern, called fluid interface; to me it seems very convenient. One of the nice things about it is that a "setter" becomes a "modifier" that can return a different object (notthis
) is a particular step allows for a different, optimized representation.