Thursday, October 06, 2005

Dealing easily with immutable objects

Immutable object are a serious design improvement when used on value objects. I mean by 'value object', a business object which has no identity as 'ProductDescription', 'Adress', 'Money',... . The reason of the improvement is very well explained in Eric Evans book : Domain Design Driven. In a few words, immutable objects avoid side effect, simplify design and need not to be cloned.
Their drawback is that it can be very unconvenient to use especially with objects containig a great amount of properties (need to pass every parameters on the constructor).

Anyway, i want introduce a kind of builder pattern that let immutable objects be very convenient to use. The principle is to embed an inner builder class in your immutable object class. Let's see an exemple :

/**
* A class for an immutable object.
*/
public class ImmutableObject {

private String coco;
private int i;
private String loala;
private double lili;
private Date date;

/**
* Constructor used by the build.
* The only requirement for use this pattern
* is that the immutable object
* need a default constructor (which can be private).
*/
private ImmutableObject() {
    super();
}

public String getCoco() {
    return this.coco;
}

public int getI() {
    return this.i;
}

public double getLili() {
    return this.lili;
}

public String getLoala() {
    return this.loala;
}

public Date getDate() {
    return this.date;
}

//------------------- The immutable object builder
//------------------- pattern code start here
//------------------- All the code can easily be generated
//------------------- automatically by an IDE

/**
* Builder class to instanciate new Immutable object
* in a convenient way.
* This Inner class can easily be generated automatically
* by an IDE.
*/
public static class Builder extends ImmutableObject {

private ImmutableObject immutableObject;

/**
* Construct a Builder to create immutableObject.
*/
public Builder() {
    super();
    this.immutableObject = new ImmutableObject();
}

/**
* Construct a Builder to create a new immutable
* object from a prototype
* (usefull for edition purpose).
* @param immutableObject
*/
public Builder(ImmutableObject prototype) {
    this();
    this.immutableObject.coco = prototype.coco;
    this.immutableObject.date = prototype.date;
    this.immutableObject.lili = prototype.lili;
    this.immutableObject.loala = prototype.loala;
    this.immutableObject.i = prototype.i;
}

/**
* Build the ImmutableObject with defined properties.
* All previously set properties are reset.
* @return A ImmuatbleObject.
*/
public ImmutableObject build() {
    ImmutableObject result = this.immutableObject;
    this.immutableObject = new ImmutableObject();
    return result;
}

// setable properties

public Builder setCoco(String coco) {
    this.immutableObject.coco = coco;
    return this;
}

public Builder setI(int i) {
    this.immutableObject.i = i;
    return this;
}

public Builder setLili(double lili) {
    this.immutableObject.lili = lili;
    return this;
}

public Builder setLoala(String loala) {
    this.immutableObject.loala = loala;
    return this;
}

public Builder setDate(Date date) {
    this.immutableObject.date = date;
    return this;
}
}

}


The user code result in :

ImmutableObject immutable = new ImmutableObject.Builder()
    .setCoco("coco")
    .setI(2)
    .setLili(32)
    .setLoala("loala")
    .setDate(new Date())
    .build();
ImmutableObject im2 = new ImmutableObject.Builder(immutable)
    .setI(3)
    .build();


This code instanciate two ImmutableObject. The second object is a copy of the first one,
but with a different i value set on it.


The important points are :
- this pattern is not intrusive for the value object main class
- the pattern code can (and should) be generated by the IDE.
- the strong typing is kept.

Unfortunately, I never heard about such an Eclipse plugin and I have no much time to learn about Eclipse plugin developement, even if i think that's not complicated.

Thursday, August 04, 2005

Override equals() and hashCode() methods on persistent objects.

Like me, you may have wondered if you should override equals() and hashCode() methods on persistent entities. I mean by persistent entity, a business object that must be identified in the system and wich must still exist even if the system reboot. Typically those objects are backed in a database and represent a business concept like customer, account, order, and so on....

Do you need to override hashCode() and equals() for persistent entity ?

You only need to override these methods if :

  • You intend to keep instances in a Set (quite a natural way for 1-n associations)

  • You want to use the instances as keys for a map

  • You want to use Collection's contains(), remove(), .... methods (specially detached entities for Hibernate's user)

Caution : Avoid overidding equals() for mutation tracking purpose (comparing each field of the entire object graph, in order to check if two objects representing the same entity are identical) : it will leads in a severe performance issue, break Set contract and introduce semantic confusion. Actually, when using a Set.add(), Collection.contains(), Collection.remove(),.... equals() are performed against each instance of the collection. There're other meaning to accomplish this (at least use another method name as hasSameProperties(Object) or isCloneOf(Object) ).

How to implement equals() and hashCode() for persistant entities ?

There's many discutions about it, especialy this one from hibernate web site.
The autor recommends to use business key over oid to implement equals()/hashCode() cause when an instance is newly created, it has no oid assigned yet (it is assigned when saving it in the database), so you could encounter problem when storing new instances in a Set.

I want to argue the opposite : oids are almost the time, better candidate than business key to implement equals method :

  • Persistant entities have no oid when they are created, but they have rarely a business key too, generally a database sequence is used to generate parts of the business key.
  • If the entity doesn't use a database sequence for generating business key, this key is often editable, so mutable : not a good candidte for equals().
  • It's not an issue to store new instance in a Set !!! (failed the argument against oids for equals method)
  • Very most of the time, Set generally store entities that already live in the database.
The 3th point need explanations :
Just say to youreself :
  • "As persistant entities aim to identify a row in the database, if entity A and B have the same oid (wich is not null) then they representing the same row in the database so : A equals B."
  • "If entity A has an oid and entity B has not, they are not representing the same row so : A do not equals B."
  • "If entity C and entity D have both no oid, C equals D if and only if C==D."
It leads to this equals implementation :

// assert oid's type is java.lang.Long
public boolean equals(Object other) {
if ( this == other) return true;
if ((other ==null) || (obj.getClass() != this.getClass() ) ) return false;
EntityA anEntity = (EntityA) other;
if (this.oid == null || anEntity.oid == null) return false;
return this.oid.equals(anEntity.oid);
}

public int hashCode() {
if (this.oid == null) return super.hashcode();
return this.oid.hashCode();
}

This equals implementation won't break the Set or Map contract !!!!!!!!!!

Indeed, if an instance with no oid is stored in a Set : it is no equals to any other element of the Set, right ! So when it will be stored, it will get an oid, this oid can't be equals with another one cause it is unique, so the entity continue to not be equals to any of the Set element.
It's the same thing for an entity, deleted from the database (so loosing it's oid).
it no longer be equals to any element of the Set.

Those generic equals/hashCode method have the advantage of being generic and efficient, you can implement it on a generic abstract entity class.

I used this approch for years, using intensively Set to implement associations betwen persisted object, I never encountered any problem on it.