JAVA 贫血的 Domain Model 的心得

来源:互联网 发布:知乎53分钟68分钟音频 编辑:程序博客网 时间:2024/06/09 17:13
好老的话题啦。拿出来炒炒冷饭。各位见谅。
——————————————————————
Domain Model贫血是说属于Domain Model的逻辑没有放在Domain Model中。那是哪些逻辑没有放到Domain Model中,从而导致贫血一说呢?原因有很多,但是我认为最主要是Service中的那些逻辑。而这些逻辑又有一个共同的特点就是依赖于DAO,或者说需要查询数据库。Robbin的帖子:http://www.javaeye.com/topic/57075,举了一个很好的例子。我取其中的一个部分在这里做演示用。

Java代码 复制代码
  1. public class Employee {   
  2.     private Set<Task> tasks = new HashSet<Task>();   
  3. }  


Java代码 复制代码
  1. public class Task {   
  2.     private String name;   
  3.     private Employee owner;   
  4.     private Date startTime;   
  5.     private Date endTime;   
  6. }  


这是一个很简单的一对多的关系。现在要查找指定员工的处理中的任务。如果忽略数据库的存在,我想大部分的同志都会这么实现:

Java代码 复制代码
  1. public class Employee {   
  2.     private Set<Task> tasks = new HashSet<Task>();   
  3.     public Set<Task> getProcessingTask() {   
  4.        ...   
  5.     }   
  6. }  


这也符合OO数据隐藏的基本原则。但是如果有数据库存在,怎么写就不那么容易决定了。如果没有Hibernate这样的ORM。那肯定是:

Java代码 复制代码
  1. public class TaskDAO {   
  2.    public Set<Task> getProcessingTasks(Employee employee) {   
  3.       ...//sql   
  4.    }   
  5. }  


那我觉得,这就导致了Domain Model的失血。因为没有数据库的时候,这这个方法本来应该在Employee上的,而不是在DAO上的。
如果有Hibernate呢?是不是我就可以把这段代码写到Employee里面去呢?

Java代码 复制代码
  1. @Entity  
  2. public class Employee {   
  3.     @OneToMany  
  4.     private Set<Task> tasks = new HashSet<Task>();   
  5.     public Set<Task> getProcessingTask() {   
  6.        ...   
  7.     }   
  8. }  


还是有问题。因为访问tasks的时候,Hibernate会去加载数据。getProcessingTask会便利所有的task。如果task的数量很多,这降极大的影响性能。所以为了能够享受到关系数据库查询速度的好处,我们要还要利用SQL。于是DAO又再次地找到了自己的位置。那么怎么解决这个问题呢?在http://www.javaeye.com/topic/57075的回帖中nihongye同学提出了一个解决方案。本质来说就是不让hibernate来映射tasks,改由查询来获得。加上Spring支持的@Configurable标记,我们可以把代码写成这样

Java代码 复制代码
  1. @Entity  
  2. @Configurable  
  3. public class Employee {   
  4.     private TaskDao dao;   
  5.     public Set<Task> getProcessingTask() {   
  6.         return dao.getProcessingTask(this);   
  7.     }   
  8.     public void setTaskDao(TaskDao dao) {   
  9.         this.dao = dao;   
  10.     }   
  11. }  


我们当然还可以把TaskDao替换成变的形式。比如http://www.javaeye.com/topic/65406里firebody提到的那样。但是本质上来说,都是让Employee能够直接去使用Hibernate做查询。但是坏处是给Domain纯净分子的口实。虽然,我认为和ActiveRecord类似,entity绑定在数据库上没啥不好。另外一个缺点就是,要么仍然有一个Dao来封装查询逻辑的实现,要么Employee的实现中出现太多的hibernate api,而且写法复杂。这也就是Robbin一再强调,ActiveRecord那样的api在Java世界中不是不可以,而是实现复杂难度高的原因。注入可以解决问题,但是对Hibernate的依赖强而且写法丑陋。
那么有没有更优美的方案呢?有:

Java代码 复制代码
  1. public class Employee {   
  2.     private RichSet<Task> tasks = new DefaultRichSet<Task>();   
  3.     public RichSet<Task> getProcessingTasks() {   
  4.         return tasks.find("startTime").le(new Date()).find("endTime").isNull();   
  5.     }   
  6. ...   
  7. }  



RichSet是我自己编造的一个名字。它是一个”rich“的set。其实就是附加了一些find,sort,sum之类的操作。

Java代码 复制代码
  1. public interface RichSet<T> extends Set<T> {   
  2.     Finder<RichSet<T>> find(String expression);   
  3.     int sum(String expression);   
  4. }  


DefaultRichSet是这些附加操作的内存版本的实现。这个能解决问题么?还是不能,这时候getProcessingTasks的时候,richSet还是去遍历内部的_tasks,然后把结果过滤出来。而且,hibernate还拒绝接受这样set。为了让hibernate能够接受RichSet,我们需要这么写配置文件。

Xml代码 复制代码
  1. <hibernate-mapping default-access="field" package="net.sf.ferrum.example.domain">  
  2.     <class name="Employee">  
  3.         <tuplizer entity-mode="pojo" class="net.sf.ferrum.RichEntityTuplizer"/>  
  4.         <id name="id">  
  5.             <generator class="native"/>  
  6.         </id>  
  7.         <property name="name"/>  
  8.         <property name="salary"/>  
  9.         <many-to-one name="department"/>  
  10.         <set name="tasks" cascade="all" inverse="true" lazy="true">  
  11.             <key/>  
  12.             <one-to-many class="Task" />  
  13.         </set>  
  14.     </class>  
  15. </hibernate-mapping>  


通过指定RichEntityTuplizer,我们可以控制Hibernate的动态增强过程。

Java代码 复制代码
  1. public class RichEntityTuplizer extends PojoEntityTuplizer {   
  2.     public RichEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappedEntity) {   
  3.         super(entityMetamodel, mappedEntity);   
  4.     }   
  5.   
  6.     protected Setter buildPropertySetter(final Property mappedProperty, PersistentClass mappedEntity) {   
  7.         final Setter setter = super.buildPropertySetter(mappedProperty, mappedEntity);   
  8.         if (!(mappedProperty.getValue() instanceof org.hibernate.mapping.Set)) {   
  9.             return setter;   
  10.         }   
  11.         return new Setter() {   
  12.             public void set(Object target, Object value, SessionFactoryImplementor factory) throws HibernateException {   
  13.                 Object wrappedValue = value;   
  14.                 if (value instanceof Set) {   
  15.                     HibernateRepository repository = new HibernateRepository();   
  16.                     repository.setSessionFactory(factory);   
  17.                     wrappedValue = new HibernateRichSet((Set) value, repository, getCriteria(mappedProperty, target));   
  18.                 }   
  19.                 setter.set(target, wrappedValue, factory);   
  20.             }   
  21.   
  22.             public String getMethodName() {   
  23.                 return setter.getMethodName();   
  24.             }   
  25.   
  26.             public Method getMethod() {   
  27.                 return setter.getMethod();   
  28.             }   
  29.         };   
  30.     }   
  31. }  


这样,tasks就不再是DefaultRichSet了。Hibernate会尝试去增强为PersisentSet,但是被RichEntityTuplizer改写为增强HibernateRichSet了。这样就形成了HibernateRichSet -> PersisentSet -> DefaultRichSet -> HashSet 的包含关系。

当用户尝试在tasks上做find的时候,就不再是DefaultRichSet来做collection遍历了,而是HibernateRichSet去拼装一个DetachedCriteria。最后当用户在查询的结果上取size()或者取具体元素的时候,这个criteria被拿去求值。

通过使用RichSet,domain model具有了对自身进行查询的能力。更重要的是,这种能力的获得,不是通过把Hibernate session注入到domain model中。domain仍然是纯净的,没有依赖于数据库的东西。而且domain是可以脱离容器使用的。new Employee出来就可以直接使用,测试。区别只是经过repository增强的entity会使用sql,而transient的entity所有的查询都是通过遍历实现的。

没有了DAO之后,Domain Model是不是能够摆脱贫血的困扰呢?这个还需要观察。不过我认为至少是向前迈了一步了。
原创粉丝点击