美文网首页
22 - 迪米特(LOD)原则

22 - 迪米特(LOD)原则

作者: 舍是境界 | 来源:发表于2021-08-23 08:08 被阅读0次

    迪米特法则。尽管它不像 SOLID、KISS、DRY 原则那样,人尽皆知,但它却非常实用。利用这个原则,能够帮我们实现代码的“高内聚、松耦合”。本文,围绕下面几个问题,并结合两个代码实战案例,来深入地学习这个法则。

    • 什么是“高内聚、松耦合”?
    • 如何利用迪米特法则来实现“高内聚、松耦合”?
    • 有哪些代码设计是明显违背迪米特法则的?对此又该如何重构?

    何为“高内聚、松耦合”?

    • “高内聚、松耦合”是一个非常重要的设计思想,能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。很多设计原则都以实现代码的“高内聚、松耦合”为目的,比如单一职责原则、基于接口而非实现编程等。
    • 实际上,“高内聚、松耦合”是一个比较通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。本文以“类”作为这个设计思想的应用对象来展开讲解,其他应用场景你可以自行类比
    • 在这个设计思想中,“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。不过,这两者并非完全独立不相干。高内聚有助于松耦合,松耦合又需要高内聚的支持。

    什么是“高内聚”?

    • 所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。可以参考单一职责原则

    什么是“松耦合”?

    • 所谓松耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。实际上,我们前面讲的依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合。

    “内聚”和“耦合”之间的关系。

    • “高内聚”有助于“松耦合”,同理,“低内聚”也会导致“紧耦合”。关于这一点,我画了一张对比图来解释。图中左边部分的代码结构是“高内聚、松耦合”;右边部分正好相反,是“低内聚、紧耦合”。
    高内聚,松耦合示意图
    • 从图中我们也可以看出,高内聚、低耦合的代码结构更加简单、清晰,相应地,在可维护性和可读性上确实要好很多。

    迪米特法则

    • 迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD。单从这个名字上来看,我们完全猜不出这个原则讲的是什么。不过,它还有另外一个更加达意的名字,叫作最小知识原则,英文翻译为:The Least Knowledge Principle。

    Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

    每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。

    不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。

    理论解读与代码实战一

    • 对于“不该有直接依赖关系的类之间,不要有依赖”。举个例子解释一下。
    • 这个例子实现了简化版的搜索引擎爬取网页的功能。代码中包含三个主要的类。其中,NetworkTransporter 类负责底层网络通信,根据请求获取数据;HtmlDownloader 类用来通过 URL 获取网页;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 类:作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象 HtmlRequest。从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的 HtmlRequest 类,重构如下:
    public class NetworkTransporter {
        // 省略属性和其他方法...
        public Byte[] send(String address, Byte[] data) {
          //...
        }
    }
    
    • Document 类:问题比较多,主要有三点。
      • 第一,构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。代码的可测试性我们后面会讲到,这里你先知道有这回事就可以了。
      • 第二,HtmlDownloader 对象在构造函数中通过 new 来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性。
      • 第三,从业务含义上来讲,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;
      }
    }
    
    • 假设在我们的项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。那基于迪米特法则后半部分“有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口。同理,只用到反序列化操作的那部分类不应该依赖序列化接口。
    • 既不想违背高内聚的设计思想,也不想违背迪米特法则,可以通过引入两个接口就能轻松解决这个问题:
    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;
      }
      //...
    }
    
    • 对于这个 Serialization 类来说,只包含两个操作,其实没有太大必要拆分成两个接口。但是,如果我们对 Serialization 类添加更多的功能,那么拆分接口则非常适合了,工作中需要能活学活用

    小结

    • “高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。
    • 所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
    • 不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

    相关文章

      网友评论

          本文标题:22 - 迪米特(LOD)原则

          本文链接:https://www.haomeiwen.com/subject/wcuqiltx.html