德墨忒耳定律和集合的构图如何协同工作?总结

2022-09-04 23:20:44

我读过几乎所有标有德墨忒耳定律的问题。我的具体问题在其他任何问题中都没有得到解答,尽管它非常相似。我的主要问题是,当你有一个具有组合层的对象,但是需要从各种对象中检索属性值时,你如何实现这一点,为什么要采取一种方法而不是另一种方法?

假设您有一个由其他对象组成的非常标准的对象,如下所示:

public class Customer {
  private String name;
  private ContactInfo primaryAddress;
  private ContactInfo workAddress;
  private Interests hobbies;
  //Etc...

  public getPrimaryAddress() { return primaryAddress; }
  public getWorkAddress() { return workAddress; }
  public getHobbies() { return hobbies; }
  //Etc...
}

private ContactInfo {
  private String phoneNumber;
  private String emailAddress;
  //Etc...

  public getPhoneNumber() { return phoneNumber; }
  public getEmailAddress() { return emailAddress; }
  //Etc...
}

private Interests {
  private List listOfInterests;
}

以下两者都违反了得墨忒耳定律:

System.out.println("Phone: " + customer.getPrimaryAddress().getPhoneNumber());
System.out.println("Hobbies: " + customer.getHobbies().getListOfInterests().toString());

我认为这也会违反得墨忒耳定律(澄清?

ContactInfo customerPrimaryAddress = customer.getPrimaryAddress();
System.out.println("Phone: " + customerPrimaryAddress.getPhoneNumber());

因此,据推测,您将向客户添加“getPrimaryPhoneNumber()”方法:

public getPrimaryPhoneNumber() {
  return primaryAddress.getPhoneNumber();
}

然后只需调用:System.out.println(“Phone: ” + customer.getPrimaryPhoneNumber());

但随着时间的推移,这样做似乎会带来很多问题,并且违背了德墨忒耳定律的意图。它使客户类成为一个巨大的包包,其中包含有关其内部类的太多知识。例如,Customer 对象似乎有一天会有不同的地址(而不仅仅是“主”和“工作”地址)。也许甚至 Customer 类也只是具有 ContactInfo 对象的列表(或其他集合),而不是特定的命名 ContactInfo 对象。在这种情况下,你如何继续遵循得墨忒耳定律?这似乎违背了抽象的目的。例如,在客户具有 ContactInfo 项目列表的情况下,这似乎是合理的:

Customer.getSomeParticularAddress(addressType).getPhoneNumber();

当你想到有些人拥有手机和固定电话时,这似乎会变得更加疯狂,然后ContactInfo必须拥有一系列电话号码。

Customer.getSomeParticularAddress(addressType).getSomePhoneNumber(phoneType).getPhoneNumber();

在这种情况下,我们不仅要引用对象内对象中的对象,而且还必须知道有效的地址类型和phoneType是什么。我绝对可以看到这个问题,但我不知道如何避免它。特别是当任何阶级正在称呼这个时,可能确实知道他们想要为有问题的客户的“主要”地址拉取“移动”电话号码。

这怎么能被重构以符合得墨忒耳定律,为什么这是好的呢?


答案 1

根据我的经验,所示的示例不是“由其他对象组成的标准对象”,因为此示例采取了将其组合片段实现为内部类的附加步骤,并且进一步使这些内部类成为私有。这不是一件坏事。Customer

一般来说,私有访问修饰符会增加信息隐藏,这是德墨忒耳定律的基础。揭露私人课程是矛盾的。NetBeans IDE 实际上包含一个默认编译器警告,用于“通过公共 API 导出非公共类型”。

我认为,将一个私有类暴露在其封闭类之外总是不好的:它减少了信息隐藏并违反了德墨忒耳定律。因此,要回答有关返回外部实例的澄清问题:是的,这是一种违规行为。ContactInfoCustomer

建议的将方法添加到 的解决方案是一个有效的选项。困惑就在这里:“客户...对自己的内部阶级有太多的了解。这是不可能的;这就是为什么这个例子不是一个标准的复合例子很重要。getPrimaryPhoneNumber()Customer

封闭类具有任何嵌套类的 100% 知识。总是。无论这些嵌套类在封闭类(或其他任何位置)中如何使用。这就是为什么封闭类可以直接访问其嵌套类的私有字段和方法的原因:封闭类本质上知道有关它们的所有信息,因为它们是在内部实现的。

给定一个类 Foo 的荒谬示例,它有一个嵌套类 Bar,它有一个嵌套类 Baz,它有一个嵌套类 Qux,因此 Foo(内部)调用 bar.baz.qux.method() 不会违反 Demeter。Foo已经知道了关于Bar,Baz和Qux的所有知识。因为他们的代码在Foo内部,所以没有额外的知识通过长方法链传递。

那么,根据德墨忒耳定律,解决方案是不返回中间对象,而不管其内部实现如何;即,无论使用多个嵌套类还是不使用嵌套类来实现,它都应该只返回其客户端类最终需要的内容。CustomerCustomer

例如,最后一个代码片段可能被重构为:customer.getPhoneNumber(addressType, phoneType);

或者如果只有少量选项,customer.getPrimaryMobilePhoneNumber();

这两种方法都会导致类的用户不知道其内部实现,并确保这些用户不必通过他们不直接感兴趣的对象进行调用。Customer


答案 2

重要的是要记住,尽管得名,但德墨忒耳定律一个指导方针,而不是一个实际的定律。我们需要在稍微更深层次上检查它的目的,以确定在这里做什么是正确的。

德墨忒耳定律的目的是防止外部物体能够进入另一个物体的内部。访问内部有两个问题:1)它提供了太多关于对象内部结构的信息,2)它还允许外部对象修改类的内部结构。

此问题的正确响应是将返回的对象与 Customer 方法中的内部表示形式分开。换句话说,我们没有返回对ContactInfo类型的私有内部对象的引用,而是定义了一个新类UnmodifiableContactInfo,并让getPrimaryAddress返回UnmodifiableContactInfo,创建它并根据需要填充它。

这让我们都受益于得墨忒耳定律。返回的对象不再是客户的内部对象,这意味着客户可以根据需要修改其内部存储,并且我们对 UnmodifiableContactInfo 所做的任何事情都不会影响客户的内部。

(实际上,我会重命名内部类,并将外部类保留为ContactInfo,但这是一个小问题)

因此,这实现了得墨忒耳定律的目标,但看起来我们仍然在打破它。我对此的看法是,getAddress方法不是返回ContactInfo对象,而是实例化它。这意味着根据Demeter规则,我们可以访问ContactInfo的方法,并且您上面编写的代码不是违规的。

当然,您必须注意,尽管“违反德米特定律”发生在访问客户的代码中,但需要在客户中进行修复。一般来说,修复是一件好事 - 提供对内部对象的访问是不好的,无论它们是否使用多个“点”访问。

一些注意事项。很明显,对德墨忒耳定律的过度严格应用会导致白痴,例如禁止:

int nameLength = myObject.getName().length()

这是我们大多数人每天都在做的技术违规行为。每个人也这样做:

mylist.get(0).doSomething();

这在技术上是一种违规行为。但现实情况是,除非我们实际上允许外部代码根据主要对象(Customer)检索到的对象影响其行为,否则这些都不是问题。

总结

代码应如下所示:

public class Customer {
    private class InternalContactInfo {
        public ContactInfo createContactinfo() {
            //creates a ContactInfo based on its own values...
        }
        //Etc....
    }
   private String name;
   private InternalContactInfo primaryAddress;
   //Etc...

   public Contactinfo getPrimaryAddress() { 
      // copies the info from InternalContactInfo to a new object
      return primaryAddress.createContactInfo();
   }
   //Etc...
}

public class ContactInfo {
   // Not modifiable
   private String phoneNumber;
   private String emailAddress;
   //Etc...

   public getPhoneNumber() { return phoneNumber; }
   public getEmailAddress() { return emailAddress; }
   //Etc...
}

}