数据模型对象的德米特定律

2022-09-03 10:07:30

我昨天从假期回来工作,在我们的日常站立会议上,我的队友提到他们正在重构我们java代码中的所有模型对象,以删除所有getter和setter,并使模型字段成为所有公共对象,而是调用Demeter定律作为这样做的原因,因为

为了促进我们遵守德墨忒耳定律:一个模块不应该知道它所操纵的“物体”的内部。由于数据结构不包含任何行为,因此它们自然会暴露其内部结构。因此,在这种情况下,Demeter不适用。

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

我认为这是对需要“对象结构的内部知识”的含义的误解,或者至少是过于字面化地理解它,并在此过程中打破了一个非常标准的约定。

所以我的问题是,直接公开模型对象内部结构,而不是以LoD的名义通过getters/setters,这实际上有什么意义吗?


答案 1

罗伯特·马丁(Robert Martin)有一本名为《清洁代码》(Clean Code)的书涵盖了这一点。

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


答案 2

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

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()

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

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

要解决此问题,我们可以传入对象,并且可以有一个知道如何确定策略是否具有未成年驱动程序的方法:hasUnderageDrivers()PolicyPolicy

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

把一个级别调低,可能是可以的——得墨忒耳定律是一个经验法则,而不是一个硬性规定。您可能也不必担心不太可能改变的事情; 可能总是会继续有一个出生日期和一种方法。customer.getPolicy().hasUnderageDrivers()DrivergetAge()

但是回到你的案例,如果我们用公共领域取代所有这些获取者会发生什么?它对得墨忒耳定律毫无帮助。您仍然可能遇到与第一个示例中完全相同的问题。考虑:

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

请注意,当我们使用公共字段而不是 getters 编写代码时,存在嵌入对象图如何组合在一起的知识(客户有一个策略,其中包含具有驱动程序的车辆)的完全相同的问题。问题与这些碎片如何组合在一起有关,而不是与是否被调用了getter有关。

顺便说一句,更喜欢getter而不是(最终的?)公共字段的正常原因是,你以后可能需要在它们后面放一些逻辑。年龄被替换为基于出生日期和今天日期的计算,或者设置者需要有一些与之关联的验证(例如,如果您通过,则抛出)。我以前没有听说过德墨忒耳定律在这种背景下被引用。null


推荐