具有数据模型对象的Demeter定律

ale*_*exD 12 java design-patterns law-of-demeter

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

为了方便我们遵守得墨忒耳定律:模块不应该知道它操纵的"物体"的内部.由于数据结构不包含任何行为,因此它们自然会暴露其内部结构.因此,在这种情况下,德米特不适用.

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

我认为这是对需要"对象结构的内部知识"意味着什么的误解,或者至少在字面意义上并且在这个过程中打破了一个非常标准的约定.

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

Nat*_*hes 16

Robert Martin有一本名为Clean Code的书,涵盖了这一点.

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

有关于得墨忒耳定律的部分:

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

更准确地说,Demeter法则说C类方法f只应该调用这些方法:

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

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

鲍勃叔叔给出了一个LoD违规的例子:

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
Run Code Online (Sandbox Code Playgroud)

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

访问器功能的使用使问题混乱.如果代码编写如下,那么我们可能不会询问Demeter违规.

final String outputDir = ctxt.options.scratchDir.absolutePath;
Run Code Online (Sandbox Code Playgroud)

所以这可能是你的同事来自的地方.我认为"我们必须这样做因为LoD"这个论点充其量是不精确的.中心问题不是LoD,而是API是由对象还是数据结构组成.当有更多紧迫的事情要做时,看起来似乎是一个不必要的,容易出错的变化.

  • 有道理......他的桌子上有一份"清洁代码".猜猜他最近正在读它:) (2认同)

Dav*_*rad 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;
}
Run Code Online (Sandbox Code Playgroud)

这将违反德米特定律,因为此代码现在具有内部知识,不需要知道.它知道司机被分配到车辆,而不是仅仅被分配到整个保险单.如果将来保险公司决定司机只是在保单上,而不是分配给特定的车辆,那么这个代码就必须改变.

问题是它调用其参数的方法getPolicy(),然后是另一个,getVehicles()然后是另一个,getDrivers()然后是另一个,getAge().Demeter法则说类的方法只应该调用方法:

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

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

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

public boolean hasUnderageDrivers(Policy policy) {
    return policy.hasUnderageDrivers();
}
Run Code Online (Sandbox Code Playgroud)

调用一级,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;
}
Run Code Online (Sandbox Code Playgroud)

(我甚driver.getAge()至转换成了driver.age,虽然这可能是基于出生日期的计算,而不是简单的字段.)

请注意,当我们使用公共字段而不是getter编写代码时,存在完全相同的问题,即嵌入有关如何将对象图组合在一起的知识(客户的策略中包含具有驱动程序的车辆).问题与如何组合在一起,而不是与是否正在调用吸气剂有关.

顺便说一句,喜欢getter(最终?)公共字段的正常理由是你可能需要稍后将一些逻辑放在后面.年龄被替换为基于出生日期和今天日期的计算,或者设定者需要进行一些验证(null例如,如果你通过,则抛出)与之相关联.我之前没有听说过在这种情况下引用的得墨忒耳法则.

  • 我对您(优秀)的政策示例有疑问.您最终是否会在策略对象中使用大量代码.每个想要了解保险合同的人都必须首先询问保单对象吗? (2认同)
  • @markus这是真的,你可能会在策略类中得到很多代码.如果`Policy`中的`hasUnderageDrivers`方法只是将责任传递给某些`Vehicles`集合对象上类似命名的方法,那么你也可能会得到很多小的"转发"方法. (2认同)