18、迪米特法则(LOD)

1、何为 "高内聚、低耦合"

1.1、什么是 "高内聚"

高内聚:相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中
相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护
实际上,我们前面讲过的单一职责原则是实现代码高内聚非常有效的设计原则

1.2、什么是 "低耦合"

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

1.3、二者之间的联系

"高内聚" 有助于 "低耦合",同理 "低内聚" 也会导致 "紧耦合"
关于这一点我画了一张对比图来解释,图中左边部分的代码结构是 "高内聚、低耦合",右边部分正好相反,是 "低内聚、紧耦合"
image

2、迪米特法则

迪米特法则的英文翻译是: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)

迪米特法则包含前后两部分,这两部分讲的是两件事情

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

3、理论解读与代码实战一

不该有直接依赖关系的类之间,不要有依赖

3.1、示例

这个例子实现了简化版的搜索引擎爬取网页的功能,代码中包含三个主要的类

  • 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);
    }
    // ...
}

3.2、问题

首先我们来看 NetworkTransporter 类

作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以我们不应该直接依赖太具体的发送对象 HtmlRequest
从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的 HtmlRequest 类

我们应该如何进行重构,让 NetworkTransporter 类满足迪米特法则呢,我这里有个形象的比喻
假如你现在要去商店买东西,你肯定不会直接把钱包给收银员,让收银员自己从里面拿钱,而是你从钱包里把钱拿出来交给收银员

这里的 HtmlRequest 对象就相当于钱包,HtmlRequest 里的 address 和 content 对象就相当于钱
我们应该把 address 和 content 交给 NetworkTransporter,而非是直接把 HtmlRequest 交给 NetworkTransporter

根据这个思路,NetworkTransporter 重构之后的代码如下所示

public class NetworkTransporter {
    // 省略属性和其他方法 ...
    public Byte[] send(String address, Byte[] data) {
        //...
    }
}

我们再来看 HtmlDownloader 类

这个类的设计没有问题,不过我们修改了 NetworkTransporter 的 send() 函数的定义,而这个类用到了 send() 函数,所以我们需要对它做相应的修改

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 类

这个类的问题比较多,主要有三点

  • 构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性
  • HtmlDownloader 对象在构造函数中通过 new 来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性
  • 从业务含义上来讲,Document 网页文档没必要依赖 HtmlDownloader 类,违背了迪米特法则

虽然 Document 类的问题很多,但修改起来比较简单,只要一处改动就可以解决所有问题,修改之后的代码如下所示

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);
    }
}

4、理论解读与代码实战二






posted @ 2023-06-26 00:44  lidongdongdong~  阅读(47)  评论(0编辑  收藏  举报