优雅代码的秘密,都藏在这6个设计原则中

article/2025/10/3 0:19:17

优雅的代码,犹如亭亭玉立的美女,让人赏心悦目。而糟糕的代码,却犹如屎山,让人避而远之。

如何写出优雅的代码呢?那就要理解并熟悉应用这6个设计原则啦:开闭原则、单一职责原则、接口隔离原则 、迪米特法则、里氏替换原则、依赖倒置原则。本文呢,将通过代码demo,让大家轻松理解这6个代码设计原则,加油~

1. 开闭原则

开闭原则,即对扩展开放,对修改关闭

对于扩展和修改,我们怎么去理解它呢?扩展开放表示,未来业务需求是变化万千,代码应该保持灵活的应变能力修改关闭表示不允许在原来类修改,保持稳定性

图片

因为日常需求是不断迭代更新的,所以我们经常需要在原来的代码中修改。如果代码设计得不好,扩展性不强,每次需求迭代,都要在原来代码中修改,很可能会引入bug。因此,我们的代码应该遵循开闭原则,也就是对扩展开放,对修改关闭

为了方便大家理解开闭原则,我们来看个例子:假设有这样的业务场景,大数据系统把文件推送过来,根据不同类型采取不同的解析方式。多数的小伙伴就会写出以下的代码:

if(type=="A"){//按照A格式解析}else if(type=="B"){//按B格式解析
}else{//按照默认格式解析
}

这段代码有什么问题呢?

  • 如果分支变多,这里的代码就会变得臃肿,难以维护,可读性低。
  • 如果你需要接入一种新的解析类型,那只能在原有代码上修改。

显然,增加、删除某个逻辑,都需要修改到原来类的代码,这就违反了开闭原则了。为了解决这个问题,我们可以使用策略模式去优化它。

你可以先声明一个文件解析的接口,如下:

public interface IFileStrategy {//属于哪种文件解析类型,A或者BFileTypeResolveEnum gainFileType();//封装的公用算法(具体的解析方法)void resolve(Object param);
}

然后实现不同策略的解析文件,如类型A解析:

@Component
public class AFileResolve implements IFileStrategy {@Overridepublic FileTypeResolveEnum gainFileType() {return FileTypeResolveEnum.File_A_RESOLVE;}@Overridepublic void resolve(Object objectparam) {logger.info("A 类型解析文件,参数:{}",objectparam);//A类型解析具体逻辑}
}

如果未来需求变更的话,比如增加、删除某个逻辑,不会再修改到原来的类啦,只需要修改对应的文件解析类型的类即可。

对于如何使用设计模式,大家有兴趣的话,可以看我以前的这篇文章哈:日常开发中常用到改善代码质量哪些设计模式–YYDS

2. 单一职责原则

单一职责原则:一个类或者一个接口,最好只负责一项职责。比如一个类C违反了单一原则,它负责两个职责P1P2。当职责P1需要修改时,就会改动到类C,这就可能导致原本正常的P2也受影响。

如何更好理解呢?比如你实现一个图书管理系统,一个类既有图书的增删改查,又有读者的增删改查,你就可以认为这个类违反了单一原则。因为这个类涉及了不同的功能职责点,你可以把这个拆分。

图片

以上图书管理系统这个例子,违反单一原则,按业务拆分。这比较好理解,但是有时候,一个类并不是那么好区分。这时候大家可以看这个标准,来判断功能职责是否单一:

  • 类中的私有方法过多
  • 你很难给类起一个合适的名字
  • 类中的代码行数、函数或者属性过多
  • 类中大量的方法都是集中操作类中的某几个属性
  • 类依赖的其他类过多,或者依赖类的其他类过多

比如,你写了一个方法,这个方法包括了日期处理借还书的业务操作,你就可以把日期处理抽到私有方法。再然后,如果你发现,很多私有方法,都是类似的日期处理,你就可以把这个日期处理方法抽成一个工具类。

日常开发中,单一原则的思想都有体现的。比如微服务拆分。

3. 接口隔离原则

接口隔离原则:接口的调用者或者使用者,不应该强迫依赖它不需要的接口。它要求建立单一的接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少,让接口中只包含客户(调用者)感兴趣的方法。即一个类对另一个类的依赖应该建立在最小的接口上。

比如类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说,都不是最小接口,则类B和类D必须去实现他们不需要的方法。如下图:

图片

这个图表达的意思是:类A依赖接口I中的method1method2,类B是对类A依赖的实现。类C依赖接口I中的method1method3,类D是对类C依赖的实现。对于实现类B和D,它们都存在用不到的方法,但是因为实现了接口I,所以必须要实现这些用不到的方法。

可以看下以下代码:

public interface I {void method1();void method2();void method3();
}@Service
public class A {@Resource(name="B")private I i;public void depend1() {i.method1();}public void depend2(){i.method2();}}@Service("B")
public class B implements I {@Overridepublic void method1() {System.out.println("类B实现接口I的方法1");}@Overridepublic void method2() {System.out.println("类B实现接口I的方法2");}//没用到这个方法,但是也要默认实现,因为I有这个接口方法@Overridepublic void method3() {}
}@Service
public class C {@Resource(name="D")private I i;public void depend1(I i){i.method1();}public void depend3(I i){i.method3();}}@Service("D")
public class D implements I {@Overridepublic void method1() {System.out.println("类D实现接口I的方法1");}//没用到这个方法,但是也要默认实现,因为I有这个接口方法@Overridepublic void method2() {}@Overridepublic void method3() {System.out.println("类D实现接口I的方法3");}
}

大家可以发现,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用到,实现类都必须去实现这些方法。实现类B没用到method3,它也要有个默认实现。实现类D没用到method2,它也要有个默认实现。

显然,这不是一个好的设计,违反了接口隔离原则。我们可以对接口I进行拆分。拆分后的设计如图2所示:

图片

接口是不是分得越细越好呢?并不是。日常开发中,采用接口隔离原则对接口进行约束时,要注意以下几点:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

4. 迪米特法则

定义:又叫最少知道原则。一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友谈心,不和陌生人说话。它的核心思想就是,尽量降低类与类之间的耦合,尽最大能力减小代码修改带来的对原有的系统的影响

比如一个生活例子:你对你的对象肯定了解的很多,但是如果你对别人的对象也了解很多,你的对象要是知道,那就要出大事了。

我们来看下一个违反迪米特法则的例子,业务场景是这样的:一个学校,要求打印出所有师生的ID。

//学生  
class Student{  private String id;  public void setId(String id){  this.id = id;  }  public String getId(){  return id;  }  
}  //老师  
class Teacher{  private String id;  public void setId(String id){  this.id = id;  }  public String getId(){  return id;  }  
}  //管理者(班长)
public class Monitor {//所有学生public List<Student> getAllStudent(){List<Student> list = new ArrayList<Student>();for(int i=0; i<100; i++){Student student = new Student();//为每个学生分配个IDstudent.setId("学生Id:"+i);list.add(student);}return list;}}//校长
public class Principal {//所有教师public List<Teacher> getAllTeacher(){List<Teacher> list = new ArrayList<Teacher>();for(int i=0; i<20; i++){Teacher emp = new Teacher();//为全校老师按顺序分配一个IDemp.setId("老师编号"+i);list.add(emp);}return list;}//所有师生public void printAllTeacherAndStudent(ClassMonitor classMonitor) {List<Student> list1 = classMonitor.getAllStudent();for (Student s : list1) {System.out.println(s.getId());}List<Teacher> list2 = this.getAllTeacher();for (Teacher teacher : list2) {System.out.println(teacher.getId());}}
}

这块代码。问题出在类Principal中,根据迪米特法则,只能与直接的朋友发生通信,而Student类并不是Principal类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲校长Principal只与管理者Monitor耦合就行了,可以让Principal继承类Monitor,重写一个printMember的方法。优化后的代码如下:

public class Monitor {public List<Student> getAllStudent(){List<Student> list = new ArrayList<Student>();for(int i=0; i<100; i++){Student student = new Student();//为每个学生分配个IDstudent.setId("学生Id:"+i);list.add(student);}return list;}public void printMember() {List<Student> list = this.getAllStudent();for (Student student : list) {System.out.println(student.getId());}}
}public class Principal extends Monitor {public List<Teacher> getAllTeacher(){List<Teacher> list = new ArrayList<Teacher>();for(int i=0; i<30; i++){Teacher emp = new Teacher();//为全校老师按顺序分配一个IDemp.setId("老师编号"+i);list.add(emp);}return list;}public void printMember() {super.printMember();for (Teacher teacher : this.getAllTeacher()) {System.out.println(teacher.getId());}}
}

5. 里氏替换原则

里氏替换原则:

如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

一句话来描述就是:只要有父类出现的地方,都可以用子类来替代,而且不会出现任何错误和异常。 更通俗点讲,就是子类可以扩展父类的功能,但是不能改变父类原有的功能。

其实,对里氏替换原则的定义可以总结如下:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  • 子类中可以增加自己特有的方法
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
  • 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等

我们来看个例子:

public class Cache {public void set(String key, String value) {}
}public class RedisCache extends Cache {public void set(String key, String value) {}}

这里例子是没有违反里氏替换原则的,任何父类、父接口出现的地方子类都可以出现。如果给RedisCache加上参数校验,如下:

public class Cache {public void set(String key, String value) {}
}public class RedisCache extends Cache {public void set(String key, String value) {if (key == null || key.length() < 10 || key.length() > 100) {System.out.println("key的长度不符合要求");throw new IllegalArgumentException();}}
}

这就违反了里氏替换原则了,因为子类方法增加了加了参数校验,抛出了异常,虽然子类仍然可以来替换父类。

6.依赖倒置原则

依赖倒置原则定义:

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。它的核心思想是:要面向接口编程,而不要面向实现编程

依赖倒置原则可以降低类间的耦合性、提高系统的稳定性、减少并行开发引起的风险、提高代码的可读性和可维护性。要满足依赖倒置原则,我们需要在项目中满足这个规则:

  • 每个类尽量提供接口或抽象类,或者两者都具备。
  • 变量的声明类型尽量是接口或者是抽象类。
  • 任何类都不应该从具体类派生。
  • 使用继承时尽量遵循里氏替换原则。

我们来看一段违反依赖倒置原则的代码,业务需求是:顾客从淘宝购物。代码如下:

class Customer{public void shopping(TaoBaoShop shop){//购物System.out.println(shop.buy());}
}

以上代码是存在问题的,如果未来产品变更需求,改为顾客从京东上购物,就需要把代码修改为:

class Customer{public void shopping(JingDongShop shop){//购物System.out.println(shop.buy());}
}

如果产品又变更为从天猫购物呢?那有得修改代码了,显然这违反了开闭原则。顾客类设计时,同具体的购物平台类绑定了,这违背了依赖倒置原则。可以设计一个shop接口,不同购物平台(如淘宝、京东)实现于这个接口,即修改顾客类面向该接口编程,就可以解决这个问题了。代码如下:

class Customer{public void shopping(Shop shop){//购物System.out.println(shop.buy());}
}interface Shop{String buy();
}Class TaoBaoShop implements Shop{public String buy(){return "从淘宝购物";}
}Class JingDongShop implements Shop{public String buy(){return "从京东购物";}
}

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

相关文章

低代码--低代码开发(LCDP)介绍

低代码开发&#xff08;LCDP&#xff09;介绍 1 介绍1.1 概述1.2 行业风向1.3 行业报告1.4 优点减少重复编程避免沟通隔阂提升效率 1.5 挑战完全抛弃代码的代价&#xff0c;就是平台能力与灵活性受限应用低代码平台阻力大技术局限老旧系统改造困难职业角色缺失应用者大多是技术…

网页设计个人主页代码

/ 01 / 前话 主题《周末の守候》采用Dreamweaver软件制作&#xff0c;主题包含了12页&#xff0c;页面能够相互跳转&#xff0c;运用了HTML5标签&#xff0c;DIVCSS布局&#xff0c;网站主题鲜明、内容丰富、健康、高雅且栏目设置合理&#xff0c;网站中页面色彩搭配合理&…

#低码系列#如何设计一个低代码平台?

低码系列文章 #低码系列#低代码来了&#xff0c;程序员会失业吗&#xff1f; 整体设计 用户群体 对于基础功能的实现&#xff0c;不需要开发人员介入。业务人员通过可视化页面&#xff0c;即可完成设计。从这个角度上看&#xff0c;低码平台面向的用户是业务人员、系统管理…

浅谈代码结构的设计

本文来自网易云社区 作者:陆秋炜 引言 :很久之前,在做中间件测试的时候,看到开发人员写的代码,有人的代码,看起来总是特别舒服,但有的开发代码,虽然逻辑上没有什么问题,但总给人感觉特别难受。后来成为了一位专职开发人员,渐渐发现,自己的代码也是属于“比较难受”…

领域驱动实践总结(基本理论总结与分析V+架构分析与代码设计+具体应用设计分析)

目录 领域驱动实践总结一:基本理论总结与分析 一、领域驱动设计两大设计:战略设计和战术设计 &#xff08;一&#xff09;战略设计 1.出发角度与目标 2.实现方式&#xff1a;事件风暴与模型确立&#xff08;用例分析、场景分析和用户旅程分析&#xff09; 3.用三步来划定…

如何设计一个低代码平台

编者按&#xff1a;近些年来&#xff0c;低代码发展火热&#xff0c;各种低代码平台如雨后春笋纷纷崛起&#xff0c;这些平台各定位不同&#xff0c;优劣不同&#xff0c;用户的选择空间很大。那么&#xff0c;如果用户想从零开始设计一个低代码平台&#xff0c;该如何做呢&…

QT纯代码设计UI界面Demo

目录 一、前言 二、界面 三、源码简析 四、Demo/源码 一、前言 UI的设计方法有几种&#xff1a; ①一种是使用Qt Designer&#xff0c;也就是可视化设计&#xff0c;这在小型项目中常见&#xff0c;优点就是可观简便&#xff1b; ②另一种就是纯代码设计UI&#xff0c;也…

Verilog RTL 代码设计新手上路

1. 做一个4选1的mux&#xff0c;并且进行波形仿真 和2选1的mux对比&#xff0c;观察资源消耗的变化&#xff1a; 实验分析&#xff1a;4选1的mux实际上就是在2选1的mux上进行拓展&#xff0c;选用2位的控制信号控制4位输入信号的选择输出 实验代码设计如下&#xff1a; …

代码设计流程

一、需求分析 1、需求分析的三层境界&#xff1a;实现者、分析者、引导者。 2、在需求中提取到合适的用例&#xff08;以抽卡系统为例&#xff09; 3、用例分析法 5W1H分析法 对上面的“抽卡”用例进行分析如下 5W内容What抽取卡牌Who玩家When游戏服务器开启期间Where抽卡…

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

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

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

二维傅里叶变换频谱图的含义 在一维傅里叶变换得到的频谱图中&#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)为转换后的矩阵&…