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 {
    @Override
    public 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.mfbz.cn/a/549256.html

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

相关文章

CTFshow-PWN-Test_your_nc(pwn0-pwn4)

1、pwn0 连上,等它程序执行完你可以直接来到 shell 界面 执行命令,获取 flag ctfshow{294ffc57-ee28-40ea-8c74-4dfeaf89d1e7} 2、pwn1 提供一个后门函数,连上即可得到flag 下载附件,拉进 ubantu ,使用命令 checksec …

深度学习基础——计算量、参数量和推理时间

深度学习基础——计算量、参数量和推理时间 在深度学习中,计算量、参数量和推理时间是评估模型性能和效率的重要指标。本文将介绍这三个指标的定义、计算方法以及如何使用Python进行实现和可视化展示,以帮助读者更好地理解和评估深度学习模型。 1. 定义…

oracle 19c数据库W00n进程使用很多PGA内存资源的分析

今天,客户反馈测试环境的数据库PGA资源不足,报错ORA-04036: 实例使用的 PGA 内存超出 PGA_AGGREGATE_LIMIT;分析是多个W00n进程使用大量PGA-触发了BUG,对应解决办法就是打补丁。(民间办法就是KILL进程、重启数据库&…

【JavaSE】搞定String类

前言 本篇会细致讲解String类的常见用法,让小伙伴们搞定String类~ 欢迎关注个人主页:逸狼 创造不易,可以点点赞吗~ 如有错误,欢迎指出~ 目录 前言 常用的三种字符串构造 字符串长度length 字符串比较 比较 比较字符串的内容equals…

一个不努力学习的人是怎么过的软考高项?

首先要感谢软考方式的改革,如果不是机考,我可能也过不了,因为自己的笔迹实在太糟糕了。其实如果不是因为笔迹太差,我觉得我19年高项就过了,19年栽倒在论文上,只得了43分,我记忆深刻。 然后说一…

【算法】深入理解二分查找算法及其应用

文章目录 1. 朴素二分查找的基本步骤:2. 总结二分模板 二分查找(Binary Search)是一种在有序数组中查找目标值的高效算法。它的基本思想是将数组分成两半,然后确定目标值可能存在的那一半,重复这个过程直到找到目标值或…

如何进行支付功能的测试?

非现金支付时代&#xff0c;非现金支付已经成为了生活不可或缺的一部分&#xff0c;我们只需要一台手机便可走遍全国各地&#xff08;前提是支付宝&#xff0c;微信有钱<00>&#xff09;。 那么作为测试人员&#xff0c;支付测试也是非常重要的一环&#xff0c;那么下面…

隐私保护?还是安全漏洞?邮箱分身双重身份及创建攻略解析!

很多人只知道微信、QQ等应用分身&#xff0c;对于邮箱分身并不是很了解。邮箱分身和他们的不同点在于我们直接在原有邮箱的基础上创立新的虚拟邮箱地址&#xff0c;并且密码一致&#xff0c;在我们需要运营多个社交媒体账号或者管理多个项目的情况下&#xff0c;邮箱分身是一个…

IntelliJ IDEA2024 安装包(亲测可用)

目录 一、软件简介 二、软件下载 一、软件简介 IDEA&#xff08;Integrated Development Environment for Apache&#xff09; 是一款专为 Apache 开发者设计的集成开发环境。该软件提供了丰富的功能和工具&#xff0c;帮助开发者更高效地创建、调试和部署 Apache 项目。 主…

MobaXterm无法登陆oracle cloud的问题

问题 我在oracle cloud上创建实例的时候&#xff0c;只能使用密钥的方式登陆&#xff0c;当时下载了私钥文件。实例创建好以后&#xff0c;在mobaxterm上使用这个私钥文件无法登陆 排查 尝试使用mobaxterm的keygen&#xff0c;把私钥文件转成ppk格式&#xff0c;还是不行。…

【论文阅读02】一种基于双通道的水下图像增强卷积神经网络

来源&#xff1a;海洋论坛▏一种基于双通道的水下图像增强卷积神经网络 当前不会的 一、背景&#xff1a; 水下图像增强方法包含有无水下成像模型的水下图像增强方法、基于水下成像模型的水下图像恢复方法、水下成像模型与深度学习相结合的方法以及完全采用深度学习的方…

STM32H7的8个串口fifo收发(兼容232和485)

STM32H7的8个串口fifo收发&#xff08;兼容232和485&#xff09; 串口硬件串口时序串口高级特性同步和异步的区别单工、半双工、全双工的区别 STM32H78个串口fifo驱动定义数据结构uart_fifo.huart驱动包括中断配置等 应用示例RS485深入理解 仅供学习。 USART 的全称是 Universa…

springcloud 整合swagger文档教程

我用的是nacos和gateway 我的模块 父依赖没什么太大关系如果出现版本冲突问题可用参考我的依赖版本 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org…

试试流量回放,不用再写烦人的自动化测试case了

接触过接口自动化测试的同学都知道&#xff0c;我们一般要基于某种自动化测试框架&#xff0c;编写自动化case&#xff0c;编写自动化case的依据来源于接口文档&#xff0c;对照接口文档里面的请求参数进行人工添加接口自动化case 其实&#xff0c;对于日常新的服务端需求的迭…

Vue3(二):报错调试,vue3响应式原理、computed和watch,ref,props,接口

一、准备工作调试 跟着张天禹老师看前几集的时候可能会遇到如下问题&#xff1a; 1.下载插件&#xff1a;Vue Language Features (Volar)或者直接下载vue-offical 2.npm run serve时运行时出现错误&#xff1a;Error: vitejs/plugin-vue requires vue (&#xff1e;3.2.13) …

Python 入门指南(五)

原文&#xff1a;zh.annas-archive.org/md5/97bc15629f1b51a0671040c56db61b92 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第十六章&#xff1a;Python 中的对象 因此&#xff0c;我们现在手头上有一个设计&#xff0c;并且准备将该设计转化为一个可工作的程序&a…

解决npm run dev跑项目,发现node版本不匹配,怎么跑起来?【已解决】

首先问题点就是我们npm run dev 运行项目的时候发现出错&#xff0c;跑不起来&#xff0c;类型下面这种 这里的出错的原因在于我们的node版本跟项目的版本不匹配 解决办法 我这里的问题是我的版本是node14的&#xff0c;然后项目需要node20的&#xff0c;执行下面的就可以正…

JavaScript事件监听测试代码

效果图 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>信息填写页面</title><link …

Linux 内核学习(2) --- regulator 框架

目录 Regulator 介绍Regulator provider 注册struct regulator_descstruct regualtor_configDTS 配置和解析On BoardConfig 配置regulator_ops总结 Regulator Consumer 使用struct regulator 获取regulator 操作使用Multi Regulator 参考博客 Regulator 介绍 Regulator 指的是…

黄金价格上涨对白银的影响是什么?

在金融市场上&#xff0c;黄金与白银通常被视为避险资产&#xff0c;它们的价格走势往往受到多种因素的影响。近期&#xff0c;随着全球经济的波动加剧&#xff0c;黄金价格出现了上涨趋势。这自然会对与之紧密相关的白银市场产生影响。具体来说&#xff0c;黄金价格上涨通常会…