Wednesday, August 29, 2007

Example of mutable extension to immutable domain model

I must not have communicated the mutable extension idea clearly enough, as I've been accused of breaking the immutability and encapsulation of the domain model which would, I agree, make the domain model sloppy. I deplore mutability in domain models and anemic / 'behaviourally absent' nonsense such as setters / getters.

I haven't done either of those things (as far as production code is concerned) here's a worked example which demonstrates this.

Here's the immutable, encapsulated, distinctly unsloppy domain model:

class Person {
private final String name;
private final int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

Here's code that's only available for testing:

class MutablePerson extends Person {
private static final String DEFAULT_NAME = "Bob";
private static final int DEFAULE_AGE = 32;

public MutablePerson() {
super(DEFAULT_NAME, DEFAULT_AGE);
}

public MutablePerson withName(String name) {
return new MutablePerson(name, getAge());
}

public MutablePerson withAge(int age) {
return new MutablePerson(getName(), age);
}

private String getName() {
return accessor.get(this, "name", String.class);
}

private int getAge() {
return accessor.get(this, "age", Integer.class);
}

private final ReflectiveAccessor accessor = new ReflectiveAccessor();
}

Now it's true that the testing code has broken encapsulation but only as far as the testing code is concerned, not for the production code. The domain model itself is unchanged. It should be noted that there is a one-to-one relationship between the immutable domain model class and the mutable extension and that the break in encapsulation for a given domain model class propagates no further than the corresponding mutable extension; and again this break only exists in the testing code.

So I would say the testing code has a minor encapsulation breach which allows the testing code to have a more succinct form than possible with the test builder (it essentially removes the need for all the build() methods).

Unlike the Test Builder it would not be suitable to move the Mutable Extensions to the production code. Immutability & non-anemicness helps keep domain models from becoming 'bags-o-data', having a mutable version of each domain model in the production code would be too much of a temptation.

3 comments:

Julio said...

LOL! How's your project going mate? It's either good - because you have time to get annoyed at this - or it's just been annoying for a very long time so this comes out.

Admittedly - this is more cerebral than version control philosophising.

JG Lvl 11.

Joshua Graham said...

Non-anemicness? How about "sanguinicity" instead ;-)


When I get back, if you havent choofed off back to ol Blighty yet, I'd like to explore with you making this more concise, perhaps with a mixture of annotations and something similar to ReflectiveAccessor to give the same result.

Would be nice to have markers like @Translucent to supply test-only encapsulation breakers and @Immutable to automatically assert exceptions are raised (IllegalState?) when the runtime attempts to change the fields after instantiation, which should all be final.

Stacy Curl said...

I must be going bonkers, it's totally unnecessary to even access the fields in the base class, just create duplicate fields in the 'mutable extension', then access them directly.

public class Person extends model.Person {
  private final String name;
  private final int age;

  public Person(String name, int age) {
    super(name, age);

    this.name = name;
    this.age = age;
  }

  public Person withName(String name) {
    return new Person(name, this.age);
  }

  public Person withAge(int age) {
    return new Person(this.name, age);
  }
}

Now the only possible problem is intermediate stages not passing some constraint.