jdon007
2013-05-06 13:10
先给出我的观点:

业务对象除了一些固有的行为(比如有些对象的内部状态位的管理),尽量不要绑定行为。行为最好根据业务场景(上下文)分配给角色来完成,合法性校验就是依赖于上下文的一种行为。

比如这个例子中,涉及Customer(顾客),Paperboy(报童),Wallet(钱包)三个业务对象,BuyPaperContext(买报)一个业务场景。

付款的代码既不适合放在Payperboy中,也不适合放在Customer中,适合放在BuyPaperContext中,如果场景涉及多个角色,可以在BuyPaperContext抽象出一个Payer的角色,把付款的行为绑定在Payer身上。

如果多个业务场景的角色存在共性,可以提取抽象类,场景也是如此。场景之间的交互一般是通过异步消息的方式,不同场景间的角色或聚合根之间的交互也是如此,而同一场景内的角色之间的交互一般以同步的方式进行。

再说原因:

文章中已经指出了放在将付款行为放在Paperboy与现实不符合,那么放在Customer就与现实符合了吗?以日常生活为例,去超市时,经常用到刷卡(卡相当于钱包),可是刷卡人可能不知道

卡里还有多少钱,只有通过机器来进行检测,检查顾客的卡的余额,不是由顾客来完成的。

好,也许你会说钱包跟卡不同,那我举一个极端的情况,比如顾客本身可能是个残疾人,没有双手,那么TA如何查看钱包的余额呢?也许TA真的可能让报童帮TA检查一下呢。

文章在结尾出提到一个比“耦合和内聚”更严谨的概念:正交性(Orthogonality)。这个概念在数学上很常见,用于指导代码的设计其实很有用。“正交”(Orthogonality)代码设计,或许可称为无冗余设计。

相似的东西看出共同点,算不了什么,看出它们真正不同的地方,更有意义;不同的东西看出不同点,也算不了什么,看出实质相通的地方,更有价值。这也是我对“正交”这一概念非数学化的理解。

比如,看出strategy和bridge的UML图形上的相似性,而看不清其语义上的正交性,又比如,observer和command其正交的地方又是什么呢?诸如此类的问题,不想清楚,想实现并运用好它们,恐怕需要运气。

把精力放在API或业务场景的“正交”设计上,比花在业务对象职责上,更容易也更清晰。因为业务场景一旦确定下来,角色也随之确定,很自然地,也会顺利地找到适合的业务对象、业务规则。

SpeedVan
2013-05-07 05:18
我大致赞同jdon007的观点。

一、我认为对象“只能”拥有“在逻辑上只与自己相关”的方法或行为,如wallet拥有币值换算等。聚合根内部对象只能聚合根调用就是这一规则的另一方面体现。

二、更进一步地,我认为状态与逻辑分离是必要的。所以我不推荐在进行业务逻辑时对对象进行任何更改,一切以值进,以值出。对场景的理解可以变为这样:场景是发生前与发生后的纽带。

三、对于可以更改状态的对象,我认为是一种异类,或者说逻辑上是异类,相当于Haskell中Monad的IO Monad(修改就相当于IO操作),不可避免,但绝不可以混淆。

在例子中,Customer(顾客),Paperboy(报童),Wallet(钱包)三个对象,到底怎么分配没有定则。Wallet的确可以放到Customer内,这是一种整体的看法,但不能修改值。付款代码的确不应该放在3个中的任何一个,因为与我第一条相悖。而BuyPaperContext中调用这3个或其中2个对象,又正好满足第一条,即使引入角色也必须满足第一条。

PS:其实这一切是状态引起的,不搞定状态,逻辑上仍然会产生各种问题。

banq
2013-05-07 07:56
看来大家对getPayment是否放在Customer中有不同意见。我想角度虽然不同,应该有一个原则,每个角度都不能发生逻辑悖论,也就是逻辑不一致性。

该getPayment方法核心是检查钱包是否足够,然后扣除取出的钞票。如下:

public float getPayment(float bill) {
   if (myWallet != null) {
      if (myWallet.getTotalMoney() > bill) {
         theWallet.subtractMoney(payment);
       return payment;
      }
    }
 }
<p>

这一行为如果不放在Customer中,比如放在一个参与支付场景的角色中,可能会发生支付主体和支付客体两者分离的局面,当然,也可以将支付场景的角色Mixin混入Customer中(DCI),但是这种运行时组合方式无法在设计阶段显式突出逻辑一致性。

逻辑一致性应该在设计或写代码时突出显式出来,让别人看到你的代码马上知道你的设计意图是什么。

两种行为设计方式比较:

1.固有行为,也就是基本职责。

优点:显式突出逻辑一致性,缺点:不容易确定是否是基本

2.角色场景行为,采取DCI混入方式。

优点:理解方便,有用的锤子,缺点:没有显式突出逻辑一致性

我再举一个例子,表达如果没有显式突出逻辑一致性导致的后果。

public class A { 
        private int lower, upper; //两个状态值
        public int getLower() { return lower; } 
        public int getUpper() { return upper; }
       ….setter方法….
   }

<p>

A模型要求逻辑上一致性是:状态值lower必须小于状态值upper,lower<upper。但是A类代码并没有显式突出这种逻辑约束。

如果我们在某个场景类或服务类中对A进行操作:

class AService{
    private A a;

     public void setAUpper(int value){
        if (value < a.getUpper()) 
             a.setLower(value);
     }

   public void setALower(int value){
       if (value > a.getLower()) 
             a.setUpper(value);
    }
}

<p>

逻辑一致性判断被放在了客户端或service中,而不是在模型A内部,在多线程环境下会造成A的状态结果混乱,会出现A.upper<A.lower情况。

假设A的lower和upper的初始值是(0, 5);

同时下面两个请求事件发生:

一个客户端请求线程A: setLower(4)

一个客户端请求线程B: setUpper(3)

A的状态是:lower和upper是 (4, 3)

破坏A模型逻辑一致性要求lower<upper。

ericyang
2013-05-07 10:40
2013-05-07 07:56 "@banq

"的内容

逻辑一致性的问题 ...

呀,这里回复咋有点困难,按钮不好找啊。。。

插一句,你们讨论的走偏了,一个在说read,一个在说write,所以,,,都对。。。能否统一下,看看有没有反对,然后再继续,否则你们3个说再多也没意义。。。

附,banq,说明你这个例子有点避重就轻了。。。

ericyang
2013-05-07 10:56
一?为什么一定要我选中一句,才能回复,呵呵。。。

多啰嗦下,DDD里面有个policy层,负责策略性东西,看看是不是适合这些isavailable啊,enoughMoney啊,它可能围绕在聚合根那里,也蛮合理。。。

另外一个话题供大家继续,有人纠结于DCI,先有C啊还是先有D,或者先有I,呵呵,行为先行,对象先行,模型先行,都来了。。。是不是有点像唯物和唯心的争论?

你们继续,我旁观,呵呵

猜你喜欢