代码设计的内功——代码设计原则

article/2025/10/3 1:15:26

引言

        好代码是设计出来的,也是重构出来的,更是不断迭代出来的。在我们接到需求,经过概要设计过后就要着手进行编码了。但是在实际编码之前,我们还需要进行领域分层设计以及代码结构设计。那么怎么样才能设计出来比较优雅的代码结构呢?

        本文总结了软件代码设计中的五大原则,按照我自己的理解,这五大原则就是程序猿代码设计的内功,而二十三种设计模式实际就是内功催生出来的编程招式,因此深入理解五大设计原则是我们用好设计模式的基础,也是我们在平时设计代码结构的时候需要遵循的一些常见规范。只有不断的在设计代码-》遵循规范-》编写代码-》重构这个循环中磨砺,我们才能编写出优雅的代码。

        有一些大神们总结出来的优雅代码的设计原则,我们分别来看下。

1.SRP  职责单一原则


        SRP(Single Responsibility Principle)原则就是职责单一原则,从字面意思上面好像很好理解,一看就知道什么意思。但是看的会不一定就代表我们就会用,有的时候我们以为我们自己会了,但是在实际应用的时候又会遇到这样或者那样的问题。原因就是实际我们没有把问题想透,没有进行深度思考,知识还只是知识,并没有转化为我们的能力。就比如这里所说的职责单一原则指的是谁的单一职责,是类还是模块还是域呢?域可能包含多个模块,模块也可以包含多个类,这些都是问题。

        为了方便进行说明,这里以类来进行职责单一设计原则的说明。对于一个类来说,如果它只负责完成一个职责或者功能,那么我们可以说这个类时符合单一职责原则。请大家回想一下,其实我们在实际的编码过程中,已经有意无意的在使用单一职责设计原则了。因为实际它是符合我们人思考问题的方式的。为什么这么说呢?想想我们在整理衣柜的时候,为了方便拿衣服我们会把夏天的衣服放在一个柜子中,冬天的衣服放在一个柜子。这样季节变化的时候,我们只要到对应的柜子直接拿衣服就可以了。否则如果冬天和夏天的衣服都放在一个柜子中,我们找衣服的时候可就费劲了。放到软件代码设计中,我们也需要采用这样的分类思维。在进行类设计的时候,要设计粒度小、功能单一的类,而不是大而全的类。

        举个栗子,在学生管理系统中,如果一个类中既有学生信息的操作比如创建或者删除动作,又有关于课程的创建以及修改动作,那么我们可以认为这个类时不满足单一职责的设计原则的,因为它将两个不同业务域的业务混杂在了一起,所以我们需要进行拆分,将这个大而全的类拆分为学生以及课程两个业务域,这样粒度更细,更加内聚。

image.png

        笔者根据自身的经验,总结了需要考虑进行单一职责拆分的几个场,希望对大家判断是否需要进行拆分有个简单的判断的标准: 1、不同的业务域需要进行拆分,就像上面的例子,另外如果与其他类的依赖过多,也需要考虑是不是应该进行拆分; 2、如果我们在类中编写代码的时候发现私有方法具有一定的通用性,比如判断ip是不是合法,解析xml等,那我们可以考虑将这些方法抽出来形成公共的工具类,这样其他类也可以方便的进行使用。 另外单一职责的设计思想不止在代码设计中使用,我们在进行微服务拆分的时候也会一定程度的遵循这个原则。

2.OCP  对修改关闭,对扩展开放原则


OCP(Open Closed Principle)即对修改关闭,对扩展开放原则,个人觉得这是设计原则中最难的原则。不仅理解起来有一定的门槛,在实际编码过程中也是不容易做到的。 首先我们得先搞清楚这里的所说的修改以及扩展的区别在什么地方,说实话一开始看到这个原则的时候,我总觉得修改和开放说的不是一个意思嘛?想来想去都觉得有点迷糊。后来在不断的项目实践中,对这个设计原则的理解逐渐加深了。

image.png

设计原则中所说的修改指的是对原有代码的修改,而扩展指的是在原有代码基础上的能力的扩展,并不修改原先已有的代码。这是修改与扩展的最大的区别,一个需要修改原来的代码逻辑,另一个不修改。因此才叫对修改关闭但是对扩展开放。弄清楚修改和扩展的区别之后,我们再想一想为什么要对修改关闭,而要对扩展开放呢? 我们都知道软件平台都是不断进行更新迭代的,因此我们需要不断在原先的代码中进行开发。那么就会涉及到一个问题如果我们的代码设计的不好,扩展性不强,那么每次进行功能迭代的时候都会修改原先已有的代码,有修改就有可能引入bug,造成系统平台的不稳定。因此我们为了平台的稳定性,需要对修改关闭。但是我们要添加新的功能怎么办呢?那就是通过扩展的方式来进行,因此需要实现对扩展开放。

这里我们以一个例子来进行说明,否则可能还是有点抽象。在一个监控平台中,我们需要对服务所占用CPU、内存等运行信息进行监控,第一版代码如下。

public class Alarm {private AlarmRule alarmRule;private AlarmNotify alarmNotify;public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {this.alarmRule = alarmRule;this.alarmNotify = alarmNotify;}public void checkServiceStatus(String serviecName, int cpu, int memory) {if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)}if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)} }}

代码逻辑很简单,就是根据对应的告警规则中的阈值判断是否达到触发告警通知的条件。如果此时来了个需求,需要增加判断的条件,就是根据服务对应的状态,判断需不需要进行告警通知。我们来先看下比较low的修改方法。我们在checkServiceStatus方法中增加了服务状态的参数,同事在方法中增加了判断状态的逻辑。

public class Alarm {private AlarmRule alarmRule;private AlarmNotify alarmNotify;public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {this.alarmRule = alarmRule;this.alarmNotify = alarmNotify;}public void checkServiceStatus(String serviecName, int cpu, int memory, int status) {if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)}if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)} if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)} }}

很显然这种修改方法非常的不友好,为什么这么说呢?首先修改了方法参数,那么调用该方法的地方可能也需要修改,另外如果改方法有单元测试方法的话,单元测试用例必定也需要修改,在原有测试过的代码中添加新的逻辑,也增加了bug引入的风险。因此这种修改的方式我们需要进行避免。那么怎么修改才能够体现对修改关闭以及对扩展开放呢? 首先我们可以先将关于服务状态的属性抽象为一个ServiceStatus 实体,在对应的检查方法中以ServiceStatus 为入参,这样以后如果还有服务状态的属性增加的话,只需要在ServiceStatus 中添加即可,并不需要修改方法中的参数以及调用方法的地方,同样单元测试的方法也不用修改。

@Data
public class ServiceStatus {String serviecName;int cpu;int memory;int status;}

另外在检测方法中,我们怎么修改才能体现可扩展呢?而不是在检测方法中添加处理逻辑。一个比较好的实现方式就是通过抽象检测方法,具体的实现在各个实现类中。这样即使新增检测逻辑,只需要扩展检测实现方法就可,不需要在修改原先代码的逻辑,实现代码的可扩展。

3.LSP  里氏替换原则


        LSP(Liskov Substitution Principle)里氏替换原则,这个设计原则我觉得相较于前面的两个设计原则来说要简单些。它的内容为子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

        里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

        我们怎么判断有没有违背LSP呢?我觉得有两个关键点可以作为判断的依据,一个是子类有没有改变父类申明需要实现的业务功能,另一个是否违反父类关于输入、输出以及异常抛出的规定。

4.ISP 接口隔离原则


ISP(Interface Segregation Principle)接口隔离原则,简单理解就是只给调用方需要的接口,它不需要的就不要硬塞给他了。这里我们举个栗子,以下是关于产品的接口,其中包含了创建产品、删除产品、根据ID获取产品以及更新产品的接口。如果此时我们需要对外提供一个根据产品的类别获取产品的接口,我们应该怎么办?很多同学会说,这还不简单,我们直接在这个接口里面添加根据类别查询产品的接口就OK了啊。大家想想这个方案有没有什么问题。

public interface ProductService { boolean createProduct(Product product); boolean deleteProductById(long id); Product getProductById(long id); int updateProductInfo(Product product);
}public class UserServiceImpl implements UserService { //...}

这个方案看上去没什么问题,但是再往深处想一想,外部系统只需要一个根据产品类别查询商品的功能,,但是实际我们提供的接口中还包含了删除、更新商品的接口。如果这些接口被其他系统误调了可能会导致产品信息的删除或者误更新。因此我们可以将这些第三方调用的接口都隔离出来,这样就不存在误调用以及接口能力被无序扩散的情况了。

public interface ProductService { boolean createProduct(Product product); boolean deleteProductById(long id); Product getProductById(long id); int updateProductInfo(Product product);
}public interface ThirdSystemProductService{List<Product> getProductByType(int type);
}public class UserServiceImpl implements UserService { //...}

5.LOD 迪米特法则


        LOD(Law of Demeter)即迪米特法则,这是我们要介绍的最后一个代码设计法则了,光从名字上面上看,有点不明觉厉的感觉,看不出来到底到底表达个什么意思。我们可以来看下原文是怎么描述这个设计原则的。 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. 按照我自己的理解,这迪米特设计原则的最核心思想或者说最想达到的目的就是尽最大能力减小代码修改带来的对原有的系统的影响。所以需要实现类、模块或者服务能够实现高内聚、低耦合。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。打个比方这就像抗战时期的的地下组织一样,相关联的聚合到一起,但是与外部保持尽可能少的联系,也就是低耦合。

image.png

最后

大家好,我是MiniFat,感谢各位小伙伴点赞、收藏和评论,文章持续更新,我们下期再见! 


转载自: 偷偷看了同事的代码找到了优雅代码的秘密 - 掘金


http://chatgpt.dhexx.cn/article/MUaVj300.shtml

相关文章

二维傅里叶变换频谱图的含义

二维傅里叶变换频谱图的含义 在一维傅里叶变换得到的频谱图中&#xff0c;每个点表示其对应的幅度频率与其坐标对应的简谐波。二位傅里叶变换的频谱图&#xff0c;简谐波的振幅由对应点处对应的亮度表示&#xff0c;每一个点表示的波形为其对应的横纵坐标所表示的简谐波的叠加…

二维傅里叶变换深度研究-图像与其频域关系

一&#xff1a;二维傅里叶变换的数学原理 1.2D离散傅里叶公式解释&#xff1a; 那么&#xff0c;其F(u,v) 本质就是&#xff1a; 后续说明时的”频域”均指的其傅里叶功率谱&#xff0c;后面为了演示方便&#xff0c;所有频域图均经过了fftshift移动到中心位置。 2.2D傅里叶频…

使用matlab对图像进行二维傅里叶变换

这学期选了《图像工程基础》这门课&#xff0c;课上老师留了一个作业&#xff1a;对图像进行二维傅里叶变换。 现在我使用matlab解决这个问题 1.实验基本指令 首先我试了一下matlab图像处理的基本指令 原图&#xff1a; 经过以下指令后 将图片导入matlab后&#xff0c;命名…

二维离散傅里叶变换

在学完一维的傅里叶变换后&#xff0c;紧接着就是二维的傅里叶变换了。直接上干货吧&#xff01;&#xff01;&#xff01; 途中会用到opencv读取与显示图片。 一. 公式 M表示图像的行数&#xff0c;N表示图像的列数。 经过欧拉公式可以得一下形式&#xff0c;这样就可以轻松…

Matlab图像的二维傅里叶变换频谱图特点研究

一、先放一些相关的结论&#xff1a; 1、傅里叶变换的幅值称为傅里叶谱或频谱。 2、F(u)的零值位置与“盒状”函数的宽度W成反比。 3、卷积定理&#xff1a;空间域两个函数的卷积的傅里叶变换等于两个函数的傅里叶变换在频率域中的乘积。f(t)*h(t) <> H(u)F(u) 4、采…

OpenCV学习——图像二值化处理及二维傅里叶变换

小古在本学期选修了《计算机视觉原理与应用》&#xff0c;最近有一份作业 —— 利用matlab或者OpenCV对图像进行一些处理&#xff0c;由于完全没有接触过matlab和OpenCV,但是学习了一些python语言&#xff0c;所以便利用opencv-python来完成作业。 1 图像二值化处理 1.1 图像…

二维傅里叶变换是怎么进行的?

1.首先回顾一下一维FT 通俗来讲&#xff0c;一维傅里叶变换是将一个一维的信号分解成若干个三角波。 对于一个三角波而言&#xff0c;需要三个参数来确定它&#xff1a;频率,幅度 A &#xff0c;相位。因此在频域中&#xff0c;一维坐标代表频率&#xff0c;而每个坐标对应的…

二维傅里叶变换需知

from: https://blog.csdn.net/wenhao_ir/article/details/51037744 代码如下&#xff0c;这个代码是实现灰度图像作二维傅里叶变换后的非线性变换哈~ clear all; Iimread(coins.png); Ffft2((im2double(I))); Ffftshift(F); Fabs(F); Tlog(F1); subplot(1,2,1); imshow(F,[]…

傅里叶级数、一维傅里叶变换到二维傅里叶变换数理推导

傅里叶级数、一维傅里叶变换到二维傅里叶变换数理推导 参考资料&#xff1a; 如何理解傅里叶级数公式 二重傅里叶级数 从傅里叶级数到傅里叶变换 高维傅里叶变换的推导 连续傅里叶变换和离散傅里叶变换 二维离散傅里叶变换 IDL实现傅里叶变换 想要用傅里叶变换的思维处理一个…

二维傅里叶变换简化方式

在处理二维矩阵时&#xff0c;常想着如何把时域转换到频域来处理&#xff0c;因此翻来了以往数分里面的常用的傅里叶(Fourier Transform); &#xff08;Notes:一下公式中 M,N分别为二维矩阵的列数和行数&#xff0c;f(x,y) 代表改二维矩阵&#xff0c;F(u,v)为转换后的矩阵&…

C++中fftw库二维傅里叶变换笔记

目录 1.相关基础知识参考链接 2.二维傅里叶变换作用简介 3.FFTW二维傅里叶变换输出分析 &#xff08;1&#xff09;原始输出数据​ &#xff08;2&#xff09;频谱中心化后的输出数据 4.频谱图绘制 5.二维傅里叶变换逆变换 6.从输出结果中分离各平面波并画出波形平面图…

二维傅里叶变换的理解和使用

目录 似模似样的前言一维傅里叶二维傅里叶 似模似样的前言 最近的瑕疵检测项目需要在有纹理的产品上做很细致的检测。由于当前做项目使用的还是halcon居多&#xff0c;目前知道的方法还是傅里叶变换比较靠谱。 但仅靠halcon自带的样例并不能很好的理解和使用傅里叶&#xff0…

二维傅里叶变换频谱图的直观理解

众所周知&#xff1a;频谱中心代表低频&#xff0c;四周代表高频。 问&#xff1a;那&#xff08;u&#xff0c;v&#xff09;一点代表什么&#xff1f; 答&#xff1a; 1.当为水平方向的正弦图片时&#xff0c;二维傅里叶变换后&#xff0c;其只有u方向的频谱值&#xff1b; 2…

Matlab:二维傅里叶变换

Matlab:二维傅里叶变换 二维傅里叶变换二维衍射模式fft2 函数将二维数据变换为频率空间。例如,您可以变换二维光学掩膜以揭示其衍射模式。 二维傅里叶变换 以下公式定义 mn 矩阵 X 的离散傅里叶变换 Y。 i 是虚数单位,p 和 j 是值范围从 0 到 m–1 的索引,q 和 k 是值范围…

matlab二维傅里叶变换ffshift,形象理解二维傅里叶变换

点击上方“机器学习与生成对抗网络”,关注"星标" 获取有趣、好玩的前沿干货! 来自 | 知乎 阿姆斯特朗 链接 | https://zhuanlan.zhihu.com/p/110026009 文仅交流,侵删 1.回顾一下一维FT 公式: 通俗来讲,一维傅里叶变换是将一个一维的信号分解成若干个复指数波 …

傅里叶变换(二维离散傅里叶变换)

离散二维傅里叶变换 一常用性质&#xff1a; 可分离性、周期性和共轭对称性、平移性、旋转性质、卷积与相关定理&#xff1b; &#xff08;1&#xff09;可分离性&#xff1a; 二维离散傅里叶变换DFT可分离性的基本思想是DFT可分离为两次一维DFT。因此可以用通过计算两次一维…

二维傅里叶变换

fft2 函数将二维数据变换为频率空间。例如&#xff0c;可以变换二维光学掩膜以揭示其衍射模式。 二维傅里叶变换 以下公式定义 mn 矩阵 X 的离散傅里叶变换 Y。 和 是以下方程所定义的复单位根。 i 是虚数单位&#xff0c;p 和 j 是值范围从 0 到 m–1 的索引&#xff0c;q …

【深度好文】二维图像傅里叶变换 YYDS

1. 傅里叶变换原理 在数学中进行傅里叶变换为连续模拟信号&#xff0c;通常来说&#xff1a; 二维连续函数f(x,y)的傅里叶正变换为&#xff1a; 相应的傅里叶逆变换公式为&#xff1a; 但是在计算机领域&#xff0c;计算机一般处理的是数字信号&#xff0c;只能进行有限次计…

微信公众号开发模式没有域名怎么办?申请免费域名

微信公众号开发采用前后端分离模式&#xff0c;那么前端使用微信开发工具开发时&#xff0c;需要域名才能访问&#xff0c;那么域名从何而来呢&#xff1f; 1、申请域名 a)、NetApp申请免费域名 : https://natapp.cn/login b)、域名申请好以后&#xff0c;下载客户端&#xf…

freenom 申请免费域名

为了降低建站成本&#xff0c;可在freenom上申请免费域名&#xff0c;可以免费使用一年。 一. 注册域名 登录freenom.com&#xff0c;输入域名&#xff0c;检测通过后&#xff0c;输入邮箱&#xff0c;登录邮箱完成激活。 二. 配置DNS 上面申请的域名未绑定外网ip&#xff0c…