【问题标题】:Law of Demeter with data model objects带有数据模型对象的得墨忒耳定律
【发布时间】:2014-11-19 04:43:42
【问题描述】:

我昨天放假回来工作,在我们的每日站会中,我的队友提到他们正在重构我们的 java 代码中的所有模型对象,以删除所有 getter 和 setter,并将模型字段改为公共对象,调用德墨忒耳法则是这样做的原因,因为

为了便于我们遵守得墨忒耳定律:模块不应该知道它所操作的“对象”的内部结构。由于数据 结构不包含行为,它们自然会暴露其内部结构。所以在那种情况下,得墨忒耳不适用。

我承认我必须重新了解我对 LoD 的了解,但对于我这辈子来说,我找不到任何表明这符合法律精神的东西。我们模型中的 getter/setter 都不包含任何业务逻辑,这是他这样做的理由,因此这些对象的客户不必了解 get/set 方法中是否正在执行某些业务逻辑。

我认为这是对需要“对象结构的内部知识”意味着什么的误解,或者至少是过于字面理解并在此过程中打破了相当标准的惯例。

所以我的问题是,直接公开模型对象内部结构而不是通过 LoD 名称中的 getter/setter 公开是否真的有意义?

【问题讨论】:

  • 听起来你的队友没有足够的工作要做。
  • 是的,这就是问题所在......我们有大量工作要做,这是一个巨大的重构,涉及 100 个文件。我要反击,我只是想确保我拥有的不仅仅是“这是浪费时间”(虽然这应该足够了)
  • 幸运的是,您没有使用 Groovy,其中 x.getStuff()x.stuff 无法区分(并且字段 stuff 在调用时甚至可能不存在......)
  • @alexD:问题中的引述是从哪里得到的?

标签: java design-patterns law-of-demeter


【解决方案1】:

Robert Martin 有一本书叫做 Clean Code,其中涵盖了这一点。

在第 6 章(对象和数据结构)中,他谈到了对象和数据结构之间的根本区别。 对象从封装中受益,而数据结构则没有。

有一段是关于得墨忒耳法则的:

有一个著名的启发式算法Law of Demeter 表示模块不应该知道它所操作的对象的内部结构。正如我们在上一节中看到的,对象隐藏了它们的数据并暴露了操作。这意味着对象不应通过访问器公开其内部结构,因为这样做是公开而不是隐藏其内部结构。

更准确地说,得墨忒耳定律说 C 类的方法 f 应该只调用这些方法:

  • C
  • 由 f 创建的对象
  • 作为参数传递给 f 的对象
  • 保存在 C 的实例变量中的对象

该方法不应调用由任何允许的函数返回的对象的方法。换句话说,与朋友交谈,而不是与陌生人交谈。

Uncle Bob 举了一个违反 LoD 的例子:

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

这是否违反 Demeter 取决于 ctxt、Options 和 ScratchDir 是对象还是数据结构。如果它们是物体,那么它们的内部结构应该隐藏而不是暴露,因此了解它们的内部结构显然违反了德墨忒耳法则。另一方面,如果 ctxt、Options 和 ScratchDir 只是没有行为的数据结构,那么它们自然会暴露其内部结构,因此 Demeter 不适用。

访问器函数的使用混淆了这个问题。如果代码是这样编写的,那么我们可能就不会询问 Demeter 违规行为了。

final String outputDir = ctxt.options.scratchDir.absolutePath;

所以这可能是你的同事来自哪里。我认为“我们必须这样做,因为 LoD”的论点充其量是不精确的。核心问题与其说是 LoD,不如说是 API 是由对象还是数据结构组成。当有更多紧迫的事情要做时,这似乎是一个不必要且容易出错的更改。

【讨论】:

  • 有道理..他的桌子上放着一本“清洁代码”。猜猜他最近在读它:)
  • 我不同意。我认为这仍然是一个问题。这样想——如果你需要使用 outputDir 测试类,你真的要实例化整个对象树——ctxt、options 和 scratchDir 吗?精彩的讨论 - youtube.com/watch?v=RlfLCWKxHJ0
  • Robert Martin 对“对象”和“数据结构”的区分听起来并不恰当。 “对象”是“数据结构”,“数据结构”是“对象”。代码重构以及“从另一个角度看待代码”将把一个变成另一个。也许如果“数据结构”被理解为“容器”类型,如 Map、List、Set 等。
  • @David:我认为 Martin 的意思是有时封装是有帮助的,有时则不是,当它没有帮助时,他会尽量不将其称为对象。在 Martin 写那本书的时候,他正在深入研究 FP 范式,他似乎更加欣赏 OOP 在某些地方比在其他地方更重要,我在早期的写作中并没有注意到这一点。
【解决方案2】:

在我看来,此更改与Law of Demeter 无关。该法则本质上是关于通过让方法调用整个其他对象链来将对象图的结构编码到代码中。例如,假设在汽车保险应用程序中,客户有保单,保单有车辆,车辆分配有司机,司机有出生日期,因此有年龄。你可以想象下面的代码:

public boolean hasUnderageDrivers(Customer customer) {
    for (Vehicle vehicle : customer.getPolicy().getVehicles()) {
        for (Driver driver : vehicle.getDrivers()) {
            if (driver.getAge() < 18) {
                return true;
            }
        }
    }
    return false;
}

这将违反得墨忒耳法则,因为该代码现在拥有不需要知道的内部知识。它知道司机被分配到车辆,而不是仅仅被分配到整个保险单。如果将来保险公司决定司机只是在保单上,而不是被分配到特定的车辆,那么这个代码就必须改变。

问题在于它调用了它的参数getPolicy() 的一个方法,然后是另一个getVehicles(),然后是另一个getDrivers(),然后是另一个getAge()。得墨忒耳法则说一个类的方法应该只调用方法:

  • 本身
  • 它的领域
  • 它的参数
  • 它创建的对象

(最后一个可能是单元测试的问题,您可能希望由工厂注入或创建对象,而不是直接在本地创建,但这与得墨忒耳定律无关。)

要解决hasUnderageDrivers() 的问题,我们可以传入Policy 对象,我们可以在Policy 上设置一个方法,该方法知道如何确定策略是否包含未成年司机:

public boolean hasUnderageDrivers(Policy policy) {
    return policy.hasUnderageDrivers();
}

调用一个级别,customer.getPolicy().hasUnderageDrivers(),可能没问题 - 得墨忒耳法则是一个经验法则,而不是一成不变的规则。您也可能不必担心不太可能改变的事情; Driver 可能总是会继续有出生日期和 getAge() 方法。

但是回到你的例子,如果我们用公共字段替换所有这些 getter 会发生什么?它对得墨忒耳法则毫无帮助。您仍然可以遇到与第一个示例完全相同的问题。考虑:

public boolean hasUnderageDrivers(Customer customer) {
    for (Vehicle vehicle : customer.policy.vehicles) {
        for (Driver driver : vehicle.drivers) {
            if (driver.age < 18) {
                return true;
            }
        }
    }
    return false;
}

(我什至将driver.getAge() 转换为driver.age,尽管这可能是基于出生日期而不是简单字段的计算。)

请注意,当我们使用公共字段而不是 getter 编写代码时,会出现与嵌入对象图如何组合的知识(客户的保单具有具有驱动程序的车辆的策略)完全相同的问题。问题在于这些部分是如何组合在一起的,而不是是否调用了 getter。

顺便说一句,喜欢 getter 而不是(final?)公共字段的正常原因是您稍后可能需要在它们后面添加一些逻辑。年龄被替换为基于出生日期和今天日期的计算,或者 setter 需要进行一些验证(例如,如果您通过 null 则抛出)与之相关联。我以前从未听说过在这种情况下援引得墨忒耳法则。

【讨论】:

  • 我对您的(优秀)政策示例有疑问。您最终不会在策略对象中得到大量代码吗?因为每个想了解保险合同的人都必须先询问保单对象?
  • @markus 是的,您最终可能会在策略类中得到大量代码。如果Policy 中的hasUnderageDrivers 方法只是将责任传递给某些Vehicles 集合对象上的类似名称的方法,那么您最终可能还会得到很多小的“转发”方法。
  • @markus Wikipedia mention this: “在方法级别,LoD 导致了狭窄的接口,只允许访问完成工作所需的尽可能多的信息,因为每个方法都需要了解密切相关对象的一小部分方法。OTOH,在类级别,LoD导致宽(即放大)接口,因为LoD需要引入许多辅助方法而不是直接挖掘对象结构。一个解决方案扩大类接口的问题是面向方面的方法..."
【解决方案3】:

关于使用对象而不是数据结构的得墨忒耳法则,在你的情况下是我理解的 DTO。

得墨忒耳定律解释说,您可以调用以下对象的方法:

  1. 作为参数传递
  2. 在方法内本地清除
  3. 实例变量(对象的字段)
  4. 全球

数据模型表示容器,其中包含一些应该在外部显示的数据。这是他们的角色,除此之外他们没有其他行为。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-30
    • 1970-01-01
    相关资源
    最近更新 更多