Rails:德米特混乱法则


13

我正在阅读一本名为《 Rails反模式》的书,他们谈论使用委派来避免违反Demeter法则。这是他们的主要例子:

他们认为在控制器中调用这样的命令是不好的(我同意)

@street = @invoice.customer.address.street

他们提出的解决方案是执行以下操作:

class Customer

    has_one :address
    belongs_to :invoice

    def street
        address.street
    end
end

class Invoice

    has_one :customer

    def customer_street
        customer.street
    end
end

@street = @invoice.customer_street

他们说,由于您仅使用一个点,因此您没有违反Demeter定律。我认为这是不正确的,因为您仍在通过客户通过地址来获取发票的街道。我主要是从我读过的博客文章中得到这个想法的:

http://www.dan-manges.com/blog/37

在博客文章中,主要的例子是

class Wallet
  attr_accessor :cash
end
class Customer
  has_one :wallet

  # attribute delegation
  def cash
    @wallet.cash
  end
end

class Paperboy
  def collect_money(customer, due_amount)
    if customer.cash < due_ammount
      raise InsufficientFundsError
    else
      customer.cash -= due_amount
      @collected_amount += due_amount
    end
  end
end

博客文章指出,尽管只有一个点customer.cash而不是customer.wallet.cash,但此代码仍违反Demeter法则。

现在在Paperboy collect_money方法中,我们没有两个点,而在“ customer.cash”中只有一个点。这个代表团解决了我们的问题吗?一点也不。如果我们看一下这种行为,一个报税员仍在直接伸入客户的钱包以提取现金。

编辑

我完全理解并同意,这仍然是一种违规行为,我需要在Walletwithdraw中创建一个方法来为我处理付款,我应该在Customer类中调用该方法。我没有得到的是,按照这个过程,我的第一个例子仍然违反了Demeter法则,因为Invoice仍然直接进入Customer街道。

有人可以帮我消除困惑。我一直在搜索过去两天,试图让这个话题陷入,但仍然令人困惑。


2
类似的问题在这里
thorstenmüller2013年

我认为博客的第二个示例(报童)没有违反Demeter法则。这可能是错误的设计(您假设客户将用现金付款),但这并不是违反Demeter的法律。并非所有的设计错误都是由违反该法则引起的。作者对IMO感到困惑。
Andres F.

Answers:


24

你的第一个例子并没有违反迪米特法则。是的,按照原样编写的代码,@invoice.customer_street确实会得到与假设相同的@invoice.customer.address.street,但是在遍历的每个步骤中,返回的值都是由所询问的对象确定的 -并不是“报童进入了客户的钱包”,就是“报税员向客户索要现金,而客户恰巧从他们的钱包中获取现金 ”。

当您说时@invoice.customer.address.street,您就在假设客户和地址内部知识- 是一件坏事。当您说时@invoice.customer_street,您正在问invoice“嘿,我想要顾客的街,您可以决定如何获得它 ”。然后,客户对地址说:“嘿,我想要您的街道,请您决定如何获得它 ”。

得墨meter耳的主旨不是 “您永远无法从图形中距您较远的对象知道 ”;而是“您自己不要为了获得值而沿对象图形走得很远”。

我同意这似乎是个微妙的区别,但是请考虑一下:在符合Demeter的代码中,当内部表示更改时,需要更改多少代码address?不符合Demeter的代码呢?


这正是我所寻找的解释!谢谢。
user2158382

很好的解释。我有一个问题:1)如果发票对象想要将客户对象返回给发票客户,这并不一定意味着它与内部持有的客户对象相同。它可能只是一个动态创建的对象,目的是向客户返回一个很好的打包数据集,其中包含多个值。使用您提供的逻辑,您说的是发票不能有代表多个数据的字段。还是我错过了一些东西。
zumalifeguard 2014年

2

第一个示例和第二个示例实际上并不完全相同。第一个谈论“单点”的一般规则,第二个谈论更多关于OO设计的其他事情,尤其是“ 告诉,不要问

委派是一种有效的技术,它可以避免违反Demeter法则,但仅针对行为而非属性。-从第二个例子来看,Dan的博客

同样,“ 仅针对行为,不针对属性

如果您要求提供属性,则应该要求。“嘿,伙计,你的口袋里有多少钱?给我看看,我会评估你是否可以付钱。” 错了,没有购物员会表现得像这样。相反,他们会说:“请付款”

customer.pay(due_amount)

客户应自行评估是否应该付款以及是否可以付款。告诉客户付款后,店员的任务就完成了。

那么,第二个例子是否证明第一个是错误的?

在我看来。,只要:

1.你要克制自己。

虽然您可以@invoice通过委派访问所有客户的属性,但在正常情况下很少需要这样做。

考虑一下在Rails应用程序中显示发票的页面。顶部将显示一个部分,以显示客户的详细信息。因此,在发票模板中,您会这样编码吗?

#customer-info
  = @invoice.customer_name
  = @invoice.customer_address
  ....

那是错误的,效率低下。更好的方法是

#customer-info
  = render partial: 'invoice_header_customer', 
           locals: {customer: @invoice.customer}

然后让客户局部处理属于客户的所有属性。

因此,通常您不需要。但是您可能有一个列表页面,其中显示了所有最近的发票,每个页面中都有一个简介字段,其中li显示了客户的姓名。在这种情况下,您确实需要显示客户的属性,并且将模板编码为

= @invoice.customer_name

2.根据此方法调用,没有进一步的操作。

在上述列表页面的情况下,发票要求客户的姓名属性,但其实际目的是“ 显示您的姓名 ”,因此它基本上还是一种行为,而不是属性。没有根据此属性进行进一步的评估和采取的措施,例如,如果您的名字叫“ Mike”,我会喜欢您的,并再给您30天的功劳。不,发票只说“告诉我你的名字”,仅此而已。因此,根据示例2中的“不要问”规则,这完全可以接受。


0

在第二篇文章中进一步阅读,我认为这个想法将变得更加清晰。这个想法只是让客户提供支付能力,并完全隐藏案件存放的地方。是字段,钱包成员还是其他?如果实现细节发生更改,则调用者不知道,不需要知道并且不会更改。

class Wallet
  attr_accessor :cash
  def withdraw(amount)
     raise InsufficientFundsError if amount > cash
     cash -= amount
     amount
  end
end
class Customer
  has_one :wallet
  # behavior delegation
  def pay(amount)
    @wallet.withdraw(amount)
  end
end
class Paperboy
  def collect_money(customer, due_amount)
    @collected_amount += customer.pay(due_amount)
  end
end

因此,我认为您的第二份参考书给出了更有用的建议。

仅“一个点”的想法是部分成功的,因为它隐藏了一些深层的细节,但仍然增加了各个组件之间的耦合。


抱歉,可能我不太清楚,但是我完全理解第二个示例,并且我知道您需要做出发布的抽象,但是我不理解的是我的第一个示例。根据博客文章,我的第一个示例是错误的
user2158382 2013年

0

听起来,丹(Dan)从这篇文章得出了他的榜样:《报童》,《钱包》和《得墨 meter 耳的法律》

得墨耳的法则一个对象的方法应仅调用以下类型的对象的方法:

  1. 本身
  2. 它的参数
  3. 它创建/实例化的任何对象
  4. 它的直接组成对象

何时以及如何应用Demeter法则

因此,您现在对法律及其好处有了很好的了解,但是我们还没有讨论如何在现有代码中确定可以应用该法律的位置(同样重要的是,在不适用该法律的地方……)

  1. 链接的“获取”语句 -应用Demeter法则的第一个最明显的地方是具有重复get() 语句的代码位置,

    value = object.getX().getY().getTheValue();

    就像当本例中的规范人被警察推翻时,我们可能会看到:

    license = person.getWallet().getDriversLicense();

  2. 很多“临时”对象 -如果代码看起来像上面的许可示例一样,

    Wallet tempWallet = person.getWallet(); license = tempWallet.getDriversLicense();

    它是等效的,但更难检测。

  3. 导入许多类 -在我从事的Java项目中,我们有一个规则,即仅导入我们实际使用的类;你永远不会看到类似的东西

    import java.awt.*;

    在我们的源代码中。有了这个规则,看到十几个左右的import语句都来自同一个程序包并不少见。如果您的代码中发生这种情况,那么这可能是查找模糊的违例示例的好地方。如果您需要导入它,则将其耦合。如果更改,您可能也必须这样做。通过显式导入类,您将开始了解您的类之间的耦合程度。

我知道您的示例使用的是Ruby,但这应适用于所有OOP语言。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.