loD:如何实现代码的“高内聚、低耦合“

设计模式专栏:http://t.csdnimg.cn/3a25S

目录

1.引用

2.何为"高内聚、低耦合"

3.LoD 的定义描述

4.定义解读与代码示例一

5.定义解读与代码示例二


1.引用

        本节介绍最后一个设计原则:LoD(Law of Demeter,迪米特法则)。尽LoD不像SOLID、KISS和DRY原则那样被广大程序员熟知,但它非常实用。这条设计原能够帮助我们实现代码的“高内聚、低耦合”。

2.何为"高内聚、低耦合"

        "高内聚、低耦合"是一个非常重要的设计思想,能够有效地提高代码的可读性和可性,能够缩小功能改动引起的代码改动范围。实际上,在之前,我们已经多次提这个设计思想。很多设计原则都以实现代码的“高内聚、低耦合”为目标,如单一职责原则基于接口而非实现编程等。

        "高内聚、低耦合"是一个通用的设计思想,可以用来指导系统、模块、类和函数的计开发,也可以应用到微服务、框架、组件和类库等的设计开发中。为了讲解方便,我们“类”作为这个设计思想的应用对象,至于其他应用场景,读者可以自行类比。

        "高内聚"用来指导类本身的设计,指的是相近的功能应该放到同一个类中,不相近的功能不要放到同一类中、相近的功能往往会被同时修改,如果放到同一个类中,那么代码可集中修改,也容易维护。单一职责原则是实现代码高内聚的有效的设计原则。

        "低耦合"用来指导类之间依赖关系的设计,指的是在代码中,类之间的依赖关系要简单、清晰。即使两个类有依赖关系,一个类的代码的改动不会或很少导致依赖类的代码的改动。前者提到的依赖注入、接口隔离和基于接口而非实现编程,以及本节介绍的LoD,都是为了实现代码的低耦合。

        注意,"内聚"和"耦合"并非完全独立,“高内聚”有助于“低耦合”。同理,“低内聚”会导致“高耦合”。例如,下图左边所示的代码结构呈现“高内聚、低耦合”,右边所示的代码结构呈现“低内聚、高耦合”。

        在上面左边所示的代码结构中,每个类的职责单一,不同的功能被放到不同的类中,代码的内聚性高。因为职责单一,所以每个类被依赖的类就会比较少,代码的耦合度低,一个类的修改只会影响一个依赖类的代码的改动。在上图右边所示的代码结构中,类的职责不够单一功能大而全,不相近的功能放到了同一个类中,导致依赖关系复杂。在这种情况下,当我们需要修改某个类时,影响的类比较多。从上图我们可以看出,高内聚、低耦合的代码的结构更加简单、清晰,相应地,代码的可维护性和可读性更好。

3.LoD 的定义描述

        单从"LoD"这个名字来看,我们完全猜不出这条设计原则讲的是什么。其实,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 strangers.”对应的中文为:每个模块(unit)只应该了解那些与它关系密切的模块(unit: only units“closely" related to the current unit)的有限知识(knowledge),或者说,每个模块只和自己的“朋友”“说话”(talk),不和“陌生人”“说话”。

        大部分设计原则和设计思想都非常抽象,不同的人可能有不同的解读,如果我们想要将它们灵活地应用到实际开发中,那么需要实战经验支撑,LoD也不例外。于是,作者结合自己易理解和以往的经验,对LoD的定文进行了重新描述,不应该存在直接依赖关系的类之间不要有依赖,有依赖关系的类之间尽量只依赖必要的接口(也就是上面LoD定义描述中的“有限知识”),注意,为了讲解统一,作者把原定义描述中的“模块”替换成了“类”。

        从上面作者给出的描述中,我们可以看出,LoD包含前后两部分,这两个部分讲的是两件事情,下面通过两个代码示例进行解读。

4.定义解读与代码示例一

        我们先来看作者给出的LoD定义描述中的前半部分:应该存在直接依赖关系的类之间不要有依赖。我们通过一个简单的代码示例进行解读,在这个代码示例中,我们实现了简化的搜索引擎“爬取”网页的功能。这段代的包合3个类,其中,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 HtmlReyuest(url));return new Html(rawHtml );}
}public class Document{private Html html;private String url;  public Document(String url){this.url = url;HtmlDownloader downloader = new HtmlDownloeder();this.html = downloader.downloadHtml(url);}
}

        虽然上述代码能够实现基本功能,但存在较多设计缺陷。我们先来分析NetworkTransporter类。NetworkTransporter类作为一个底层网络通信类我们希望它的功能是通用的,而不只是服务于下载HTML网页,因此,它不应该直接依HtmlRequest类。从这一点上来讲,NetworkTransporter类的设计违反 LoD。

        如何重构NetworkTransporter类才能满足LoD呢?我们举一个比较形象的例子,假如我们去商店买东西,在结账的时候,肯定不会直接把钱包给收营员,让收银员自己重里面拿钱,而类中的address和content(HtmlRequest类的定义在上面的代码中并为给出),它包含address类相当于收银员。我们应该把address和content交给NetworkTransporter类,而非直接把HtmlRequest类交给NetworkTransporter类,让NetworkTransporter自己取出address和content。根据这个思路,我们对NetworkTransporter类进行重构,重构后的代码如下所示:

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

        我们再来分析 HtmlDownloader类。HtmlDownloader类原来的设计是没有问题的,不过我们修改了 NetworkTransporer 类中 sond()函数的定义,而 HtmlDownloader类调用了send()函数,因此,HtmlDownloader类也要做相应的修改。修改后的代码如下所示。

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

        最后,我们分析Document类。Document类中存在下列3个问题。第一,构造函数中的downloader.downloadHtml()的逻辑比较复杂,执行耗时长,不方便测试,因此它不应该放到构造函数中。第二,HtmlDownloader 类的对象在构造函数中通过new创建,违反了基于接口面非实现编程的设计思想,也降低了代码的可测试性。第三,Document类依赖了不该依赖的HtmlDownloader类,违反了LoD。

        虽然Document类中有3个问题,但修改一处即可解决所有问题。修改之后的代码如下所示。

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

5.定义解读与代码示例二

        现在,我们再来看一下作者给出LoD定义描述中后半部分:“有依赖关系的类之间尽量只依赖必要的接口”。我们还是结合一个代码示例进行讲解。下面这段代码中的Serialization 类负责对象的序列化和反序列化。

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

        单看 Serialization类的设计,一点问题都没有。不过,如果把 Serialization 类放到一定应用场景中,如有些类只用到了序列化操作,而另一些类只用到了反序列化操作,那么,于“有依赖关系的类之间尽量只依赖必要的接口”,只用到序列化操作的那些类不应该依赖反序列化接口,只用到反序列化操作的那些类不应该依赖序列化接口,因此,我们应该Serialization类拆分为两个更小粒度的类,一个类(Serializer类)只负责序列化,另一个类(Deserializer 类)只负责反序列化。拆分之后,使用序列化操作的类只需要依赖Serializar类使用反序列化操作的类只需要依赖 Deserializer类。拆分之后的代码如下所示。

public class Serializer{public string serialize(0bject object){String serializedResult = ...;...return serializedResult;}
}public class Deserializer {public object deserialize(String str){0bject deserializedResult = ...;...return deserializedResult;}
}

        不过,尽管拆分之后的代码满足LoD,但违反了高内聚的设计思想。高内聚要求相近的功能在同一个类中实现,当需要修改功能时,修改之处不会分散。对于上面的这个例子,如果修改了序列化的实现方式,如从JSON换成XML, 那么反序列化的实现方式也需要一并修改,也就是说,在Serialization类未拆分之前,只需要修改一个类,而在拆分之后,需要修改两个类。显然,拆分之后的代码的改动范围变大了。

        如果我们既不想违反高内聚的设计思想,又不想违反LoD,那么怎么办呢?实际上,引入两个接口就能轻松解决这个问题。具体代码如下所示。

public interface serializable{String serialize(0bject object);
}public interface Deserializable {object deserialize(string text);
}public class serialization implements serializable, Deserializable {@Overridepublic String serialize(object object){String serializedResult = ...;...return serializedResult;}
}@Override
public object deserialize(String str){0bject deserializedResult = ...;...return deserializedResult;
}public class DemoClass_l{private Serializable serializer;public Demo(Serializable serializer){this.serializer = serializer;}...
}public class Democlass_2{private Deserializable deserializer;public Demo(Deserializable deserializer){this.deserializer = deserializer;}....
}

        尽管我们还是需要向DemoClass_1类的构造函数中传入同时包含序列化和反序列化操作的Serialization类,但是,DemoClass_1类依赖的Seializable接口只包含序列化操作,因此DemoClass_1类无法使用Serialization类中的反序列化函数,即对反序列化操作无“感知”,这就符合了作者给出的LoD定义描述的后半部分“有依赖关系的类之间尽量只依赖必要的接口”的要求。

        Serialization类包含序列化和反序列化两个操作,只使用序列化操作的使用者即便能够“感知”到另一个函数(反序列化函数),其实也是可以接受的,那么,为了满足LoD,将一个简单的类拆分成两个接口,是否是过度设计呢?

        设计原则本身没有对错。判定设计模式的应用是否合理,我们结合应用场景,具体问题具体分析。

        对于Serialization类,虽然只包含了序列化和反序列化两个操作,看似没有必要拆分成两个接口,但是,如果我们向Serialization类中添加更多的序列化和反序列化函数,如下面的代码所示,那么,序列化操作和反序列化操作的拆分就是合理的。

public class Serializer{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){...}
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ncnn模型部署——训练自己YOLOv5模型转ncnn模型并部署到Android手机端

目录 一、前述二、源码包准备2.1 配套源码包2.2 官网源码包2.2.1 ncnn版YOLOv5源码包下载2.2.2 ncnn预编译库下载2.2.3 拷贝ncnn预编译库 三、可能遇到问题3.1 gradle下载失败3.2 CMake问题3.2.1 报错3.2.2 问题分析3.2.3 解决办法3.2.4 添加环境变量3.2.5 测试CMake 3.3 Unabl…

27. 【Android教程】下拉选择框 Spinner

本节我们将学习 Android 提供的下拉选择框——Spinner,它也是 Adapter 的常客。不仅仅是在 Android 端,在 Windows 上我们也经常会看到 Spinner 类型的样式。通常它是以下拉的形式存在,Spinner 在下拉列表中包含很多可供用户选择的选项&#…

Docker+Uwsgi部署Django项目

在之前的文章中,已经给大家分享了在docker中使用django自带的命令部署项目,这篇文章主要讲解如何使用uwsgi部署。 1. 在Django项目的根目录下新建Dockerfile文件 #Dockerfile文件 # 使用 Python 3.9 作为基础镜像 FROM python:3.9# 设置工作目录 WORKDI…

Intel显卡驱动导致Qt opengl 渲染YUV时拉伸窗口内存泄漏

最近在使用QOpenGLWidget做YUV视频渲染,发现在拉伸窗口的时候内存暴涨,如果窗口不动则内存不变。 可以得出结论一定是resizeGL出了问题,但是其实这里代码很简单 glViewport(0, 0, w, h); 还有就是变换矩阵计算,根本没资源建立与释…

大模型微调的几种常见方法

在文章深入理解大语言模型微调技术中,我们详细了解大语言模型微调的概念和训练过程,本篇给大家介绍大模型微调常见的7种训练方法。 1、Adapter Tuning 2019年谷歌的研究人员首次在论文《Parameter-Efficient Transfer Learning for NLP》提出针对 BERT 的…

NIO学习

文章目录 前言一、主要模块二、使用步骤1.服务器端2.客户端 三、NIO零拷贝(推荐)四、NIO另一种copy总结 前言 NIO是JDK1.4版本带来的功能,区别于以往的BIO编程,同步非阻塞极大的节省资源开销,避免了线程切换和上下文切换带来的资源浪费。 一、主要模块 Selector&a…

ENSP-旁挂式AC

提醒:如果AC不能成功上线AP,一般问题不会出在AC上,优先关注AC-AP线路上的二层或三层组网的三层交换机 拓扑图 管理VLAN:99 | 业务VLAN:100 注意点: 1.连接AP的接口需要打上pvid为管理vlan的标签 2.AC和…

Vitis HLS 学习笔记--readVec2Stream 函数-探究

目录 1. 高效内存存取的背景 2. readVec2Stream() 参数 3. 函数实现 4. 总结 1. 高效内存存取的背景 在深入研究《Vitis HLS 学习笔记--scal 函数探究》一篇文章之后,我们对于scal()函数如何将Y alpha * X这种简单的乘法运算复杂化有了深刻的理解。本文将转向…

imgcat 工具

如果经常在远程服务器或嵌入式设备中操作图片,要查看图片效果,就要先把图片dump到本地,比较麻烦。可以使用这个工具,直接在终端上显示。类似于这种效果。 imgcat 是一个终端工具,使用 iTerm2 内置的特性,允…

FOR循环指令计算累加和(CODESYS ST+SMART梯形图代码)

1、SMART PLC FOR循环指令应用 SMART PLC FOR循环指令_smart plc可以调用多少次for循环-CSDN博客文章浏览阅读2.4k次,点赞2次,收藏6次。SMART PLC的FOR循环: PLC里写需要加上: NEXT指令_smart plc可以调用多少次for循环https://r…

2024 年10个最佳 Ruby 测试框架

QA一直在寻找最好的自动化测试框架,这些框架提供丰富的功能、简单的语法、更好的兼容性和更快的执行速度。如果您选择结合使用Ruby和Selenium进行Web测试,可能需要搜索基于Ruby的测试框架进行Web应用程序测试。 Ruby测试框架提供了广泛的功能&#xff0…

打一把王者的时间,学会web页面测试方法与测试用例编写

一、输入框 1、字符型输入框: (1)字符型输入框:英文全角、英文半角、数字、空或者空格、特殊字符“~!#¥%……&*?[]{}”特别要注意单引号和&符号。禁止直接输入特殊字符时,…