5/09/2008
Anemic Domain Model
Martin wrote a blog a long time before: http://www.martinfowler.com/bliki/AnemicDomainModel.html. It was about domain model without rich behavior (anemic). Today, I am going to analyze why we have this problem, and try to give a elegant solution.
Let's give a example first. This is a task management system. Two entities in the domain, Employee, Task. So we can write the relationship as following codes:
public class Employee { private Set<Task> tasks = new HashSet<Task>(); } public class Task { private String name; private Employee owner; private Date startTime; private Date endTime; }It is a very typical parent/child relationship. Now, I want to add a behavior to my domain model. The behavior is: get all the processing task owned by a specified employee. If we ignore the existence of database, very naturally, this behavior belongs to Employee entity.
public class Employee { private Set<Task> tasks = new HashSet<Task>(); public Set<Task> getProcessingTask() { ... } }But if we do care the database. This design is not acceptable. Where can I get all my tasks? Are you going to load all my tasks when building the employee object? If we only have five tasks, that is OK. But if we have 5000 tasks, that probably is not acceptable. So, before the age of hibernate, we wrote:
public class TaskDAO { public Set<Task> getProcessingTasks(Employee employee) { ...//sql } }hmmm, wait a moment... Is DAO part of domain model. Yeah... you can. Just rename it to TaskRepository, then it is part of your domain model. Really? I don't believe it. DAO is not part of your domain model. Instead, it stole the logic from domain. It is the reason why our domain model is anemic. Because the getProcessingTasks was part of Employee, but now belongs to a DAO. Can hibernate solve the problem?
@Entity public class Employee { @OneToMany private Set<Task> tasks = new HashSet<Task>(); public Set<Task> getProcessingTasks() { ... } }yes! Hibernate rocks! Have we succeed? No, not yet. Hibernate can make the tasks lazy-loaded. But you only have two options. Load, or not. If you are iterating tasks inside the impl of getProcessingTasks, you still end up as loading all the tasks from the database. To solve this problem, many people tried many different ways. The goal was "injecting something" into domain, then domain can execute query itself. The attempts including using hibernate interceptor, static code instrument, aspectj... Spring gave a answer to this:
@Entity @Configurable public class Employee { private TaskDao dao; public Set<Task> getProcessingTask() { return dao.getProcessingTask(this); } public void setTaskDao(TaskDao dao) { this.dao = dao; } }The @Configurable annotation was introduced to inject DAO into domain model. Now, the domain can do what it supposed to do. Really? domain model depending on DAO made lots of people unhappy. The argued, the cyclic dependencies between DAO layer and Domain layer. The argued, domain should not be "bound" with database or any container. I personally think, it is not that a big issue... I think RoR Active Record is bounding the domain model with database, people still love it. Anyway, I started again, and looking for a more elegant solution. Finally, I found, what if I wrote this:
public class Employee { private RichSet<Task> tasks = new DefaultRichSet<Task>(); public RichSet<Task> getProcessingTasks() { return tasks.find("startTime").le(new Date()).find("endTime").isNull(); } ... }RichSet is a Set with extra capabilities (query, sum...)
public interface RichSet<T> extends Set<T> { Finder<RichSet<T>> find(String expression); int sum(String expression); }DefaultRichSet is pure in memory implementation of those operations by iterating the set. So you can new a Employee in your unit test, and test the getProcessingTasks right way. No need to worry about database or dependency injection. Do you feel better? But, where is the database? Er... This is complicated, you know. The first thing I need to do is mapping the entity in Hibernate. Er... hibernate do not like it. Hibernate expect a Set, not RichSet. I think I need to write more things to make hibernate happy:
<hibernate-mapping default-access="field" package="net.sf.ferrum.example.domain"> <class name="Employee"> <tuplizer entity-mode="pojo" class="net.sf.ferrum.RichEntityTuplizer"/> <id name="id"> <generator class="native"/> </id> <set name="tasks" cascade="all" inverse="true" lazy="true"> <key/> <one-to-many class="Task" /> </set> </class> </hibernate-mapping>What is tuplizer? It is used by hibernate to replace your set with hibernate enhanced set. So, I wrote my own tuplizer, and replace your set with my enhanced set.
public class RichEntityTuplizer extends PojoEntityTuplizer { public RichEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappedEntity) { super(entityMetamodel, mappedEntity); } protected Setter buildPropertySetter(final Property mappedProperty, PersistentClass mappedEntity) { final Setter setter = super.buildPropertySetter(mappedProperty, mappedEntity); if (!(mappedProperty.getValue() instanceof org.hibernate.mapping.Set)) { return setter; } return new Setter() { public void set(Object target, Object value, SessionFactoryImplementor factory) throws HibernateException { Object wrappedValue = value; if (value instanceof Set) { HibernateRepository repository = new HibernateRepository(); repository.setSessionFactory(factory); wrappedValue = new HibernateRichSet((Set) value, repository, getCriteria(mappedProperty, target)); } setter.set(target, wrappedValue, factory); } public String getMethodName() { return setter.getMethodName(); } public Method getMethod() { return setter.getMethod(); } }; } }In short, the code means:
employee.tasks = new HibernateRichSet<Task>(...)This version of RichSet is much smarter. It will translate your find statements from
tasks.find("startTime").le(new Date()).find("endTime").isNull();--->
DetachedCriteria.forClass(..).add(...).add(...)Now, in the domain, you can query against your collection without worrying about how the query will be done. Domain is still pure, no dependency on DAO. Domain is still all InMemory, no need to start up your container, your database to test domain logic.
Subscribe to Posts [Atom]