设计模式学习笔记 - 设计原则 - 8.迪米特法则(LOD)

前言

迪米特法则,是一个非常实用的原则。利用这个原则,可以帮我们实现代码的 “高内聚、松耦合”。

围绕下面几个问题,来学习迪米特原则。

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

什么是 “高内聚、松耦合”?

“高内聚、松耦合”是一个非常重要的思想,能有效地提高代码的可读性和可维护性,缩小功能改动导致的代码范围改动。

很多设计原则都已实现代码的“高内聚、松耦合”为目的,比如单一职责原则、基于接口而非实现编程。

在这个设计思想中,“高内聚” 用来指导类本身的设计,“松耦合” 用来指导类与类之间依赖关系的设计。不过,这两种并非独立不相干。高内聚有助于松耦合,松耦合有需要高内聚的支持。

什么是“高内聚”呢?

所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中

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

什么是“松耦合”?

松耦合是指,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的改动

前面讲的依赖注入接口隔离基于接口而非实现编程,以及今天的迪米特法则,都是为了实现代码的松耦合。

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

“高内聚” 有助于 “松耦合”,同理 “低内聚” 也会导致 “紧耦合”。

在这里插入图片描述
上图的左边的代码结构是 “高内聚、松耦合”,右边部分是 “低内聚、紧耦合”。

  • 左边部分的代码设计中,类的粒度较小,每个类的职责比较单一。相近的功能都放到了一个类中,不相近的功能被分割到了多个类中。这样类更加独立,代码的内聚性更高。因为职责单一,所以每个类被依赖的类就会比较少,代码低耦合。一个类的修改,只会影响到一个依赖类的代码改动。我们只需要测试依赖类是否还能正常工作就行了。
  • 右边部分的代码设计中,类粒度较大,低内聚,功能大而全,不相近的功能放到一个类中。这就导致很多其他类都依赖这个类。当修改这个类的某一个功能代码时,会影响依赖它的多个类。我们需要测试这三个依赖类是否正常工作。这也就是所谓的 “牵一发而动全身”。

另外,从图中我们可以看出,高内聚、低耦合的代码结构更加简单、清晰,相应地,可维护性和可读性要好很多。

“迪米特法则”理论描述

迪米特法则,Law Of Demeter,缩写是 LOD,它也叫做最小知识原则。

这个原则的英文定义:

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

翻译:每个模块只应该了解与它关系紧密的模块的有限知识。或者说,每个模块只和自己的朋友 “说话”,不和陌生人说话。

我们把其中的模块,替换成类,按照自己的意思理解下:不该有直接依赖关系的类,不要有依赖;有依赖关系的类,尽量只依赖必要的接口

从上面的描述中,我们可以看出,迪米特法则包含前后两部分,这两部分讲的是两件事,我们用两个实战案例讲解下。

理论解读与代码实战一 —— 不要有直接依赖关系的类,不要有依赖

我们举一个搜索引擎爬取网页的功能。代码中包含三个主要类。其中 NetworkTransporter 类负责底层网络通信,根据请求获取数据;HtmlDownloader 类用来通过 URL 获取网页;Document 表示网页文档,后续的网页内容抽取、分词、索引都是依次为处理对象。具体代码如下:

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

public class HtmlDownloader {
    private NetworkTransporter transporter; // 通过构造函数注入...
    
    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 类中的 addresscontent 交给 NetworkTransporter ,而非直接把 HtmlRequest 交给 NetworkTransporter。按照这个思路,NetworkTransporter 重构之后的代码如下所示:

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

在看下 HtmlDownloader。这个类的设计没有问题。不过因为修改了 NetworkTransporter,所以要对它做响应的改动。

public class HtmlDownloader {
    private NetworkTransporter transporter; // 通过构造函数注入...

    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(url) 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。
  • 第二: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 htmlDownloader;

    public DocumentFactory(HtmlDownloader htmlDownloader) {
        this.htmlDownloader = htmlDownloader;
    }

    public Document createDocument(String url) {
        Html html = htmlDownloader.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) {
        String deserializedResult = ...;
        // ...
        return deserializedResult;
    }
}

但看这个类没有一点问题。不过,若把它放到一定的应用场景里,那就还有继续优化的空间。 假设,在我们的项目中,有些类只用到了序列化操作,有些类去只用了反序列化操作。基于迪米特法则的后半部分 “有依赖关系的类,尽量只依赖必要的接口”,只用到序列化的那部分类不应该依赖反序列化接口。同理,只用到反序列化的那部分类不应该依赖序列化接口。

根据这个思路,将 Serialization 拆分成两个更小粒度的类,一个只负责序列化 Serializer,另一个只负责反序列化 Deserializer

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

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

不过,虽然拆分之后的代码更能满足迪米特法则,但却违背了高内聚的设计思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的地方不至过于分散。对于刚刚的例子,如果我们修改了序列化的实现方式,比如从 JSON 换成了 XML,那反序列化的实现逻辑也需要修改。在未拆分的情况下,只要修改一个类即可。在拆分之后,需要修改两个类。显然,这种设计思路的代码改动范围变大了。

实际上,通过引入两个接口,就可以既不违背高内聚的设计思想,也不违背迪米特法则。具体的代码如下所示,实际上在讲解 “接口隔离原则”课程的时候,第三个例子就用了类似的思路。你可以结合着一块来看。

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

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

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

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

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

尽管,还是要往 DemoClass_1 类的构造函数中,传入包含序列化和反序列化的 Serialization 实现类,但是我们依赖的 Serializable 接口只包含序列化操作, DemoClass_1 无法使用 Serialization 的发序列化接口,对反序列化操作无感知,这也就符合了迪米特法则后半部分所说的“依赖有限接口”的要求。

实际上,上面的代码思路,也体现了 “基于接口而非实现编程” 的设计原则,结合迪米特法则,我们可以总结出一个新的设计原则,那就是 “基于最小接口而非最大实现编程”。

辩证思考与灵活应用

Serialization 类只包含序列化和反序列化两个操作,只用到序列化操作的使用者,即便能够感知到反序列化接口,问题也不大。为了满足迪米特法则,我们将一个非常简单的类,拆分出两个接口,是否有过度设计的意思呢?

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

对于刚刚的 Serialization 来说,只包含两个操作,确实没有太大必要拆分成两个接口。但是,如果我们对 Serialization 类添加更多的功能,实现更多更好的序列化和反序列化函数,我们来重新思考下这个问题。修改后的代码如下所示:

public class Serialization {
    public String serialize(Object object) { /*...*/ }
    public String serialize(Map map) { /*...*/ }
    public String serialize(List list) { /*...*/ }
    
    public Object deserialize(String str) { /*...*/ }
    public Map deserialize(String str) { /*...*/ }
    public List deserialize(String str) { /*...*/ }
}

在这种场景下,第二种设计思路更好些。因为基于之前的应用常见来说,大部分代码只需要用到序列化的功能,这部分使用者,没有必要了解反序列化知识,而修改之后的 Serialization 的反序列化知识从一个变成三个。 一旦,反序列化操作有代码改动,我们都需要检查、测试所有依赖 Serialization 类的代码是否还能正常工作。为了减少耦合和测试工作量,我们应该按照迪米特法则,将反序列化和序列化功能隔离开。

总结

1.如何理解“高内聚、松耦合”

“高内聚、松耦合” 是一个非常重要的设计思想,能有效提高代码的可读性、可维护性,缩小功能改动导致的代码改动范围。“高内聚” 用来指导类本身的设计,“低耦合” 用来指导类与类之间的依赖关系的设计。

所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。

所谓松耦合,是指,在代码中,类与类之间的依赖关系简单清晰。即使两个有类有依赖关系,一个类的改动也不会(或者很少)导致依赖类的代码改动。

2.如何理解“迪米特法则”

不该有直接依赖关系的类之间,不要有依赖关系;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/425747.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Python】FastAPI 项目创建 与 Docker 部署

文章目录 前言&需求描述1. 本地FastAPI1.1 Python 环境准备1.2 本地 Pycharm 创建FastAPI项目 2. Python FastAPI 部署2.1 服务器配置Python环境2.2.1 下载与配置Git、Pyenv等工具2.2.2 下载与配置Python 2.2 FastAPI 打包成镜像2.2.1 项目准备所需环境文件2.2.2 编写Docke…

Java基于SpringBoot的在线文档管理系统的设计与实现论文

摘 要 随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,在线文档管理当然也不能排除在外。在线文档管理系统是以实际运用为开发背景,运用软件工程原理和开发方法&am…

合并两个有序链表

题目 题目链接 合并两个排序的链表_牛客题霸_牛客网 题目描述 代码实现 class Solution { public:/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * param pHead1 ListNode类 * param pHead2 ListNode类 * return …

1分钟学会Python字符串前后缀与编解码

1.前缀和后缀 前缀和后缀指的是:字符串是否以指定字符开头和结尾 2.startswith() 判断字符串是否以指定字符开头,若是返回True,若不是返回False str1 "HelloPython"print(str1.startswith("Hello")) # Trueprint…

Python 微信自动化工具wxauto开发系列01(2024年3月可用 支持3.9最新微信)

下载链接:GitHub - cluic/wxauto: Windows版本微信客户端(非网页版)自动化,可实现简单的发送、接收微信消息,简单微信机器人d 推荐大家在github下载: git clone https://github.com/cluic/wxauto.git cd w…

数据结构——基本术语和概念

目录 1.数据 2.数据元素 3.数据项 4.数据对象 数据元素与数据对象 5.数据结构 1.逻辑结构 逻辑结构的种类 划分方式1 1.线性结构 2.非线性结构 ​ 划分方式2——四类基本逻辑结构 2.物理结构(存储结构) 1.顺序存储结构 2.链接存储结构 3…

【贪心算法】Leetcode 455.分发饼干 376. 摆动序列 53. 最大子数组和

【贪心算法】Leetcode 455 分发饼干 376. 摆动序列【规律很多】53. 最大子数组和 455 分发饼干局部最优推全局最优:尽量用大饼干去满足大胃口的小朋友 376. 摆动序列【规律很多】思想:注意考虑一个坡度留首尾两个点、平坡、首尾 53. 最大子数组和【好思想…

SD-WAN助力企业数据传输安全

随着企业网络需求的不断增长,SD-WAN成为企业网络组网的首选方案,能够实现多种网络拓扑结构的无缝连接,其中包括总部-分支、总部-分支-数据中心、总部-数据中心、总部-分支-云服务等。如何确保企业数据在传输过程中的安全性成为企业关注的重要…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的植物病害检测系统(Python+PySide6界面+训练代码)

摘要:开发高效的植物病害检测系统对于提升农业生产效率和作物健康管理意义重大。本篇博客详细阐述了如何运用深度学习技术构建一个植物病害检测系统,并提供了完整的实现代码。该系统基于先进的YOLOv8算法,对YOLOv7、YOLOv6、YOLOv5进行了性能…

用指针数组完成单词倒排

描述 对字符串中的所有单词进行倒排。 说明: 1、构成单词的字符只有26个大写或小写英文字母; 2、非构成单词的字符均视为单词间隔符; 3、要求倒排后的单词间隔符以一个空格表示;如果原字符串中相邻单词间有多个间隔符时&…

更换个人开发环境后,pycharm连接服务器报错Authentication failed

原因:服务器中更换个人开发环境后,密码变了。 解决:在pycharm中修改服务器开发环境密码即可。 1 找到Tools-Depolyment-Configuration 2 点击SSH Configuration后的省略号 3 修改这里面的Password即可

SVN教程-SVN的基本使用

SVN(Apache Subversion)是一款强大的集中式版本控制系统,它在软件开发项目中扮演着至关重要的角色,用于有效地跟踪、记录和管理代码的演变过程。与分布式系统相比,SVN 的集中式架构使得团队能够更加协同地进行开发&…

Docker将本地的镜像上传到私有仓库

使用register镜像创建私有仓库 [rootopenEuler-node1 ~]# docker run --restartalways -d -p 5000:5000 -v /opt/data/regostry:/var/lib/registry registry:2[rootopenEuler-node1 ~]# docker images REPOSITORY TAG IMAGE…

DataIntegrityViolationException异常产生原因及解决方案

DataIntegrityViolationException异常产生原因及解决方案 01 异常的发生场景 在我新写了一个接口之后出现的 //org.springframework.dao.DataIntegrityViolationException日志报错的意思是参数设置了一个错误的值 02 异常的产生及其原因 我最开始认为是MySQL数据库表设计…

【学习总结】什么是DoS和DDoS

[Q&A] 什么是DoS DoS 是 “Denial of Service”(拒绝服务)的缩写,它是一种网络攻击方式,其目的是使目标计算机或网络资源无法为合法用户提供正常的服务。通过向目标系统发送大量请求、消耗其带宽、处理器或内存等资源&#…

网络安全: Kali Linux 使用 nmap 扫描目标主机

目录 一、实验 1.环境 2. Kali Linux (2024.1) 使用 namp 扫描目标主机 3.Kali Linux (2024.1)远程登录 Windows Server 4.Kali Linux (2024.1) 使用crunch字典工具 5.Kali Linux (2024.1)使用hydra密码工具 6.Kali Linux (2022.3) 通过SSH端口获取 Ubuntu 密码 二、问题…

备考2024年北京高考数学:20114~2023十年选择题练习和解析

距离2024年高考还有三个月的时间,如何用三个月的时间再提高北京数学高考的成绩?吃透历年真题以及背后的知识点是行之有效的方法 之一。 今天我们来看一下2014-2023年的北京市高考数学的选择题,从过去十年(2014-2023)的…

SandBox中的JavaAgent技术

8.1 JavaAgent Java Agent 是一种强大的技术,在运行时动态修改已加载类的字节码,为应用程序注入额外的功能和行为。 JDK 1.5 支持静态 Instrumentation,基本的思路是在 JVM 启动的时候添加一个代理(javaagent)&#…

【python debug】python常见编译问题解决方法_2

序言 记录python使用过程中碰到的一些问题及其解决方法上一篇:python常见编译问题解决方法_1 1. PermissionError: [Errno 13] Permission denied: ‘/lostfound’ 修改前: 修改后(解决): 此外,可能文件夹…

048 异常

什么是异常 异常体系结构 异常的继承关系 Error Exception 异常处理机制 try:用{}将可能产生异常的代码包裹catch:与try搭配使用,捕获try包裹代码中抛出的异常并进行后续动作finally:跟在try后,在try和catch之后执行…