论坛首页 Java企业应用论坛

再次小结领域模型的种种观点

浏览 145575 次
该帖已经被评为精华帖
作者 正文
   发表时间:2005-12-15  
关于领域模型的设计问题,JavaEye已经组织过n多次大规模讨论,几乎每过一段时期就会出现一次。最近出现了一个新的趋势,Craig Walls在自己的blog上面写一篇文章,介绍如何使用Spring2.0和AspectJ的新特性给domain object注入DAO依赖,即如何实现post-instantiation,请见:
http://jroller.com/page/habuma?entry=spring_2_0_vs_the

与此同时,ajoo也给出了nuts的post-instantiation方案,请见:
http://www.iteye.com/display/ajoo/Dependency+Injection+For+Rich+Domain+Model

因此,从技术手段来上说,对于Spring/Hibernate架构,Martin的Rich domin model变得可行了,那么让我们看看究竟有哪些领域模型,以及他们的优缺点:

一、失血模型

失血模型请看
http://forum.iteye.com/viewtopic.php?t=11712
中列举的第一种模型,简单来说,就是domain object只有属性的getter/setter方法,没有任何业务逻辑。

二、贫血模型

贫血模型请看
http://forum.iteye.com/viewtopic.php?t=11712
中列举的第二种模型,简单来说,就是domain ojbect包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。
Service(业务逻辑,事务封装) --> DAO ---> domain object

这种模型的优点:
1、各层单向依赖,结构清楚,易于实现和维护
2、设计简单易行,底层模型非常稳定
这种模型的缺点:
1、domain object的部分比较紧密依赖的持久化domain logic被分离到Service层,显得不够OO
2、Service层过于厚重

三、充血模型
充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在domain object里面(包括持久化逻辑),而Service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。
Service(事务封装) ---> domain object <---> DAO

这种模型的优点:
1、更加符合OO的原则
2、Service层很薄,只充当Facade的角色,不和DAO打交道。
这种模型的缺点:
1、DAO和domain object形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。
2、如何划分Service层逻辑和domain层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序。
3、考虑到Service层的事务封装特性,Service层必须对所有的domain object的逻辑提供相应的事务封装方法,其结果就是Service完全重定义一遍所有的domain logic,非常烦琐,而且Service的事务化封装其意义就等于把OO的domain logic转换为过程的Service TransactionScript。该充血模型辛辛苦苦在domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了。

四、胀血模型
基于充血模型的第三个缺点,有同学提出,干脆取消Service层,只剩下domain object和DAO两层,在domain object的domain logic上面封装事务。
domain object(事务封装,业务逻辑) <---> DAO
似乎ruby on rails就是这种模型,他甚至把domain object和DAO都合并了。
该模型优点:
1、简化了分层
2、也算符合OO
该模型缺点:
1、很多不是domain logic的service逻辑也被强行放入domain object ,引起了domain ojbect模型的不稳定
2、domain object暴露给web层过多的信息,可能引起意想不到的副作用。

在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都已经是可行的了。但是我个人仍然主张使用贫血模型。其理由:

1、参考充血模型第三个缺点,由于暴露给web层程序拿到的还是Service Transaction Script,对于web层程序员来说,底层OO意义丧失了。

2、参考充血模型第三个缺点,为了事务封装,Service层要给每个domain logic提供一个过程化封装,这对于编程来说,做了多余的工作,非常烦琐。

3、domain object和DAO的双向依赖在做大项目中,考虑到团队成员的水平差异,很容易引入不可预知的潜在bug。

4、如何划分domain logic和service logic的标准是不确定的,往往要根据个人经验,有些人就是觉得某个业务他更加贴近domain,也有人认为这个业务是贴近service的。由于划分标准的不确定性,带来的后果就是实际项目中会产生很多这样的争议和纠纷,不同的人会有不同的划分方法,最后就会造成整个项目的逻辑分层混乱。这不像贫血模型中我提出的按照是否依赖持久化进行划分,这种标准是非常确定的,不会引起争议,因此团队开发中,不会产生此类问题。

5、贫血模型的domain object确实不够rich,但是我们是做项目,不是做研究,好用就行了,管它是不是那么纯的OO呢?其实我不同意firebody认为的贫血模型在设计模型和实现代码中有很大跨越的说法。一个设计模型到实现的时候,你直接得到两个类:一个实体类,一个控制类就行了,没有什么跨越。

关于领域模型的问题,限于时间原因,暂时不能展开详谈,待有空,写篇更加详细的文章。
   发表时间:2005-12-15  
我倒不认为从技术可行性上充血模型就没有一点问题了。

以前xiecc给的方法和Spring 2.0给的方法都严重依赖AOP。程序无法脱离AOP的预编译器运行。Spring 2.0更在domain object里面引入了spring自己定义的annotation,并且对bean name有一个强耦合。

我给的方法(其实你提供的那个链接主要讲的原理,这里这篇是使用Nuts xml之后的配置方法:http://docs.codehaus.org/display/YAN/Simple+Rich+Domain+Injection),虽然不依赖AOP,也没有引入任何domain object对框架的依赖,但是它并不是100%的方便。你不能new,必须要通过factory;hibernate等第三方库必须被抽象在dao等接口下。


总而言之,都太复杂,不够简单好用。我的感觉,充血模型是马丁说话不过大脑的一个例证。它也许有它的方便性在里面,但是无论从OO理论上,还是实践中的可行性上,它都不是绝对比贫血模型更优越。最多只能算各有千秋。
0 请登录后投票
   发表时间:2005-12-15  
to robbin:
我认为你有关“充血”DomainModel的观点,是对这种模式的一种误解。
目前,俺也不想长篇大论,会有时间来好好讨论下的。

关于DomainObject,可以从是“Active”还是“Passive”的方式的视角来观察。“贫血”采用的是Passive,老马定义的是Active。

至于DAO,它算不上DomainLogic。而是同DomainLogic正交的一个方面
0 请登录后投票
   发表时间:2005-12-15  
ajoo 写道
总而言之,都太复杂,不够简单好用。我的感觉,充血模型是马丁说话不过大脑的一个例证。

过分。
0 请登录后投票
   发表时间:2005-12-15  
赫赫。
其实我想收回我那句话: 贫血模型在设计模型和实现代码中有很大跨越。
如果现在要重新辩论一番的话,我想我的观点是:
简单就是美。 既然可以吧逻辑放到一个对象体系中去,我不认为把逻辑分散到两个层面的对象体系中是一种设计技巧,我认为它是一种不得已而为之的做法。
为什么现在丰富领域模型很难做到,我所知道的原因有以下几点:
1)毕竟IOC很难在new中注入,虽然可以通过各种办法,但是总归不是很优雅,ServiceLocator最多是一种比较丑陋的折衷。
2)事务的管理,要获得事务管理,依照Spring的做法,也是需要proxy对象的,这也完全阻碍了领域模型中需要事务的逻辑方法,不得已的一个折衷是,还得外包一个Service,不过这个Service比起那些贫血的领域模型要薄得多。


这两个折衷,让我实在不敢越雷池一步。代码写起来很别扭。
正如我在一个贴子里提到的,我可以这样声明,声明既所得,意味着我只要声明了,那么我new出来的对象就具备了这些资源,只不过这些资源都是从Spring里IOC进来,事务也是通过Spring的事务管理容器来做。
看看这段代码:
public class Teacher{
  @bean(name="teacherDAO");
  private TeacherDAO teacherDAO;
   @id(generator=generatorType.AUTO);
  private int id;
  private String name;
  @oneToMany(cascade=CascadeType.ALL);
  private List<Student> stduents = new ArrayList<Student>();;
  @transaction(strategy="required");
  public void removeStudent(Student student);{
            this.sudents.remove(student);;
             teacherDAO.merge(this);;
   }
   @transaction(strategy="required");
   public void delete();{
              teacherDAO.removeTeacher(this);;
    }
    @transaction(strategy="required");
    public void save();{
           teacherDAO.createTeacher(this);;
    }

   public void complextLogic();{
           Student lastStudent =students(studeints.size(); -1); ;
           Teacher teacher = Teacher.findOperTeacher();;
           lastStudent .setTeacher(teacher );;
           teacher .update();;
            
   
   }
   
 


}


在APP中可以这么写代码:
  new Teacher();.save();; 


在具体的门面Action中:
比如对应complextLogic有这么一个ComplextLogicAction:
public class ComplextLogicAction implements Action{

      private Teacher teacher;
      private Stirng teacherID;
       public void setTeacherID(String teacherID);{
                   this.teacherID = teacherID;
               teacher = Teacher.findByID(teacherID);;
       }
       public void execute();{
                 teacher.complextLogic();;
       }
}


看起来verySimple .

如果一个逻辑似乎不能放到某个对象上,那么可以这样来

new SomeComplexLogic();.doLogic();;
0 请登录后投票
   发表时间:2005-12-15  
引用
正如我在一个贴子里提到的,我可以这样声明,声明既所得,意味着我只要声明了,那么我new出来的对象就具备了这些资源,只不过这些资源都是从Spring里IOC进来,事务也是通过Spring的事务管理容器来做。

想法很好,有点类似ejb3的思路。

不过,问题仍然会回到rich domain inection,因为如果你一旦自己new Teacher(),而不是从spring注入Teacher,spring仍然无法给你提供事务管理。

这个东西比Craig Wall的blog里面提到的还要复杂一点,因为你要求的是proxy,而不是简单地注入几个依赖。我恐怕用spring的transaction proxy都是不可行的(new Teacher()不返回Teacher而是返回一个proxy?),必须用aspectj来静态织入。
0 请登录后投票
   发表时间:2005-12-15  
to firebody:你举的代码例子是一个胀血的模型,可不是Martin的充血模型。Martin的充血模型中可不允许你直接在Web Action中使用Teacher的业务方法。而是必须使用Service来隔离。

to partech:你好像也误解我的意思,我可从来没有说DAO就是domain logic,这是两码事。
1 请登录后投票
   发表时间:2005-12-15  
ajoo 写道
引用
正如我在一个贴子里提到的,我可以这样声明,声明既所得,意味着我只要声明了,那么我new出来的对象就具备了这些资源,只不过这些资源都是从Spring里IOC进来,事务也是通过Spring的事务管理容器来做。

想法很好,有点类似ejb3的思路。

不过,问题仍然会回到rich domain inection,因为如果你一旦自己new Teach(),而不是从spring注入Teacher,spring仍然无法给你提供事务管理。

这个东西比Craig Wall的blog里面提到的还要复杂一点,因为你要求的是proxy,而不是简单地注入几个依赖。我恐怕纯粹用spring的transaction proxy都是不可能的,必须用aspectj来静态织入。

ajoo同志,我不认为这是难事,至少你所想的东西我还是在我笨笨的脑子里过了几遍的。 我想我已经有大概的思路了,完成手头的紧要的项目后,立即作这个实现,我会以Spring作为底层基础设施。
实现基本思路就是byteCode Change. 需要预编译。 这里需要提几点:
1) 侵入程度的有效控制, 我们需要提供一套足够薄的织入代码,这些织入代码是可以由开发人员扩展和实现的。 从领域模型类---〉Spring容器之间会由这么一个构架:
hook layer---&gt;your impl layer-----&gt;Container

这里的hook Layer是指被植入到领域实体的几行钩子级别的代码,这些代码可以标准化,宗旨是足够简单和robust .
我基本的思路是植入一些事件通知钩子,比如进入方法,返回,Exception,还有就是对资源的IOC注入钩子。

your impl layer是指你需要做的扩展,类似一套事件监听者。
你可以实现事务的监听,拦截器的实现等等。

COntainer就是指你可以选择不同的容器。比如Spring或者Jboss等等。

这里,如果AspectJ可以较容易的扩展,那么我想我最好不要自己做bytecode的wave了。
2) 预编译需要多一个步骤。
  这个问题已经可以通过IDE增加一个插件来解决,我们早已作了这个实现。
  源代码的修改,自动编织被修改的源代码,相当于编译一样透明。
0 请登录后投票
   发表时间:2005-12-15  
robbin 写道
to firebody:你举的代码例子是一个胀血的模型,可不是Martin的充血模型。Martin的充血模型中可不允许你直接在Web Action中使用Teacher的业务方法。而是必须使用Service来隔离。

to partech:你好像也误解我的意思,我可从来没有说DAO就是domain logic,这是两码事。

赫赫,
这个必须有点莫名其妙,增加多中间一层和少一层一定要套得这么清楚吗?
对于可能的复杂业务,确实应该通过Service来做的,不过我不喜欢叫它Service,我喜欢称之为LogicObject。

如果你觉得胀血,我想分离到Service Object也是一种很好的重构。 哈哈
0 请登录后投票
   发表时间:2005-12-15  
不是难事。就看你做的好不好而已。要做成象spring 2.0那样要求domain object里面还要指定bean name这种反向依赖就不怎么样了。

这个问题,如果要允许new Teacher(),aspectj就是必须的。预编译的代价不管你怎么简化,怎么用ide,也只能减轻,不能消除。不过,逼不得已,做做看吧。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics