【Java设计模式 经典设计原则】 八 经典设计原则小结

简介: 【Java设计模式 经典设计原则】 八 经典设计原则小结

说起来设计原则的第一篇是在6月份,之后7、8月因为工作比较忙基本都没怎么学习,9月份才又赶上来了,这段时间基于极客时间的《设计模式之美》重新学习了下经典设计原则,收获认知提升颇多。这里对这一阶段的经典设计原则的学习和重点进行一个小结。

经典设计原则博客目录

话不多说,对这段时间学习的七篇设计原则进行总结

这其中涉及到不少知识点,总体总结在这里:

左侧的5 个设计原则组成的SOLID,它们分别是:SRP单一职责原则(the single responsibility principle )、OCP开闭原则(the open closed principle)、LSP里氏替换原则(the liskov substitution principle)、ISP接口隔离原则(the interface segregation principle)、DIP依赖反转原则(the dependency inversion principle)

经典设计原则重点内容

接下来回顾一下这些设计原则学习过程中的重点

SRP单一职责原则

以下内容为我认为认知得到加深的内容:

SRP单一职责原则的定义是什么

一个类或者模块只负责完成一个职责(或者功能),也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类

SRP原则解决什么问题

单一职责原则的目标是设计粒度小、功能单一的类,从类自身功能角度出发考虑。通过重构类的功能实现代码高内聚(功能相关高内聚)、促进代码低耦合(功能无关低耦合),提高代码的可复用性、可读性、可维护性

如何实现SRP原则

主观上依据当前场景合理拆分,开始设计要尽量避免过度设计,因为根本不知道业务未来会发展成什么样,能做的最好只是提前预留好扩展点,静候业务发展然后实时做出调整,可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构

客观上可以参照如下几个标准,下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:

  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分; 从另一个角度来看,当一个类的代码,可读性非常差,实现某个功能时不知道该用哪个函数,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函数、属性过多了
  • 类依赖的其他类过多,或者依赖类的其他类过多(被其它类使用),不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

有了主观重构意识再加上客观条件判断能帮助我们写出符合SRP的代码

OCP开闭原则

以下内容为我认为认知得到加深的内容:

OCP开闭原则的定义是什么

软件实体(模块、类、方法等)应该对扩展开放、对修改关闭。详细表述就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)

对于方法的扩展是对于类的修改,这样不满足OCP么

  • 同样一个代码改动,在粗代码粒度下,被认定为修改,在细代码粒度下,又可以被认定为扩展。改动一添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为修改;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。那么该如何衡量呢?抓住一个原则就可以了:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,它就符合OCP

OCP规定代码一行都不能改么

  • 添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则

写代码越OCP越好么

  • 写OCP的代码成本比非OCP的高,逻辑也更负责,读起来更费劲,有些时候代码的扩展性会跟可读性相冲突对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求

OCP原则解决什么问题

OCP的目标就是提高代码的可扩展性,23种设计模式大多数也是这个目标,所以说OCP非常重要

如何实现OCP原则

主观上要具备扩展意识、抽象意识、封装意识 ,具体的方法论则是:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)

LSP里氏替换原则

以下内容为我认为认知得到加深的内容:

LSP里氏替换原则的定义是什么

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏

LSP原则解决什么问题

用来指导继承关系中子类该如何设计 ,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性,保证父子关系更加稳定健壮,通常用于指导框架版本向后兼容增强而不改变原有功能的设计。搭配【基于接口而非实现编程】使用效果最佳

如何实现LSP原则

子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系

ISP接口隔离原则

以下内容为我认为认知得到加深的内容:

ISP接口隔离原则的定义是什么

客户端不应该被强迫依赖它不需要的接口。其中的客户端,可以理解为接口的调用者或者使用者,这里的接口不仅指Java里的接口类,实际上它有三种含义: 一组 API 接口或方法集合单个 API 接口或方法OOP 中的接口概念

SRP和ISP都是降低代码依赖耦合,区别是什么

  • 通过拆分方法让代码粒度变细的方式,ISP和SRP有点类似,不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。也就是说ISP的判定更为主观一些

ISP原则解决什么问题

解决代码的无用依赖问题,指导接口的设计,让接口的功能更内聚单一,让调用方和接口直接的耦合度降低,提升接口的可复用性、可读性、可维护性

如何实现ISP原则

  • 接口是【一组 API 接口或方法集合】:调用者的角度出发:如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口
  • 接口是【单个 API 接口或方法】:调用者的角度出发:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现
  • 接口是【OOP 中的接口概念】:使用者和调用者的角度出发:那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数

DIP依赖反转原则

以下内容为我认为认知得到加深的内容:

DIP依赖反转原则的定义是什么

高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions),所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层

所谓反转并不是指反过来低层要依赖高层,而是二者都依赖抽象,这里的反转指的是原本高层依赖低层,在DIP下实际上是规范低层次模块的设计。低层次模块提供的接口要足够的抽象、通用,在设计时需要考虑高层次模块的使用种类和场景。明明是高层次模块要使用低层次模块,对低层次模块有依赖性。现在反而低层次模块需要根据高层次模块来设计,出现了「倒置」的显现

DIP原则解决什么问题

DIP用来指导框架设计,框架是高层,我们写的代码是低层,我们要按照通用的约定实现自己的底层代码,以满足高层的调用需求。例如:

  • JVM是高层模块,我们编写的Java代码是底层模块。JVM和Java代码没有直接的依赖关系,两者都依赖同一个抽象,也就是class字节码。字节码不依赖具体的JVM虚拟机和Java语言,而JVM虚拟机和Java依赖字节码规范。 这样做的好处就是JVM与Java高度解耦,Java语言可以替换成Groovy、Kotlin,只要能编译为字节码,符合虚拟机的执行规范
  • Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范

如何实现DIP原则

如果我们编写框架,就得让框架依赖的抽象规范明确,基于抽象而非实现编程;如果我们编写实现代码,要想让框架或者容器能调用我们的实现方法实现功能,我们就得依赖满足高层运行功能抽象规范去写自己的实现,也就是按照约定编程。

KISS保持简单原则

以下内容为我认为认知得到加深的内容:

KISS保持简单原则的定义是什么

KISS原则:Keep It Simple and Stupid。翻译成中文就是:尽量保持简单,这里的简单并不是代码行数越少就越简单,还要考虑逻辑复杂度、实现难度、代码的可读性,还有一点就是本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则

KISS原则解决什么问题

KISS 原则是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。

如何实现KISS原则

KISS原则其实本来就比较主观,不过还是有一些验证方式的:

  1. 不要使用同事可能不懂的技术来实现代码。例如复杂的正则表达式,还有一些编程语言中过于高级的语法等。如果想用,培训一下大家
  2. 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
  3. 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

主观验证的方式就是代码CR,让大家看看是否满足。

YAGNI勿过度设计原则

以下内容为我认为认知得到加深的内容:

YAGNI勿过度设计原则的定义是什么

不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计,当然这并不意味着我们不要预留代码扩展点

YAGNI原则解决什么问题

防止项目中代码冗余,防止降低代码的可读性和可维护性

如何实现YAGNI原则

感觉未来要实现的地方,预留扩展点,先不要实现。

DRY勿重复编码原则

以下内容为我认为认知得到加深的内容:

DRY勿重复编码原则的定义是什么

就是字面意思:不要写重复的代码。但这里的重复有不同的含义,包括:实现逻辑重复(代码写重复了)、功能语义重复(方法的用处相同)、代码逻辑重复(方法内代码块重复执行)

DRY原则可以提升代码的复用性么?

  • 代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码。代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用,DRY原则是一条原则要保证的是不要写重复的代码,但并不意味着DRY就提高了代码的可复用性

如何提高代码复用性

  1. 减少代码耦合,对于高度耦合的代码,当希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,要尽量减少代码耦合。
  2. 满足单一职责原则,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。
  3. 模块化,模块不单单指一组类构成的模块,还可以理解为单个类、函数。要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。
  4. 业务与非业务逻辑分离,越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,需要将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。
  5. 通用代码下沉,从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,通常只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。
  6. 继承、多态、抽象、封装,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。
  7. 应用模板等设计模式,一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。

DRY原则解决什么问题

减少项目中的重复功能代码,提高代码的可读性和可维护性

如何实现DRY原则

实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则,可以通过更细粒度的SRP改造来去除重复代码。实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。除此之外,代码执行重复也算是违反 DRY 原则,后边这两种情况需要通过重构去除重复功能方法、逻辑代码块以及重构来满足DRY

LOD迪米特最小知道法则

以下内容为我认为认知得到加深的内容:

LOD迪米特最小知道法则的定义是什么

每个模块(unit)只应该了解那些与它关系密切的模块的有限知识(knowledge)也即每个模块只和自己的朋友说话,不和陌生人说话,不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)

LOD迪米特法则解决什么问题

让代码尽量高内聚,低耦合,重点侧重低耦合,设计类间关系时要多加小心,防止过度依赖。目的也是提高代码的可读性和可维护性

如何实现LOD法则

  • 不该有直接依赖关系的类之间,不要有依赖。可以通过调整方法,例如依赖的类有5个属性只用到其中2个,那么方法不要传入整个依赖类,而是把这两个属性提出来当做参数
  • 有依赖关系的类之间,尽量只依赖必要的接口。可以通过ISP隔离接口,然后通过基于接口而非实现的方式将依赖注入,这样就能只依赖必要接口了。

经典设计原则横向对比

这里对所有涉及到的原则和法则做一个横向总结,这里的接口指OOP中的接口或抽象类。

设计原则 应用范围 看待视角 解决什么问题
SRP单一职责原则 模块、类(接口)、方法 实体自身视角 提高代码的内聚性,可复用性、可读性、可维护性
OCP开闭原则 模块、类(接口)、方法 实体自身视角 提高代码的可扩展
LSP里氏替换原则 类(接口) 父子间关系视角 指导子类设计、继承设计
ISP接口隔离原则 接口 实体间关系视角 降低调用者或使用者依赖,提升接口的可复用性、可读性、可维护性
DIP依赖反转原则 模块 框架设计视角 指导框架设计
KISS保持简单原则 方法 全局视角 提高代码的简洁性、可读性、可维护性
YAGNI勿过度设计原则 类(接口)、方法 全局视角 不过度设计只预留扩展防止降低代码的可读性和可维护性
DRY勿重复编码原则 类(接口)、方法 全局视角 去除重复代码 防止降低代码的可读性和可维护性
LOD迪米特法则 类(接口) 实体间关系视角 防止类间过度依赖,降低代码的耦合性,提高代码的可读性和可维护性

总结一下

其实无论是SOLID、还是KISS、YAGNI、DRY以及LOD,都是服务于写出高质量代码。简单而言就是写出:可维护性、可读性、可扩展性、灵活性、简洁性(简单、复杂)、可复用性、可测试性强的高质量代码。可读性、可维护性的一个基本设计就是写出高内聚、低耦合的代码,其中SRP服务于高内聚,ISP和LOD服务于低耦合。毋庸置疑OCP服务于代码的扩展性,LSP和DIP场景就比较具体了:LSP用于指导父子关系设计,DIP用于指导框架设计。而KISS、YAGNI以及DRY都是从代码的整体视角去告诉我们写代码要尽量简单明了,不要写现在没用或重复的代码,这样代码的可读性和可维护性才高。

相关文章
|
2月前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
48 0
[Java]23种设计模式
|
26天前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
2月前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
3月前
|
存储 设计模式 安全
Java设计模式-备忘录模式(23)
Java设计模式-备忘录模式(23)
|
3月前
|
设计模式 存储 算法
Java设计模式-命令模式(16)
Java设计模式-命令模式(16)
|
3月前
|
设计模式 存储 缓存
Java设计模式 - 解释器模式(24)
Java设计模式 - 解释器模式(24)
|
3月前
|
设计模式 安全 Java
Java设计模式-迭代器模式(21)
Java设计模式-迭代器模式(21)
|
3月前
|
设计模式 缓存 监控
Java设计模式-责任链模式(17)
Java设计模式-责任链模式(17)
|
3月前
|
设计模式 运维 算法
Java设计模式-策略模式(15)
Java设计模式-策略模式(15)
|
3月前
|
设计模式 算法 Java
Java设计模式-模板方法模式(14)
Java设计模式-模板方法模式(14)