Layered Entities
Concept to separate the basic aspects of what defines an entity into separate instances of different layers:
-
Identity, a never to be replaced instance representing an entity in terms of references to it
-
Logic, nestable in an arbitrary number of dynamically created logic layers, e.g. logging, locking, versioning, etc.
-
Data, always immutable
Entity graphs are constructed by strictly only referencing identity instances (the "outer shell" of an entity), while every inner layer instance is unshared. This also allows the actual data instance to be immutable, while at the same time leaving referential integrity of an entity graph intact.
Ready-to-use logic layers are provided for:
-
Logging
-
Versioning
While the layers admittedly introduce considerable technical complexity and runtime overhead, this concept is a production ready solution for nearly all requirements regarding cross cutting concerns and aspects.
To use this concept in your code, there need to be at least implementations for the entity’s identity and data.
Let’s say the entity looks like this:
public interface Person extends Entity
{
public String firstName();
public String lastName();
}
There needs to be an identity class:
public class PersonEntity extends EntityLayerIdentity implements Person
{
protected PersonEntity()
{
super();
}
@Override
protected Person entityData()
{
return (Person)super.entityData();
}
@Override
public final String firstName()
{
return this.entityData().firstName();
}
@Override
public final String lastName()
{
return this.entityData().lastName();
}
}
And a data class:
public class PersonData extends EntityData implements Person
{
private final String firstName;
private final String lastName ;
protected PersonData(final Person entity,
final String firstName,
final String lastName )
{
super(entity);
this.firstName = firstName;
this.lastName = lastName ;
}
@Override
public String firstName()
{
return this.firstName;
}
@Override
public String lastName()
{
return this.lastName;
}
}
A lot of code to write to get an entity with two properties! But don’t worry, there is a code generator for that. An annotation processor to be precise. The only code you have to provide are the entity interfaces, all the other stuff will be generated. Just add the annotation processor type |
The generator also builds a creator:
public interface PersonCreator extends Entity.Creator<Person, PersonCreator>
{
public PersonCreator firstName(String firstName);
public PersonCreator lastName(String lastName);
public static PersonCreator New()
{
return new Default();
}
public static PersonCreator New(final Person other)
{
return new Default().copy(other);
}
public class Default
extends Entity.Creator.Abstract<Person, PersonCreator>
implements PersonCreator
{
private String firstName;
private String lastName ;
protected Default()
{
super();
}
@Override
public PersonCreator firstName(final String firstName)
{
this.firstName = firstName;
return this;
}
@Override
public PersonCreator lastName(final String lastName)
{
this.lastName = lastName;
return this;
}
@Override
protected EntityLayerIdentity createEntityInstance()
{
return new PersonEntity();
}
@Override
public Person createData(final Person entityInstance)
{
return new PersonData(entityInstance,
this.firstName,
this.lastName );
}
@Override
public PersonCreator copy(final Person other)
{
final Person data = Entity.data(other);
this.firstName = data.firstName();
this.lastName = data.lastName ();
return this;
}
}
}
An Updater:
public interface PersonUpdater extends Entity.Updater<Person, PersonUpdater>
{
public static boolean setFirstName(final Person person, final String firstName)
{
return New(person).firstName(firstName).update();
}
public static boolean setLastName(final Person person, final String lastName)
{
return New(person).lastName(lastName).update();
}
public PersonUpdater firstName(String firstName);
public PersonUpdater lastName(String lastName);
public static PersonUpdater New(final Person person)
{
return new Default(person);
}
public class Default
extends Entity.Updater.Abstract<Person, PersonUpdater>
implements PersonUpdater
{
private String firstName;
private String lastName ;
protected Default(final Person person)
{
super(person);
}
@Override
public PersonUpdater firstName(final String firstName)
{
this.firstName = firstName;
return this;
}
@Override
public PersonUpdater lastName(final String lastName)
{
this.lastName = lastName;
return this;
}
@Override
public Person createData(final Person entityInstance)
{
return new PersonData(entityInstance,
this.firstName,
this.lastName );
}
@Override
public PersonUpdater copy(final Person other)
{
final Person data = Entity.data(other);
this.firstName = data.firstName();
this.lastName = data.lastName ();
return this;
}
}
}
An optional equalator, with equals
and hashCode
methods:
public interface PersonHashEqualator extends HashEqualator<Person>
{
public static PersonHashEqualator New()
{
return new Default();
}
public final class Default implements PersonHashEqualator, Stateless
{
public static boolean equals(final Person person1, final Person person2)
{
return X.equal(person1.firstName(), person2.firstName())
&& X.equal(person1.lastName (), person2.lastName ())
;
}
public static int hashCode(final Person person)
{
return Objects.hash(
person.firstName(),
person.lastName ()
);
}
Default()
{
super();
}
@Override
public boolean equal(final Person person1, final Person person2)
{
return equals(person1, person2);
}
@Override
public int hash(final Person person)
{
return hashCode(person);
}
}
}
And an optional Appendable:
public interface PersonAppendable extends VarString.Appendable
{
public static String toString(final Person person)
{
return New(person).appendTo(VarString.New()).toString();
}
public static PersonAppendable New(final Person person)
{
return new Default(person);
}
public static class Default implements PersonAppendable
{
private final Person person;
Default(final Person person)
{
super();
this.person = person;
}
@Override
public VarString appendTo(final VarString vs)
{
return vs.append(this.person.getClass().getSimpleName())
.append(" [lastName = ")
.append(this.person.lastName())
.append(", firstName = ")
.append(this.person.firstName())
.append(']');
}
}
}