设计模式学习相关源码仓库:https://gitee.com/letoco/design

SOLID

SOLID 是 5 个设计原则开头字母的缩写,其本身就有稳定的意思,寓意是遵循 SOLID 原则可以建立稳定、灵活、健壮的系统。

  • S:Single Responsibility Principle,SRP,单一职责原则
  • O:Open Close Principle,OCP,开闭原则
  • L:Liskov Substitution Principle,LSP,里氏替换原则
  • I:Interface Segregation Principle,ISP,接口隔离原则
  • D:Dependency Inversion Principle,DIP,依赖倒置原则

其中,

  • 设计目标:开闭原则、里氏替换原则
  • 设计方法:单一职责原则、接口隔离原则、依赖倒置原则

SRP

SRP要求每个软件模块职责要单一,衡量标准是模块是否只有一个被修改的原因。职责越单一,被修改的原因就越少,模块的内聚性(Cohesion)就越高,被复用的可能性就越大,也更容易被理解。

OCP

软件实体应该对扩展开放,对修改关闭:

  • 对扩展开放:指存在新的需求或变化时,可以对现有代码进行扩展,以适应新的情况
  • 对修改关闭:意味着类一旦设计完成,就可以独立完成工作,而不要对其进行任何修改

在面向对象设计中,我们通常通过继承和多态来实现OCP,即封装不变部分。对于需要变化的部分,通过接口继承实现的方式来实现“开放”。因此,区别面向过程语言和面向对象语言最重要的标志就是看它是否支持多态。

java 设计模式中能够靠近此目标的设计模式:

  • 装饰器模式:在不改变被装饰的对象的情况下,通过包装(wrap)一个新类来扩展其功能
  • 策略模式:通过制定一个策略接口,让不同的策略实现称为可能
  • 适配器模式:在不改变原有类的基础上,让其适配(Adapt)新的功能
  • 观察者模式:可以灵活地添加或删除观察者(Listener)来扩展系统的功能

注意:不修改是有点理想主义的,不要提前做过大的大设计,避免犯YAGNI(You Ain’t Gonna Need It)的错误

LSP

里氏替换原则:程序中的父类都应该可以在不改变程序的正确性的情况下被子类替换
根据该原则,如果在程序中出现了 instanceOf、强制类型转换或函数覆盖,极有可能违背了该原则

ISP

多个特定客户端接口要好于一个宽泛用途的接口
即接口隔离原则认为不能强迫用户去依赖那些他们不使用的接口,换言之,使用多个专门的接口比使用单一的总接口更好
示例:

  • 不遵循ISP原则

save_share_review_picture_1609205041.jpeg

  • 遵循ISP原则

image.png
同样,在做接口拆分时,也要尽量满足单一职责原则,让一个接口的职责尽量单一,可以降低耦合

DIP

模块之间交互应该依赖抽象,而非实现,即面向接口编程

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象

案例说明:Java 中的日志框架
在 Java 应用中使用 Logger 框架有很多选择,比如 log4j、logback、common logging 等。每个 Logger 的 API 和用法都稍有不同,有的需要用 isLoggable()来进行预判断,以便提高性能,有的则不需要。如果要切换不同的 Logger 框架,会非常复杂,可能要改动很多地方。产生这些问题的原因是我们直接依赖了 Logger 框架,应用和 Logger 框架强耦合在一起了,如下图:
image.png
导致这种复杂依赖的根源,是引用直接依赖 Logger 框架,导致后续的 Logger 框架升级必须保持向后兼容,然后越来越复杂
image.png
解决:遵循依赖倒置原则,即反转依赖的方向,让原来紧耦合的依赖关系得以解耦,这样依赖方和被依赖方都有很高的自由度
image.png
因此,在业务系统中不要直接使用日志实现,而应该使用日志门面。

合成复用原则

合成复用原则:Composite/Aggregate Reuse Principle,CARP

  • 说明:指尽量使用对象组合(has-a)或对象聚合(contanis-a)的方式实现代码复用,而不是用继承关系达到代码复用的目的
  • 优点:合成复用原则可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较小

继承和组合/聚合的区别:

  • 继承:又称白箱复用,相当于把所有实现细节暴露给子类
  • 组合、聚合:又称黑箱复用,对类以外的对象是无法获取到其设计细节的

合成复用原则的使用案例:以操作数据库为例

  1. // 1.获得数据库连接
  2. public class DbConnection {
  3. public String getConnection(){
  4. return "MySQL数据库连接";
  5. }
  6. }
  7. // 2.创建ProductDao类
  8. public class ProductDao {
  9. private DbConnection dbConnection;
  10. public void setDbConnection(DbConnection dbConnection){
  11. this.dbConnection = dbConnection;
  12. }
  13. public void addProduct(){
  14. String conn = dbConnection.getConnection();
  15. System.out.println("使用" + conn + "增加产品")
  16. }
  17. }

这是一种典型的合成复用原则应用场景,但是,对于目前的设计来说,DbConnection 还不是一种抽象,不利于系统扩展,只需要将 DbConnection 中 getConnection 修改成 abstract,然后进行

  1. public abstract class DbConnection {
  2. public abstract String getConnection();
  3. }
  4. public class OracleConnection extends DbConnection {
  5. public String getConnection(){
  6. return "Oracle数据库连接";
  7. }
  8. }
  9. public class MySqlConnection extends DbConnection {
  10. public String getConnection(){
  11. return "MySQL 数据库连接";
  12. }
  13. }

软件设计三原则

1、DRY:Don’t Repeat Yourself 的缩写,指在程序设计和计算中避免重复代码

  • 系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能
  • DRY 原则的优势:避免一个地方的改动,会牵扯到多处修改

2、YAGNI:You Ain’t Gonna Need It的缩写,意为你不会需要它,即避免大设计,指除了核心功能之外,其他的功能不要提前设计,这样可以加快软件的开发

3、Ruler of Three:三次原则,由于 DRY 和 YAGNI 原则存在冲突,于是做了折中,在代码冗余和开发成本之间做了折中

  • 第一次用到某个功能时,写一个特定的解决方法
  • 第二次又用到的时候,复制上一次的代码
  • 第三次出现的时候,才着手“抽象化”,写出通用的解决方法