锁定老帖子 主题:贫血的Domain Model
该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2008-05-09
—————————————————————— Domain Model贫血是说属于Domain Model的逻辑没有放在Domain Model中。那是哪些逻辑没有放到Domain Model中,从而导致贫血一说呢?原因有很多,但是我认为最主要是Service中的那些逻辑。而这些逻辑又有一个共同的特点就是依赖于DAO,或者说需要查询数据库。Robbin的帖子:http://www.iteye.com/topic/57075,举了一个很好的例子。我取其中的一个部分在这里做演示用。 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; } 这是一个很简单的一对多的关系。现在要查找指定员工的处理中的任务。如果忽略数据库的存在,我想大部分的同志都会这么实现: public class Employee { private Set<Task> tasks = new HashSet<Task>(); public Set<Task> getProcessingTask() { ... } } 这也符合OO数据隐藏的基本原则。但是如果有数据库存在,怎么写就不那么容易决定了。如果没有Hibernate这样的ORM。那肯定是: public class TaskDAO { public Set<Task> getProcessingTasks(Employee employee) { ...//sql } } 那我觉得,这就导致了Domain Model的失血。因为没有数据库的时候,这这个方法本来应该在Employee上的,而不是在DAO上的。 如果有Hibernate呢?是不是我就可以把这段代码写到Employee里面去呢? @Entity public class Employee { @OneToMany private Set<Task> tasks = new HashSet<Task>(); public Set<Task> getProcessingTask() { ... } } 还是有问题。因为访问tasks的时候,Hibernate会去加载数据。getProcessingTask会便利所有的task。如果task的数量很多,这降极大的影响性能。所以为了能够享受到关系数据库查询速度的好处,我们要还要利用SQL。于是DAO又再次地找到了自己的位置。那么怎么解决这个问题呢?在http://www.iteye.com/topic/57075的回帖中nihongye同学提出了一个解决方案。本质来说就是不让hibernate来映射tasks,改由查询来获得。加上Spring支持的@Configurable标记,我们可以把代码写成这样 @Entity @Configurable public class Employee { private TaskDao dao; public Set<Task> getProcessingTask() { return dao.getProcessingTask(this); } public void setTaskDao(TaskDao dao) { this.dao = dao; } } 我们当然还可以把TaskDao替换成变的形式。比如http://www.iteye.com/topic/65406里firebody提到的那样。但是本质上来说,都是让Employee能够直接去使用Hibernate做查询。但是坏处是给Domain纯净分子的口实。虽然,我认为和ActiveRecord类似,entity绑定在数据库上没啥不好。另外一个缺点就是,要么仍然有一个Dao来封装查询逻辑的实现,要么Employee的实现中出现太多的hibernate api,而且写法复杂。这也就是Robbin一再强调,ActiveRecord那样的api在Java世界中不是不可以,而是实现复杂难度高的原因。注入可以解决问题,但是对Hibernate的依赖强而且写法丑陋。 那么有没有更优美的方案呢?有: 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是我自己编造的一个名字。它是一个”rich“的set。其实就是附加了一些find,sort,sum之类的操作。 public interface RichSet<T> extends Set<T> { Finder<RichSet<T>> find(String expression); int sum(String expression); } DefaultRichSet是这些附加操作的内存版本的实现。这个能解决问题么?还是不能,这时候getProcessingTasks的时候,richSet还是去遍历内部的_tasks,然后把结果过滤出来。而且,hibernate还拒绝接受这样set。为了让hibernate能够接受RichSet,我们需要这么写配置文件。 <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> <property name="name"/> <property name="salary"/> <many-to-one name="department"/> <set name="tasks" cascade="all" inverse="true" lazy="true"> <key/> <one-to-many class="Task" /> </set> </class> </hibernate-mapping> 通过指定RichEntityTuplizer,我们可以控制Hibernate的动态增强过程。 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(); } }; } } 这样,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是不是能够摆脱贫血的困扰呢?这个还需要观察。不过我认为至少是向前迈了一步了。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2008-05-09
怎么看怎么像DLinq。
不过楼主不容易,用java的方式实现了一把! |
|
返回顶楼 | |
发表时间:2008-05-09
和Linq还是不一样。Linq就是一种查询语言。你可以在colllection上做linq查询,也可以在数据库上做linq查询。但是以现在的实现而言。如果你认为tasks是collection,在Employee的task上做linq查询,那就是对collection的过滤。如果你认为tasks是数据库的表,在Employee上做linq查询,那就是sql查询。但是无法做到连接到数据库的时候做sql,不连接数据库的时候做过滤。也就是说,linq也会造成你的domain model和数据库绑定。
|
|
返回顶楼 | |
发表时间:2008-05-09
嗯嗯!感谢楼主贡献啊!
在公司内转了,应该对我们很有用! |
|
返回顶楼 | |
发表时间:2008-05-09
刚才又研究了一下hibernate,发现这种对collection包装的方法可以避免。可以直接在field上写RichSet<Task> tasks = new DefaultRichSet<Task>()。但是不能用annotation了,必须用hbm文件。而且要用自己的RichEntityTuplizer。
public class RichEntityTuplizer extends PojoEntityTuplizer { public RichEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappedEntity) { super(entityMetamodel, mappedEntity); for (int i = 0; i < setters.length; i++) { Setter setter = setters[i]; if (!(setter instanceof DirectPropertyAccessor.DirectSetter)) { continue; } DirectPropertyAccessor.DirectSetter directSetter = (DirectPropertyAccessor.DirectSetter) setter; Field field = null; try { Field declaredField = directSetter.getClass().getDeclaredField("field"); declaredField.setAccessible(true); field = (Field) declaredField.get(directSetter); } catch (Exception e) { throw new FerrumException(e); } if (field.getName() == "children") { setters[i] = new Setter() { public void set(Object target, Object value, SessionFactoryImplementor factory) throws HibernateException { try { Field childrenField = target.getClass().getDeclaredField("children"); childrenField.setAccessible(true); childrenField.set(target, new InMemoryRichSet((Set)value)); // here we can use HibernateRichSet instead } catch (Exception e) { throw new FerrumException(e); } } public String getMethodName() { return null; } public Method getMethod() { return null; } }; } } } } <hibernate-mapping default-access="field" package="net.sf.ferrum.example.domain"> <class name="Parent"> <tuplizer entity-mode="pojo" class="net.sf.ferrum.RichEntityTuplizer"/> <id name="id"> <generator class="increment"/> </id> <set name="children" cascade="all-delete-orphan"> <key/> <one-to-many class="Child"/> </set> </class> </hibernate-mapping> public class Parent { private long id; private RichSet<Child> children = new DefaultRichSet<Child>(); public void addChild() { children.add(new Child()); } public long getId() { return id; } } |
|
返回顶楼 | |
发表时间:2008-05-09
JavaEye的Java论坛板块里好久没出现这种好帖了。
|
|
返回顶楼 | |
发表时间:2008-05-09
enhance....无非就是让速度更慢而已。
天啊。。。莫非我看错了,竟然还有反射。。。 |
|
返回顶楼 | |
发表时间:2008-05-09
对于动态代码增强还有什么好说的呢?现在早就不是以前JDO做静态代码增强的年代了。Hibernate用动态代理这么多年,好像也没出啥问题。我只不过是在Hibernate的动态代理的collection的基础上再往前走了一步。而且这里也没产生动态代理,只是用反射给一个private field设置了值而已。性能损失是一次性的(一次private field setter)。根本就可以忽略不计。
|
|
返回顶楼 | |
发表时间:2008-05-09
taowen 写道 和Linq还是不一样。Linq就是一种查询语言。你可以在colllection上做linq查询,也可以在数据库上做linq查询。但是以现在的实现而言。如果你认为tasks是collection,在Employee的task上做linq查询,那就是对collection的过滤。如果你认为tasks是数据库的表,在Employee上做linq查询,那就是sql查询。但是无法做到连接到数据库的时候做sql,不连接数据库的时候做过滤。也就是说,linq也会造成你的domain model和数据库绑定。
俺用扩展方法轻松搞定http://www.iteye.com/topic/180343 |
|
返回顶楼 | |
发表时间:2008-05-09
还可以用linq。用linq to hibernate(www.ayende.com)。
扩展方法与引用一个静态方法没有本质上的区别。你没有办法在运行时切换扩展方法的实现。如果使用static来引入依赖是不好的,用扩展方法来引入依赖同样是不好的。在domain对象的依赖注入这个问题上,spring已经用@Configurable回答过了。我认同这种把DAO注入到domain对象中的做法,有效,简单。但是也招来“非议”。会有什么样的非议,大家心知肚明。为了尝试更纯净的无容器数据库依赖的实现(只依赖特定的collection接口),我才做了上面的实验。 |
|
返回顶楼 | |