• 2004-11-16

    关于Transaction Script、Domain Model模式讨论

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://bpnrtech.blogbus.com/logs/499362.html

    关于Transaction Script、Domain Model模式讨论

    这是 domain driven model design该如何设计的几个连接

    http://gigix.blogdriver.com/gigix/166013.htmlhttp://gigix.blogdriver.com/gigix/168486.html
    http://gigix.blogdriver.com/gigix/168634.htmlhttp://gigix.blogdriver.com/gigix/169972.html

    现将其精华谪录如下:

    我们现在的做法是Transaction Script模式http://www.martinfowler.com/eaaCatalog/transactionScript.html)。实体(也就是需要持久化的对象,有时我们也说它们就是领域对象)只是原始数据的对象形式,或者加上一些最简单的操作(只涉及本身数据的简单处理)。DAO 负责将数据转化为实体对象,我们通常用Hibernate来实现。业务逻辑放在service里,每个service方法是一个Transaction Script。AppFuse(http://https://appfuse.dev.java.net/)也是按这种方式架构的。
     
    Martin Fowler更倾向于Domain Model模式(http://www.martinfowler.com/eaaCatalog/domainModel.html)。领域对象不仅是原始数据的对象形式封装,而且包含几乎所有重要的业务逻辑。领域对象可以直接访问数据库,因此DAO不是必要的,而是通过一个他称为 “mapper”的东西提供O/R映射。由于领域对象封装了业务逻辑,service只是一个很薄的facade,提供面向服务的接口。Spring的 JPetstore范例就是按照这种方式架构的。但是,JPetstore的业务逻辑相当简单,我草草看了一下,似乎领域对象没有直接操作持久化逻辑,因此这并不是一个很有说服力的例子。或许更有说服力的是Pluto(http://jakarta.apache.org/pluto/)。在Pluto这里,需要持久化的大多是些配置信息(包括portal本身的配置、portlet的配置和用户配置),它并没有使用DAO,而是直接通过XML binding持久化到XML文件。
     
    我不知道应该如何选择这两个体系结构模式。我不知道Transaction Script模式的缺点在哪里。我不知道Domain Model模式用起来会有什么困难。我不知道Martin Fowler为什么如此激烈地批评“贫血的领域对象”。这让我感到困惑和苦恼。我要首先再看一遍PoEAA的相关章节,再想想,再问问。我不喜欢感觉自己像个白痴。
    。。。。。。
     

    评论人:charon

    2004-05-23 15:08:51   

    hehe,tell和ask有区别吗?
    在agent 交互的讨论中,一个一致的结论是实际上只需要tell就能把所有东西表达出来(虽然每个基于speech-act的语言不仅仅有tell原语)。而实际上,tell也好,ask也好,或者别的什么东西,一个动作或者交互的目的都是对接收方产生影响,所谓的原语是从影响的角度来划分的。
    tell或者ask和是不是oo一点关系都没有。

    。。。。。。

    评论人:potian

    2004-05-22 13:49:04 

    我说了一万遍了,service层的目的两个:
    1 一般是客户代码,随客户而变
    2 一般是服务代码,封装内部模型的复杂性

    在分布式程序中的一个副作用是提高效率(通过DTO)

    - 评论人:partech

    2004-05-22 11:53:35 

    补充一下,关于Transaction Script,Table Module,Domain Model 的选择从martin Fowler的那张图可以看出他的选择。我没有系统的研究过三者的差异,我感觉这对的。按照图上的含义,对于小型项目首选应该是Table Module,随着业务逻辑的复杂程度的增加Domain Model 将是首选。
    在.NET环境中因为有ADO.NET类似“内存数据库”的支持使得使用Table Module很自然。Java环境中也可以通过组合纪录集的形式实现Table Module。对于批量类型的对象操作可以考虑Transaction Script,因为单个记录或者单个对象操作效率可能太低。

    服务层本质上是facade,确实不应该包含业务逻辑。

    - 评论人:partech

    2004-05-22 11:24:22 

    按照你们现在的做法,业务层还不能说是面向对象,因为对象的数据和操作这些数据的行为不是封装的而是分离的。
    要发挥面向对象的所有威力需要将整个Domain层对象化,行为和数据必须封装,这是最基本的,然后还要考虑分层,对业务层进行抽象等等。

    你们的做法我说一个缺点,不知道你们的应用同参与者是否只有一种交互形式?比如只通过WEB。然而参与者进行业务可能有很多种方式,拿银行来说吧,可以通过柜台,也可以通过ATM,还有电话,和网上银行。很明显这些应用后台都需要服务,而且这些服务的交互方式是不同的,也就是用例不同。所以Service层会不同,DTO会不同。但是如果我在这些应用中都进行转账业务,那么不管我使用的是哪一种途径,负责转账的代码都该只有一份。Service层的代码没多少复用价值,真正需要考虑的是Domain层的复用。
    领域对象不单包含实体对象,还应该包含控制对象。很明显转账的整个操作没法划给单个对象,但是分解出的转出和转入操作还是可以作为帐户实体的方法的。
    不过我感觉有人常常将领域层的控制对象同应用层的服务对象混淆。区分两者很简单,一个是不针对特定应用的,一个是针对特定应用的,也就是系统中烟囱的部分。
    服务层充当的是系统中烟囱的部分,所以在不同应用中不考虑复用。或者说尽量避免进行复用,这样各应用就可以独立演化了。
    可以看看下面的网站,作者的书里提到了很多如何进行领域层开发的方法。
    http://domaindrivendesign.org

    。。。。。。

     下午Jacques Lebrun帮忙找到了Domain Driven Design,还没来得及看。Martin Fowler说应该由Domain Model负责访问持久化逻辑,并借助Data Mapper封装持久化的实现细节。但是,我竟然找不到这样的一个例子。Spring JPetstore细看之下原来也是Transaction Script,只不过service的方法都非常非常简单,前天乍一看还以为和我们做的Transaction Script有什么不同呢。看JPetstore唯一的收获是:不要使用DTO,用轻量级持久化框架(例如Hibernate或者iBatis),直接把实体对象到处传。

     
    Domain Model模式看了仍然是半懂不懂,很多细节上的问题想不清楚。比如说,领域对象(或者说,实体)的创建和查找(对应于DAO的create方法和 load方法)由谁来调用?仍然在service里调用吗?如果要放到实体类,在创建/查找操作之前,没有一个具体的实体对象存在的,那么就只能放在一个 static方法里了,这是一个好的做法吗?
     
    到哪里可以找到一个Domain Model体系结构的例子?我想看看究竟应该怎样去做。如果到明天晚上还没有进展,我就写信去问Martin Fowler本人。
    。。。。。。
     
     

    评论人:Windy

    2004-05-24 20:09:22 

    领域对象(etc.)调用finder调用mapper调用领域对象的构造函数创建原来已经存储在数据库里的领域对象,看起来好像是绕口令,实际上可以这么做,如果你真的想用DomainModal+DataMapper的话 :P

    - 评论人:partech

    2004-05-24 13:31:58   

    领域对象(或者说,实体)的创建由业务控制对象,其他领域对象创建或者在查找时自动创建。
    领域对象的查找同样可以由业务控制对象,其他领域对象或者其他查找方法调用。
    服务层最好调用业务控制对象,而不是直接调用实体对象。
    DDD里面的Repository可以回答你余下的问题。

    。。。。。。

    评论人:potian

    2004-05-23 20:14:27 

    模型不复杂的情况这样做就足够了,典型的CRUDSevice层在WEB应用中的作用特别明显,我以前很多桌面程序没有这样一个明显的service层,而如果只需要CRUD,DTO-》Service-》DTOFactory-》DAO就足够了,并且确实是最佳的方安。

    但当模型复杂的时候,Service就需要通过模型来完成工作,或者从Service得到合适的领域对象,直接利用这些对象的行为进行操作 我在绍兴、兰亭玩

    - 评论人:gigix

    2004-05-23 20:05:35   

    但是OSUser和Delphi程序和web应用,我觉得有点不同,也说不上来究竟是哪里不同。恰恰是web应用,我发现好多人都是照我们现在这个做法来做的,比如spring jpetstore和appfuse。
    今天下午本来想到你家去问你这个问题顺便蹭饭的,可惜你不在。

    - 评论人:potian

    2004-05-23 19:59:25 

    gigix,不要这样大惊小怪好不好,这样的例子到处都是,譬如OSUser。或者我以前写过的任何一个delphi程序,或者我写过的任何复杂一点的程序

    而事实上没有一个纯粹的什么Domain或者纯粹的Transaction Script,(我对这些名词越来越讨厌了),很多时候需要混合,  譬如service-》model-》〔Manager〕DAO,或者是直接的service-》manager-》DAO

    。。。。。。

     

    Potian几句话说得我好象有点开窍了。原来,采用什么体系结构模式取决于我的实体(领域对象)干什么、我要持久化什么。如果像Pluto、 OSUser,领域对象本身有复杂的功能,持久化的是一个功能组件的状态,Domain Model就比较合适。而我们做的很多web应用,实体代表的是简单的数据(比如“用户”,甚至是某种公文),业务操作大多是对领域对象的CRUD操作, Transaction Script就挺合理。我感觉桌面应用和这种web应用好象有点区别,原来就是这个区别:它们的业务操作是做了不同的事。
     
    刚才对G-Roller做了一点修改,取消了DTO(http://www.aspectoriented.org:9080//space/2004-04-22#DTO?_我需要吗?),直接把实体传到表现层,感觉除了让程序变简单之外,并没有什么影响。不过有一点:接口上好象不那么漂亮。比如说,User有个getPosts()方法,但User和Post的关联是lazy的,如果直接在表现层调用User.getPosts()方法,就有可能来一个 LazyInitializationException。当然,表现层本来就不应该这样获得Post对象,但这个接口是不是会给使用者造成困惑呢?好象又有点不开窍了。
    。。。。。。
     

    评论人:potian

    2004-05-24 04:58:27 

    模型的复杂性是问题域本身决定的,而不应该有采用何种体系结构来决定。
    至于模型的持久有谁来负责还是由模型的复杂性来决定的,一个简单的没有多少行为的模型实际上就是一个实体模型(关系模型),它们的持久可以通过 O/Rmapping(或者JDBC)完全解决,因此我们可以通过DAO完全解决掉,这样就成了Service->Manager->DAO ->Entity(这时候领域对象退化为简单的实体对象),但是如果简单的关系模型不足以反映领域内部对象的复杂性,那么这些领域对象还需要依赖于多个管理其他领域对象的Manager来处理。在这种情况下,出于对方便利用IOC考虑,一般并不是在领域对象内部持有这个Manager属性,而是领域对象的某些方法里面可能会有Manager这样的参数,它们来自IOC到Service的注入,或者来自客户端通过IOC容器得到,然后显式地在调用该方法的时候传入。

    - 评论人:potian

    2004-05-24 04:34:04 

    有了这些基本的前提,就可以看到,如果领域模型并不复杂,基本的操作都是CRUD,那么我们可能会选择第一种方式,因为这个时候Service不会复杂到无法起到Service本身作用的程度,这个时候也没有多少东西可以复用。复用是以行为为核心,而不是状态。

    如果是领域模型比较复杂,那么完全通过Service就可能导致Service非常复杂,这个时候Service层就作用不大了。我们会提供一些简洁的Service方法来获取不同的领域对象,然后把领域对象直接暴露给客户端使用。

    还有一个影响使用何种Service的情况就是客户端的状态问题,假设我们的领域对象比较复杂,但客户端和领域对象处于不同的JVM,那么一种方式利用 Remote对象存取领域对象,例如EJB里面的远程对象,这种方式可以直接操作领域对象,但是却会造成性能上的问题,所以就会产生DTO这样的模式。



    - 评论人:potian

    2004-05-24 04:09:02 

    这两种情况下,service的作用和范围就大大的不同了
    第一种方式下,所有的工作必须经过Service,好处是接口简单,很少需要直接和背后的对象打交道,但坏处则是Service代码经常需要随着客户界面或行为的变化而变化,去实现新的接口方法,新的对象交互方式

    第二种方式,service的主要作用是能够新建和获取(包括查询方法,通常用来统计和查询跨领域对象的数据和对象
    )到领域对象,绝大部分的操作度直接在领域对象上进行,坏处是需要知道很多后面对象的接口和行为,但好处是客户端的变化通常不影响到服务端的变化。




    - 评论人:potian

    2004-05-24 03:24:32 

    没有例外,因为Session一直还在

    在Web 应用程序中出现很多疑惑的原因是HTTP的无状态,在一个桌面应用程序中,我们可以一直在客户代码中持有这个领域对象,不断地对这个对象进行操作,但在 Web应用程序中,这个就很难,因此通常必须依赖service来进行“一记头“的操作。传递的数据通常是简单的数据类型或者是简单的数据类型组装起来的 Bean对象。

    而WebWork"官方“宣称有两种方式,一种把Action作为简单的控制器,这是“传统“的方式,这种方式就可能直接导致必须使用service,而存取的数据通常是原始类型的数据。而另外一种方式则是所谓的领域丰富的Action,因为Xwork的ognlstack和 Xwork的action不需要象struts一样依赖于任何其它(例如serlvet),因此可以(在Action方法或者在Interceptor) 中通过Service得到领域对象,然后直接在界面上存取领域对象,这种方式比较象传统的桌面应用程序。另外Xwork可以直接把界面上的简单数据类型转换为对象。

    。。。。。。

    名称:
    用Domain Model替代Transaction Script
     
    意图:
    你的业务层采用Transaction Script模式实现
    你希望将实现方式改为Domain Model模式
     
    Bad Smells:
    • Long Method / Large Class——由于放置Transaction Script的service组件涉及全部业务逻辑的实现细节,其方法很容易变得冗长;如果对其使用Extract Method重构,则会使service实现类变得庞大。
    • Duplicate Code——不容易从现有的Transaction Script中找出可复用的代码,因此常常重复实现类似的逻辑。
    • Switch Statements——在领域对象有继承关系时,Transaction Script需要对不同类型的领域对象做不同的操作,此时容易造成基于类型的switch语句。
    • Data Class——领域对象只携带了数据,作为对象的职责不够。
     
    重构过程:
    1. 使用Extract Method,消除Transaction Script中的重复代码。
    2. 使用Move Method,将领域对象固有的业务逻辑移到领域对象类中。
      • 如果该方法需要使用DAO,则将DAO作为参数传入。
      • 如果该方法需要使用其他service,说明该方法并不适合存在于特定领域对象类,而应该仍然存在于service组件中。
      • 如果领域对象是一个继承体系,将业务逻辑移到其基类中。
    3. 如果第2步搬移的方法中有switch语句,使用Replace Conditional with Polymorphism消除之,将业务逻辑分配到领域对象的各个子类。

    。。。。。。

    评论人:gmark

    2004-05-26 11:26:16   

    Domain应该关心持久化吗?我觉得应该把持久化的工作放在Service里做,XPlanner可以参考,不过它的所谓domain也挺单薄的。它没有做DAO,也没有分DTO和PO,直接在Action里开Session处理就完了。非常简单和清楚。

    - 评论人:UltraFool

    2004-05-25 20:54:52 

    我觉得把DAO作为传参数不好,至少
    1)这样似乎违反了OO封装原则?客户端还要在调用方法的时候还要准备你依赖的东西,一一了解你的实现细节。如果DAO能传,service为何就不能传。 IoC整个都是把依赖关系都建立在类上,而这样却把它提到了方法上。何况domain不但是暴露给service,如果没有DTO它还会直接暴露给客户端。
    2)如果Domain有N个方法都要DAO怎么办?多麻烦啊,而且这个数量多少取决于你的O/R Mapping的能力,hibernate也许直接能映射,不需要传DAO,但JDBC就不行,你的应用就没有移植性

    。。。。。。

    评论人:Ronald Matt

    2004-05-25 11:02:25 

    http://timeandmoney.sourceforge.net/

    这么Cool的代码是重构不出来的,因为重构只有Code Smell作为输入。
    那究竟漏掉了什么?呵呵。

    - 评论人:iseeisee

    2004-05-25 10:48:19   


     

     这样:
    service -----dao 
        |                |
        |                |
      domain-------

    or:

    service -  domain -  dao

    - 评论人:iseeisee

    2004-05-25 10:12:27   

    哦,这样也可以,我可能有点纯化了,
    不过我想这样会不会让很多接口都包含dao的传递,而不是清洁的逻辑参数呢???
    而且如果你的domain 对象包含其他domain对象,
    这样2,3层的就更麻烦了。


    - 评论人:透明

    2004-05-25 09:52:08 

    DAO不是已经注入到service里了吗?service再统一调度domain object和DAO,把DAO作为参数传入。我觉得这个依赖关系挺好的。

    - 评论人:iseeisee

    2004-05-25 09:44:07   

    传入dao?
    这样是否有违分层的原则:只与下一层通信。

    我觉得dao也没必要注射了,直接全局访问
    ServiceLocator.getXXXDao()

    >>如果该方法需要使用其他service???

    假设
    1.
    UserChanagePasswordService...
       changePassword(String username,String password){
          User u = UserManager().getUser(username);
          u.changePassword(password);
       }    

    User...  
      void changePassword(String password){
         _password = password;
         ServiceLocator.getUserDao().save(this);
         ServiceLocator.getMessageService().send("成功修改密码:"+password);
      }


    2.
    UserChanagePasswordService...
       changePassword(String username,String password){
          User u = UserManager().getUser(username);
          u.changePassword(password)
          //_messageService可以注射进来,
          _messageService.send("成功修改密码:"+password);
       }    
    User...  
      void changePassword(String password){
         _password = password;
         ServiceLocator.getUserDao().save(this);
      }

    呵呵~~~~,也分不出哪个更好些,
    甚至可以:
    user.addPasswordChangeListener(xxxxxxx);
    不过这太难了,不好吧??


    - 评论人:moxie

    2004-05-25 09:11:19 

    看了gigix的blog,我意识到我们以前忽略的一个问题:为了项目层次的清晰(完全独立的Domain Model层、DAO层、Service层),而把Domain Model的功能剥离到Service层了,Domain Model只是一个简单的实体(实体最好不要涉及到业务操作)。层次分清晰了,却与面向对象的原则相斥了,也给Service层带来了不必要的负责。所以:属于Domain Model的功能必须按照面向对象的法则给其归属回去,Domain Model不能实现的业务再有Service来实现。

     

    2004年10月18日 17:21


    收藏到:Del.icio.us