迪米特法则(Law of Demeter)与领域模型行为

13-05-05 banq
                   

领域模型的行为设计中我们提到

2013-04-22 15:37 "@banq

"的内容

我们把A对象自身固有行为看成是A的一种能力,而把需要依赖其他对象的方法称为交互行为。哪些属于A的自身方法?哪些属于交互方法?设计思路和方法是如何考虑的? ...

那么什么是对象的固有行为?我们认为是那些保证该对象逻辑一致性的行为,称为对象的基本职责,保证自己的存在。

迪米特法则(Law of Demeter)则详细地定义了对象的方法行为,其定义是:

一个对象的方法只应该调用下面对象的方法:

1. 可以调用自己的方法

2. 参数对象的方法

3. 创建自己或初始化时涉及到其他对象的方法

4. 它的直接组件的对象的方法(聚合体内部等)

迪米特法则实际从一个公理原则角度对对象的行为设计进行了界定,举例如下:

顾客有一个钱包,PayBoy收款员要求顾客支付,首先,顾客对象如下:

public class Customer {
 private String firstName;
 private String lastName;
 private Wallet myWallet;
 public String getFirstName(){
 return firstName;
 }
 public String getLastName(){
 return lastName;
 }
 public Wallet getWallet(){
 return myWallet;
 }
}

<p>

钱包:

public class Wallet {
 private float value;
 public float getTotalMoney() {
 return value;
 }
 public void setTotalMoney(float newValue) {
 value = newValue;
 }
 public void addMoney(float deposit) {
 value += deposit;
 }
 public void subtractMoney(float debit) {
 value -= debit;
 }
}
<p>

收款员进行收款时代码如下:

//这段代码是位于Payboy类中:
 payment = 2.00; // “I want my two dollars!”
 Wallet theWallet = myCustomer.getWallet();
 if (theWallet.getTotalMoney() > payment) {
    theWallet.subtractMoney(payment);
 } else {
   // come back later and get my money
 }
<p>

这段代码的意思是,检查顾客的钱包中余额,是否足够,然后支付。

注意,检查顾客钱包中余额这一功能是在PayBoy中实现的,PayBoy有权检查顾客的钱足够吗?应该是顾客知道自己钱包余额是否足够。

如果类似Payboy这样调用者进行下面代码:

myCustomer.setWallet(null);

这不是将顾客这个对象的钱包清空了吗?

这实际就是破坏了顾客这个对象内部的逻辑一致性,顾客对自己的钱包拥有支配权,不能随便将钱包的操作暴露给外界。

也就是说问题出在Customer,它只有setter/getter方法,是一种纯粹的数据结构,是一种失血模型。

我们需要重构Customer,将保证顾客逻辑一致性的行为显式的表达出来,顾客应该拥有这样的基本职责:对自己钱包余额情况有足够了解。

代码实现如下:

public class Customer {
 private String firstName;
 private String lastName;
 private Wallet myWallet;
 public String getFirstName(){
 return firstName;


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

我们看到,将钱包检查验证是否足够放在Customer这个对象中,这实际也回答了“关于领域驱动设计中的“合法”性校验?”:

http://www.jdon.com/45363

这样Payboy的调用代码如下:

payment = 2.00; // “I want my two dollars!”
 paidAmount = myCustomer.getPayment(payment);
 if (paidAmount == payment) {
 // say thank you and give customer a receipt
 } else { 
 // come back later and get my money
 }
<p>

迪米特法则可能带来Customer行为增加,导致复杂性,其实熟悉DDD应该知道,我们可以使用规格模式Specification将那些确保

实体逻辑一致性的合法性检查划分出去,这样,Customer和那些规格对象就组成一个聚合群,成为一个聚合体。

[该贴被banq于2013-05-05 13:32修改过]

[该贴被admin于2013-05-05 17:11修改过]

                   

3
banq
2013-05-05 14:35

从依赖和松耦合角度看,Payboy前后两段代码区别是没有了Wallet这个对象,这样,类Payboy只与Customer发生耦合,而不会直接和Wallet发生紧耦合。

Payboy只要告诉(Tell)Customer做什么即可,而不直接参与怎么做(Ask)。这实也是一种“Tell, Don't Ask”原则。

或者可以说:Payboy只要命令(Tell)Customer做什么即可,而不关注Customer是怎么做的。

DDD中领域模型表达的是一种业务目标,也就是做什么问题,不能将大量怎么做细节放入领域模型中。

[该贴被banq于2013-05-05 14:42修改过]

banq
2013-05-05 14:49

2013-05-05 14:35 "@banq

"的内容

Payboy前后两段代码区别是没有了Wallet这个对象 ...

Payboy后面的代码是:

paidAmount = myCustomer.getPayment(payment);

我们注意到这是使用了一个整数型的值paidAmount,也就是说,这里可以使用Wallet的一个值对象,而不必直接引用整个实体Wallet,这也符合DDD中聚合的设计。可以默认Customer为一个聚合根。

banq
2013-05-05 14:54

2013-05-05 14:35 "@banq

"的内容

Payboy只要告诉(Tell)Customer做什么即可,而不直接参与怎么做(Ask)。这实也是一种“Tell, Don't Ask”原则。 ...

实现Tell, Don't Ask”原则或迪米特法则(Law of Demeter)的最好方法是在技术上使用消息机制,在语义上引入事件。

消息为信封,事件为信函内容。事件也就是Tell的内容。如下图

使用这种编程风格后,更加符合TDD测试驱动方式,见:

"Tell, Don't Ask" Object Oriented Design

使用依赖注入实现聚合根之间调用的逻辑悖论:

http://www.jdon.com/45318

依赖注入与事件编程:

http://www.jdon.com/45264

[该贴被banq于2013-05-05 15:01修改过]


banq
2013-05-05 16:54

如果不结合领域模型的业务逻辑一致性来分析行为,单纯从迪米特法则分析一切对象行为,有时容易走极端。

比如凡是看到:

// customer.wallet.totalMoney;

// customer.apartment.bedroom.mattress.totalMoney;

// customer.apartment.kitchen.kitchenCabinet.totalMoney;

都应该考虑是否使用

customer.getPayment(..)

重构替代。

在JSP中,其实我们经常使用遍历XX.YY.ZZ来获取某对象子对象的值。如果我们将所有的子对象获取行为都放入XX根对象中,根对象将非常复杂。

因此,必须结合DDD聚合设计原则,需要从业务上考虑它们是否属于一个聚合边界内,比如顾客Customer有职责对自己的钱包Wallet进行操作,这很符合日常知识。但是如果你得出所有A都有职责对自己的B进行操作这个普遍真理就不恰当了。

换句话说,迪米特法则只是规定了一个对象只能和哪些对象直接交互,不能与除此之外的其他对象交互,分析其定义如下:

1. 可以调用自己的方法,那么除自己之外的对象怎么办?

2. 参数对象的方法,如果自己有方法参数,那么可以调用参数对象的方法,比如PayBoy调用参数Customer的getPayment方法。

3. 创建自己或初始化时涉及到其他对象的方法,创建自己时,涉及到一些其他对象,比如规格对象,其中封装了自己创建的合法性检查。

4. 它的直接组件对象的方法(聚合体内部等),如果它在一个组件边界内,应用在业务模型上,一个聚合边界看成一个组件边界,聚合体内部可以直接相互调用。

除以上情况以外都不能调用,如果需要调用,那么向允许的这些对象发出Tell命令,委托他们实现即可。

由此看来:迪米特法则特别适合聚合根实体的行为设计。

[该贴被admin于2013-05-05 17:12修改过]

4Go 1 2 3 4 下一页