15

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

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

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

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

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

4

3 回答 3

19

罗伯特·马丁(Robert Martin)有一本书叫做清洁代码,涵盖了这一点。

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

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

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

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

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

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

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 是由对象还是数据结构组成。当有更多紧迫的事情要做时,这似乎是一个不必要且容易出错的更改。

于 2014-09-24T16:11:38.203 回答
13

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

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例如,如果您通过,则抛出)与之相关联。我以前从未听说过在这种情况下援引得墨忒耳法则。

于 2014-09-24T16:37:44.770 回答
0

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

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

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

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

于 2017-06-25T17:47:48.100 回答