《设计模式之美》笔记之迪米特法则

272 阅读5分钟

迪米特法则同样是来帮助开发者写出“高内聚、松耦合”的程序

迪米特法则的含义是这样的

“不该有依赖关系的模块之间,不要有依赖;有依赖关系的模块之间,尽量只依赖必要接口”

高内聚、低耦合是一个比较通用的思想,可以应用到不同粒度的设计当中,比如一个模块,一个类、一个函数中

下午通过两个例子来介绍迪米特法则

实例一

我们通过该实例来看一下迪米特法则的前半段--“不该有依赖关系的模块之间,不要有依赖”

有三个类,NetworkTransporter负责底层网络请求,HtmlDownloader通过URL请求网页html数据,Document表示网页文档数据对象,后续网页内容的抽取、分析都依赖这个类

public class NetworkTransporter {
    // 省略属性和其他方法...
    public Byte[] send(HtmlRequest htmlRequest) {
      //...
    }
}

public class HtmlDownloader {
  private NetworkTransporter transporter;//通过构造函数或IOC注入
  
  public Html downloadHtml(String url) {
    Byte[] rawHtml = transporter.send(new HtmlRequest(url));
    return new Html(rawHtml);
  }
}

public class Document {
  private Html html;
  private String url;
  
  public Document(String url) {
    this.url = url;
    HtmlDownloader downloader = new HtmlDownloader();
    this.html = downloader.downloadHtml(url);
  }
  //...
}
  • 首先NetworkTransporter类,它用来发送网络请求,不应该依赖HTMLRequest类,依赖它就好像只能发送HTML的请求了。就好像你去商店里买东西付钱的时候,如果这样的写法就好像你直接把钱包给了店主,让店主自己拿钱。正确的做法应该是你拿出钱给店主
  • NetworkTransporter没必要依赖其他request类,只接受必需的参数即可
public class NetworkTransporter {
    // 省略属性和其他方法...
    public Byte[] send(String url, Byte[] data) {
      //...
    }
}

HtmlDownloader类设计的没啥问题,需要下载数据所以必须依赖NetworkTransporter,但因为网络请求类修改了,它也得做相应修改

public class HtmlDownloader {
  private NetworkTransporter transporter;//通过构造函数或IOC注入
  
  // HtmlDownloader这里也要有相应的修改
  public Html downloadHtml(String url) {
    HtmlRequest htmlRequest = new HtmlRequest(url);
    Byte[] rawHtml = transporter.send(
      htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
    return new Html(rawHtml);
  }
}

再来看一下Document类,有如下几个问题:

  1. 在构造函数中执行下载操作不合适,时机不对,耗时较长,不易测试
  2. HtmlDownloader通过new的方式初始化,违反了基于接口而非实现编程的设计思想
  3. 从业务含义上来说Document不应该依赖HtmlDownloader

基于上面的问题,我们做如下修改

  • Document类中只有url和html数据
  • 通过一个工厂类来创建Document对象
  • 通过依赖注入方式给工厂类传递HtmlDownloader
public class Document {
  private Html html;
  private String url;
  
  public Document(String url, Html html) {
    this.html = html;
    this.url = url;
  }
  //...
}

// 通过一个工厂方法来创建Document
public class DocumentFactory {
  private HtmlDownloader downloader;
  
  public DocumentFactory(HtmlDownloader downloader) {
    this.downloader = downloader;
  }
  
  public Document createDocument(String url) {
    Html html = downloader.downloadHtml(url);
    return new Document(url, html);
  }
}

实例二

再通过实例二来看一下法则的后半句--“有依赖关系的模块之间,尽量只依赖必要的接口”

有一个类Serialization,负责对象的序列化和反序列化,如下所示

public class Serialization {
  public String serialize(Object object) {
    String serializedResult = ...;
    //...
    return serializedResult;
  }
  
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    //...
    return deserializedResult;
  }
}

乍一看,这么简单的类能有什么问题呢?如果将它放到具体的应用场景中可能就有问题了。比如有些情况只用到了序列化方法,而另一些只用到了反序列化方法。

基于迪米特法则--“有依赖关系的类之间,尽量只依赖必要接口”,我们考虑让只用到序列化方法的类只依赖序列化的接口。简单来做,我们可以将上面的Serialization拆分成两个类

public class Serializer {
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
}

public class Deserializer {
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

拆分后,符合了迪米特法则。但去违背了高内聚思想,序列化和反序列化都是相近似的工作,最好应放到一个类中来做,这样在将来修改的时候,也只用修改一处地方

基于上面的考虑,我们做一下修改


public interface Serializable {
  String serialize(Object object);
}

public interface Deserializable {
  Object deserialize(String text);
}

public class Serialization implements Serializable, Deserializable {
  @Override
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
  
  @Override
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

public class DemoClass_1 {
  private Serializable serializer;
  
  public Demo(Serializable serializer) {
    this.serializer = serializer;
  }
  //...
}

public class DemoClass_2 {
  private Deserializable deserializer;
  
  public Demo(Deserializable deserializer) {
    this.deserializer = deserializer;
  }
  //...
}

这样便能满足要求了

但你可能会说,不就序列化和反序列化两个方法么,至于设计的这么复杂么?是不是过渡设计了?

设计原则本身没有错,只有说能否用对之说。不要为了应用设计原则而强行用设计原则,我们在应用设计原则的时候,要具体问题具体分析

如果仅有序列化和反序列化两个方法,其实放到一个类里面就行了,对于只需要序列化的场景的类,其实能调用到反序列化方法问题也不大,带来的影响并不大

但如果随着项目发展,序列化和反序列化越来越多,比如

public class Serialization { // 参看JSON的接口定义
  public String serialize(Object object) { //... }
  public String serializeMap(Map map) { //... }
  public String serializeList(List list) { //... }
  
  public Object deserialize(String objectString) { //... }
  public Map deserializeMap(String mapString) { //... }
  public List deserializeList(String listString) { //... }
}

当序列化和反序列化的方法都增加到了3个,那对于只使用序列化的使用者来说,一旦反序列化的实现发生了变化,我们可能就得检查依赖Serialization的地方是否正常工作