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

news2024/11/27 10:42:42

设计模式专栏: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.coloradmin.cn/o/1599694.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试:lock 和 synchronized

一、语法层面 synchronized 是关键字,源码在jvm中,用c语言实现Lock 是接口,源码由jdk提供,用java语言实现使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法…

Linux中进程和计划任务

一.程序 1.什么是程序 (1)是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具 (2)用于描述进程要完成的功能,是控制进程执行的指令集 二.进程 1.什么是进程…

量化过程信息损耗分析(MATLAB)

MATLAB代码 clear_all; Mrand(5,5)*100;% 假设M是待转换的矩阵 a min(M(:)); b max(M(:));% 将M映射到[0, 255] M_mapped functionA(M, a, b); M_mapped_floorfloor(M_mapped); % 将M_mapped恢复到原始范围 M_original functionB(M_mapped_floor, a, b);disp(M); disp(M_m…

【吊打面试官系列】Java高并发篇 - 什么是多线程中的上下文切换?

大家好,我是锋哥。今天分享关于 【什么是多线程中的上下文切换?】面试题,希望对大家有帮助; 什么是多线程中的上下文切换? 在上下文切换过程中,CPU 会停止处理当前运行的程序,并保存当前程序运行…

【Altium Designer 20 笔记】PCB铺铜过程

PCB铺铜步骤 切换到Keep-Out Layer(禁止布线层) 使用shifts键切换单层显示 画禁止布线范围(防止铺铜过大) 切换到需要铺铜的层 选择铺铜网络,通常是地(GND)或某个电源网络 隐藏覆铜:…

为什么说linux操作系统要比windows稳定?

正常人说windows的时候是指一整套桌面系统,而说linux的时候是一个命令行内核。后者从原理上就比前者稳定。 如果你日常使用的是linux的发行版桌面系统,看到这话估计直接想骂娘。就我12年的ubuntu使用体验来说,定期备份系统是必须的&#xff…

MySQL DDL 通用语法

🌹作者主页:青花锁 🌹简介:Java领域优质创作者🏆、Java微服务架构公号作者😄 🌹简历模板、学习资料、面试题库、技术互助 🌹文末获取联系方式 📝 往期热门专栏回顾 专栏…

NIO学习

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

C++设计模式|创建型 2.工厂模式

1.简单工厂思想 简单工厂模式不属于23种设计模式之⼀,更多的是⼀种编程习惯。它的核心思想是将产品的创建过程封装在⼀个⼯⼚类中,把创建对象的流程集中在这个⼯⼚类⾥⾯。卡码网将其结构描述为下图所示的情况: 简单⼯⼚模式包括三个主要⻆⾊…

【STL详解 —— priority_queue的使用与模拟实现】

STL详解 —— priority_queue的使用与模拟实现 priority_queue的使用priority_queue的介绍priority_queue的定义方式priority_queue各个接口的使用 priority_queue的模拟实现仿函数priority_queue的模拟实现 priority_queue的使用 priority_queue的介绍 std::priority_queue 是…

基于Echarts的超市销售可视化分析系统(数据+程序+论文

本论文旨在研究Python技术和ECharts可视化技术在超市销售数据分析系统中的应用。本系统通过对超市销售数据进行分析和可视化展示,帮助决策层更好地了解销售情况和趋势,进而做出更有针对性的决策。本系统主要包括数据处理、数据可视化和系统测试三个模块。…

专项1:理论横向误差计算

1.前言 车辆实际位置与轨迹要求的位置的误差大小是反映自动驾驶控制精度的关键性指标,也是作为控制系统的输入量。在对车辆的控制算法进行研究时候,首先需要厘清控制系统的输入。控制系统的输入的关键性环节就是笛卡尔坐标系和frent坐标系之间的转换。 …

【进阶篇】四、字节码增强框架:ASM、ByteBuddy

文章目录 1、ASM2、ASM字节码增强3、ASM入门案例4、ASM Java Agent实现增强类的方法5、Byte Buddy6、Byte Buddy案例 相比自己的代码里用Spring AOP添加某些功能,字节码增强更适配无侵入式的Java Agent场景。比如下面写个Java Agent打印 任意Java程序中方法执行的…

电商技术揭秘九:搜索引擎中的SEO数据分析与效果评估

相关系列文章 电商技术揭秘一:电商架构设计与核心技术 电商技术揭秘二:电商平台推荐系统的实现与优化 电商技术揭秘三:电商平台的支付与结算系统 电商技术揭秘四:电商平台的物流管理系统 电商技术揭秘五:电商平台的个性…

DC-3渗透测试复现

DC-3渗透测试复现 目的: 获取最高权限以及5个flag 过程: 信息打点-sql注入-反弹shell- pkexec提权(CVE-2021-4034) 环境: 攻击机:kali(192.168.85.136) 靶机:DC_3(192.168.85.133) 复现…

记录一下hive跑spark的insert,update语句报类找不到的问题

我hive能正常启动,建表没问题,我建了一个student表,没问题,但执行了下面一条insert语句后报如下错误: hive (default)> insert into table student values(1,abc); Query ID atguigu_20240417184003_f9d459d7-199…

「每日跟读」英语常用句型公式 第13篇

「每日跟读」英语常用句型公式 第13篇 1. How was __? __怎么样? How was the concert last night? (昨晚的音乐会怎么样?) How was your trip to the museum? (你去博物馆的旅行怎么样?&#xff09…

Rust腐蚀服务器修改背景和logo图片操作方法

Rust腐蚀服务器修改背景和logo图片操作方法 大家好我是艾西一个做服务器租用的网络架构师。在我们自己搭建的rust服务器游戏设定以及玩法都是完全按照自己的想法设定的,如果你是一个社区服那么对于进游戏的主页以及Logo肯定会有自己的想法。这个东西可以理解为做一…

嵌入式4-16

tftpd #include <myhead.h> #define SER_IP "192.168.125.243" //服务器IP地址 #define SER_PORT 69 //服务器端口号 #define CLI_IP "192.168.125.244" //客户端IP地址 #define CLI_PORT 8889 //客户端端…

MSQL DML数据操作语言

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 往期热门专栏回顾 专栏…