具有数据模型对象的Demeter定律
昨天我从休假回来工作,在我们的日常站立中,我的队友们提到他们正在重构我们的java代码中的所有模型对象以删除所有的getter和setter,并使模型字段成为所有公共对象,调用Law of Law德米特之所以这样做是因为
为了方便我们遵守得墨忒耳定律:模块不应该知道它操纵的“物体”的内部。 由于数据结构不包含任何行为,因此它们自然会暴露其内部结构。 因此,在这种情况下,德米特不适用。
我承认我必须了解我对LoD的了解,但对于我的生活,我找不到任何迹象表明这符合法律的精神。 我们模型中的getter / setter都不包含任何业务逻辑,这是他这样做的理由,因此这些对象的客户端无需了解是否在get / set方法中执行了某些业务逻辑。
我认为这是对需要“对象结构的内部知识”意味着什么的误解,或者至少在字面意义上并且在这个过程中打破了一个非常标准的约定。
所以我的问题是,直接暴露模型对象内部结构而不是通过LoD名称中的getter / setter实际上是否有意义?
Robert Martin有一本名为Clean Code的书,涵盖了这一点。
在第6章(对象和数据结构)中,他谈到了对象和数据结构之间的根本区别。 对象受益于封装,而数据结构却没有。
有关于得墨忒耳定律的部分:
有一种着名的启发式方法叫做得墨忒耳 ,它说模块不应该知道它操纵的物体的内部。 正如我们在上一节中看到的,对象隐藏了它们的数据并公开了操作。 这意味着对象不应该通过访问器暴露其内部结构,因为这样做是为了暴露而不是隐藏其内部结构。
更确切地说,Demeter法则说C类的方法f应该只调用这些方法:
- C
- f创建的对象
- 作为参数传递给f的对象
- 保存在C实例变量中的对象
该方法不应调用任何允许函数返回的对象上的方法。 换句话说,与朋友交谈,而不是与陌生人交谈。
鲍勃叔叔给出了一个LoD违规的例子:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
这是否违反Demeter取决于ctxt,Options和ScratchDir是否为对象或数据结构。 如果它们是对象,那么它们的内部结构应该隐藏而不是暴露,因此对其内部的了解明显违反了得墨忒耳法则。 另一方面,如果ctxt,Options和ScratchDir只是没有行为的数据结构,那么它们自然会暴露其内部结构,因此Demeter不适用。
访问器function的使用使问题混乱。 如果代码编写如下,那么我们可能不会询问Demeter违规。
final String outputDir = ctxt.options.scratchDir.absolutePath;
所以这可能是你的同事来自的地方。 我认为“我们必须这样做因为LoD”这个论点充其量是不精确的。 中心问题不是LoD,而是API是由对象还是数据结构组成。 当有更多紧迫的事情要做时,这似乎是一种不必要的,容易出错的变化。
在我看来,这种变化与得墨忒耳法有任何关系。 从本质上讲,法则是通过让方法调用整个其他对象链来将对象图的结构编码到代码中。 例如,假设在汽车保险申请中,客户有策略,策略有车辆,车辆有分配给他们的司机,司机有出生日期,因此有年龄。 您可以想象以下代码:
public boolean hasUnderageDrivers(Customer customer) { for (Vehicle vehicle : customer.getPolicy().getVehicles()) { for (Driver driver : vehicle.getDrivers()) { if (driver.getAge() < 18) { return true; } } } return false; }
这将违反Demeter法则,因为此代码现在具有内部知识,而不需要知道。 它知道司机被分配到车辆,而不是仅仅被分配到整个保险单。 如果将来保险公司决定司机只是在保单上,而不是分配给特定的车辆,那么这个代码就必须改变。
问题是它调用其参数getPolicy()
的方法,然后调用另一个getVehicles()
,然后getVehicles()
另一个getDrivers()
,然后getAge()
另一个getAge()
。 Demeter法则说类的方法只应该调用方法:
- 本身
- 它的领域
- 它的参数
- 它创建的对象
(最后一个可能是unit testing的问题,您可能希望在工厂注入或创建对象而不是直接在本地创建对象,但这与Demeter法则无关。)
要解决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(最终?)公共字段的正常理由是你可能需要稍后将一些逻辑放在后面。 使用基于出生日期和今天日期的计算替换年龄,或者setter需要进行一些validation(例如,如果传递null
,则抛出)与之关联。 我之前没有听过在这种情况下引用的德米特定律。
Demeter的法则关于使用对象 ,而不是数据结构,在你的情况下DTO我理解。
Demeter法解释说你可以调用以下对象的方法:
- 作为参数传递
- 在方法内局部清除
- 实例变量(对象的字段)
- 全球
数据模型表示其中包含一些应在外部显示的数据的容器。 这是他们的角色,除此之外他们没有其他一些行为。