logo头像

分享技术,品味人生

架构师-GOF设计模式

GOF,Gang of Four 设计大师四人组,90年代出版了设计模式的书籍《设计模式:可复用面向对象软件的基础》

设计模式是对软件设计反复出现的各种问题提出的解决方案,如问题反复出现三次就应该重构了!主要解决可扩展性!

img

image-20220823142346138

设计原则:

七大原则是日常开发应遵守的原则,也是设计模式研究的依据

  1. 单一职责,一个类只处理一种职责,一个方法也是。
  2. 接口隔离,类的依赖应该建立在最小的接口上,无关的设计不应该出现,可设置多个接口
  3. 依赖倒转,面向接口编程,高层抽象不依赖底层实现
  4. 里氏替换原则,子类不应该破坏父类的实现代码,而能做到无缝替换
  5. 开闭原则,扩展开放,修改关闭,也就是做到功能或需求修改时,只修改供方、拓展供方即可(核心,也是接口隔离的一种表现)
  6. 迪米特原则,最少原则暴露,高内聚,类中不应该出现陌生临时对象!
  7. 合成复用原则,尽量能用合成、聚合的地方就不要用继承。

1、单一原则

一个类只处理一种职责,这样易于扩展、阅读,后续修改也不会连锁反应。

  • 代码量很少的情况用方法1
  • 方法很少的情况下允许用方法3

image-20220729160024152

2、接口隔离原则

类的依赖应该建立在最小的接口上,如果接口较多,应该拆解。

image-20220729164914259

注意:下方Class1和2的形参从原来的1变成了2、3

image-20220729164806427

3、依赖倒转

核心思想是面向接口编程,高层双方以抽象方式(接口)交互,底层依赖高层,制定出了规范,有了缓冲

实现方式,高层双方接口调用,接口的传递可以用方法构造器set方法,三种方式。

image-20220803111036726

4、里氏替换原则

对继承的规范和契约,要求子类不对父类已实现的方法进行修改破坏,如果确实需要修改,建议做新的方法!这样父类修改可以不考虑子类的影响面!

简单地说,引用基类的地方必须能透明的使用子类的对象。

image-20220803112421865

5、开闭原则

编程中最基础、最重要的原则,对提供方的扩展(增加方法)开放,对使用方的修改()关闭

当软件发生变化时,通过扩展而不是修改来达到目的,也就是说仅修改供方一边的代码即可完成功能!也是从依赖倒转的另一个角度来解读。

image-20220803114103482

6、迪米特原则

最少知道原则,使用方尽量对依赖类知道的越少越好,不要出现陌生对象,仅与直接朋友有关系就好!

直接朋友指类中的成员变量,方法的入参、出参。

image-20220803141052542

7、合成复用原则

尽量使用依赖或聚合、合成的方式,而不是使用继承

image-20220803141532551

设计原则的核心思想:

  • 找出应用中变化的部分,独立出来,跟鲜少变动的代码混合在一起
  • 针对接口编程而不是面向实现编程
  • 为了交互对象的松耦合而努力!

设计模式

序号 分类 模式 解决问题 设计方案(实现方法) 应用场景 扩展延伸
1 创建型1 单例模式 只能存在一个对象实例 构造函数私有化,对外提供静态实例 项目中只需要一个实例,如hibernate的sessionfactory作为数据存储源的代理 饿汉式(编译生成)、懒汉式(首次使用初始化)、双重检查(并发优化)、枚举(新特性)
2 创建型2 简单工厂 大量创造某种对象 定义一个创建对象的类(工厂),由它来实例化对象 单维度重复生成、封装实现细节 静态简单工厂、工厂模式
3 创建型2 工厂模式 大量创造多维需求对象 定义产品接口,由多个不同的子类决定生产什么产品(更多工厂) 多维度重复生成对象 抽象工厂
4 创建型3 抽象工厂模式 多类多维产品族生成代码爆炸 定义抽象类抽象方法,由子类决定生产什么产品(组合工厂) 多类产品族生成、简化工厂类
5 创建型4 原型模式 创建新对象比较复杂,需要实时状态,克隆羊 重写并调用Object.clone或反序列化+序列化来实现 深度拷贝场景,如spring中对bean生命周期的判断
6 创建型5 建造者模式 产品和产品建造过程解耦,指定产品类型和内容就可创建复杂对象 由产品、抽象建造者(流程)、具体建造者、指挥者builder组成 复杂对象创建,按蓝图设计,如stringbuilder及其父类的组合使用
7 结构型1 适配器模式 类型转换 通过中间类进行接口的调用转发、扩展 SpringMVC中DispatchServlet接收请求返回modelAndView时用适配器去找到最应的controller 类适配、对象适配(继承换成聚合)、接口适配
8 结构型2 桥接模式 多维度关系带来类爆炸、违反OCP,如手机款式、品牌的指数级组合 多的维度抽象为接口被原有类实现并通过构造器组合,完成解耦 如DriverManager中聚合了Connection,通过注册、获取来完成业务解耦
9 结构型3 装饰模式 单维、多维组合导致类爆炸,如咖啡单点或咖啡+奶精的组合 装饰者和主体都继承同一个父类,装饰器组合主体完成套娃组合 如FilterInputStream做各种流数据转换 套娃解决了代码爆炸,但不易理解,用对象数组组合,如桥接或再抽象一层会易于理解和维护
10 结构型4 组合模式 单维、多维爆炸又没有实际继承关系 管理关系统一继承,层级关系用组合达成树形结构 如HashMap可以添加节点或叶子
11 结构型5 外观模式 家庭影院、摇滚等成套场景需要 高层接口提供一致化操作,子类实现并被聚合到操作类中 如Mybatis中的Configuration中操作MetaObject,做不同类型判断再聚合
12 结构型6 享元模式 对象复用的池技术 通过操作静态成员来完成池的建立和维护 如Integer对-127~128之间的数值做了缓存
13 结构型7 代理模式 提供增强版的代理对象,如增加权限判断、日志审计等 静态代理(新建代理类)、动态代理/jdk代理(Proxy反射,需实现接口)、cglib代理(第三方jar,通过asm反编译,跳过接口) 外网代理、缓存代理、远程代理、同步代理
14 行为型1 模板方法 完成某个过程要一系列步骤,几乎都相同,仅个别过程有变化,或如架构师和同事的分工协作,做好算法,留钩子 抽象类定义执行方法模板,子类实现具体各个方法。 Spring ioc 容器初始化 跟建造者模式很像,但是在一组父子类中完成
15 行为型2 命令模式 特定场景需求,如遥控器、智能家居app,对批量发送类似指令 定义调用者、接受者、命令对象,其中命令对象是桥梁,起到解耦的作用 JDBCTemplate中execute对CRUD的命令设计,方便任务边界划分,工作量化 设计空命令可以省去很多判断。 还可以做成多线程处理队列
16 行为型3 访问者模式 特殊场景,数据结构稳定,操作易变且无关联性,这时对数据结构和操作进行解耦 被访问类内部提供一个对外接待的接口,双分派,把自己做参数,互相传递两次! 符合单一职责、便于扩展,常用于UI、报表、拦截器、过滤器等设计 破坏了ocp、迪米特法则、依赖倒转等规则,但特定场景使用
17 行为型4 迭代器模式 集合元素种类不同,遍历需要多种方式,还暴露元素内部结构 实现迭代器接口 ArrayList等集合类
18 行为型5 观察者模式 解决消息通知,类似MQ场景的需求 通过注册中心、用户两级,以聚合、继承等形式实现 JDK中的Observable
19 行为型6 中介者模式 网状结构梳理成星型结构 网状改三层,提供中间层(中介)进行解耦 SpringMVC中的Controller
20 行为型7 备忘录模式 需要多快照回滚的场景 增加守护者角色,通过集合、map提供对象缓存(很常规的做法) 事务回滚,回档、重做等操作 跟原型模式组合使用,简单、节省内存
21 行为型8 解释器模式 计算器、解释器、正则表达式等特殊场景 通过分词器、语法分析器构建语法分析树,过程是比较复杂的。 Spring中的SpelExpressionPasser
22 行为型9 状态模式 if/else因状态判断多而爆炸,怎么优雅的扩展;事件有很多状态,状态间可以互转 定义接口,用抽象类异常实现,在多子类中定义状态,然后组装。 缺点:状态类会有很多
23 行为型10 策略模式 if/else因策略判断多而爆炸,怎么优雅的扩展; 定义策略接口,策略子类作为参数传递,实现效果,解耦 JDK的Arrays.sort就可以传递策略对象
24 行为型11 职责链模式 多级请求、审批流、字符流处理器等场景,减少不必要的代码爆炸 组成对象无环图DUG,类似链表的形式进行挨个处理,节点不能太多,不然影响性能 SpringMvc中的HandlerExecutionChain

设计模式是问题总结出的经验,不是代码,而是某类问题的通用解决方案,在代码重构中特别有用!

设计模式的本质是提高软件的维护性、通用性、扩展性,并降低软件的复杂度

《设计模式——可复用面向对象软件的基础》有本经典书,由Gof四人组编写。

设计模式不局限于某种语言,通常面向对象的语言都适用。

设计模式分三种类型:

  • 创建型,解决类的创建问题
    • 单例模式,仅生成1个实例
    • 抽象工厂模式,提供创建产品的接口,子类来生产一系列产品
    • 原型模式,深度拷贝原型
    • 建造者模式
    • 工厂模式,定义创建产品接口,子类决定生产什么产品
  • 结构型,解决类的爆炸性问题
    • 适配器模式
    • 桥接模式
    • 装饰模式
    • 组合模式
    • 外观模式
    • 享元模式
    • 代理模式
  • 行为型,解决类的各种行为问题
    • 模板方法
    • 命令模式
    • 访问者模式
    • 迭代器模式
    • 观察者模式
    • 中介者模式
    • 备忘录模式
    • 解释器模式
    • 状态模式
    • 策略模式
    • 职责链模式

创建型模式(5)

创建型解决了类的创建问题,主要有工厂和建造者模式,单例、原型解决特殊场景需要

  • 工厂系列
    • 简单工厂,就是有个代理人(工厂)来统一对接后端工坊的创建。
    • 工厂模式,就是有代理商直接负责工坊的创建,是一种高效的过渡模式。
    • 抽象工厂,由区域代理调度不同的工厂,解决产品族的创建问题。
  • 建造者模式,就是用蓝图,按顺序去创建,如stringbuilder里面的多个方法顺序执行。
  • 特殊场景
    • 单例模式,通过关闭构造函数,提供静态实现的方法,其中有几种变种。
      • 饿汉模式,直接jvm静态初始化、提供,占用内存,但没有并发风险。
      • 懒汉模式,首次使用时初始化,因有了判断所以并发存在风险,而采用不同锁技术来控制。
        • 还有通过匿名内部类、枚举等新鲜玩法。
    • 原型模式,解决深度克隆的问题,用的object.clone或反序列、序列两种技术。

1、单例模式

限定某个类只能有一个实例,并提供一个获取的静态方法,如Hibernate的SessionFactory,有八种方式:

推荐四种使用:枚举、静态内部类、双重检查、饿汉式(稍微有点内存浪费,无所谓)

使用场景:创建需耗费大量时间、大量资源,且高频重复使用,如数据源、sessionFactory等文件、数据库资源。

  • 饿汉式, JDK1.0的runtime就是用这个了。

    • 静态常量,构造器私有化,类设置静态常量并实例,提供public的静态方法供使用,可用
    • 静态代码块,同上,把new的过程放在静态代码块中,可用
    • 优点:装载就完成实例化,没有线程同步问题
    • 缺点:没有懒加载效果,如果没有使用,那就内存浪费

  • 懒汉式

    • 线程不安全,同饿汉式,但只在提供时判断是否实例化,仅适用于单线程,不推荐
    • 线程安全,同步方法,同上,在提供的方法上加上synchronized 关键字,可用,但锁+每次判断,导致效率低
    • 线程安全,同步代码块,比较诡异的写法,在线程不安全写法之上,在if代码块中对new的过程做同步代码块,如果并发都进入if代码块,这里就没有意义了,**不推荐
  • 双重检查

    • 用volatile+synchronized,并套两层if,对并发所有情况进行控制,推荐!
    • 优点:解决线程安全问题,解决懒加载问题,并解决效率问题!
    • 缺点:编写复杂,首次不易理解

双重检查

  • 静态内部类

    • 效果同双重检查,依赖的是静态内部类延迟加载且由锁的特性,推荐!但不同版本的jvm未验证过。

    image-20220803163549964

  • 枚举

    • 依赖jdk1.5的特性实现,效果还行,Effective Java作者推荐!据说还可防止反序列化再重新创建。

image-20220803164006988

2、简单工厂模式(静态工厂,如果加static修饰)

面对原始需求,我们用面向对象做了三层封装,并对商品做了抽象

image-20220804103628437

但增加商品、供方,或同时增加都会成倍增加复杂度

image-20220804103730967

image-20220804103748369

改进: 用简单工厂对商品和供方进行解耦,无论增加多少商品或供方,均是最小耦合性

image-20220804103817636

小结:商品(对象)的暴增用简单工厂解耦,如果要增加商品系列,则需要对工厂进行抽象

3、工厂方法模式

增加产品系列,对供方进行抽象,让多个供方实例做工厂,也就是供应商兼工厂!这是种过渡方式

image-20220804105920497

4、抽象工厂模式

按产品系列进行工厂的抽象、解耦,适合大型复杂环境。

image-20220804110007786

5、原型模式

解决克隆羊的需求,只要实现cloneable接口、实现clone方法即可。

其中clone有两种方式:

  • 调用object的clone方法,不过类中如果有其他对象需要挨个clone,后续还需要持续修改。
  • 通过序列化、反序列化可一次完成工作,没有后续烦恼。
//通过Object的拷贝
@Override
protected Object clone() throws CloneNotSupportedException {
    FactoryTest clone = (FactoryTest) super.clone();
    clone.friend = (FactoryTest) this.friend.clone();
    return clone;
}

/*通过序列化、反序列化来获取*/
@Override
protected Object clone() throws CloneNotSupportedException {
    ByteArrayOutputStream byteArrayOutputStream = null;
    ObjectInputStream objectInputStream = null;
    ByteArrayInputStream byteArrayInputStream = null;
    ObjectOutputStream objectOutputStream = null;
    try {

        byteArrayOutputStream = new ByteArrayOutputStream();
        objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(this);

        byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        objectInputStream = new ObjectInputStream(byteArrayInputStream);
        return objectInputStream.readObject();
    } catch (IOException e) {
        throw new RuntimeException(e);
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    } finally {
        try {
            byteArrayOutputStream.close();
            objectInputStream.close();
            byteArrayInputStream.close();
            objectOutputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

6、建造者模式

参考对象:StringBuilder

把产品、零件生产、组装进行解耦,比如盖房子,需要打地基、筑墙、盖屋顶等步骤,按一定蓝图来建造,所以有三个主要角色:

  • 产品
  • (抽象)建造者,如果有多个不同的建造者
  • 建造者,完成不同的建造过程
  • 指挥者,组装(蓝图)

StringBuilder是个例子,实际情况就是指挥者也可能充当一部分建造者职能,甚至有其他模式混用。

image-20220804160431156

建造者模式和抽象工厂模式的差异:

  • 抽象工厂解决产品族的创建

  • 建造者模式按蓝图生产零件,然后组装的过程

结构型模式(7)

1、适配器模式

参考对象:SpringMVC的HandlerMappper下研究Dispatcher,根据不同的controller获取对应adapter,然后adapter再处理controller

解决类型转换的问题,通过一个适配器来做中间转换,适配器有三种形式

  • 类适配器

image-20220804162458726

  • 对象适配器,将上图做改进,将继承换成聚合。
  • 接口适配器,用抽象类先全空的形式实现接口,在使用时才针对部分刚需做实际的实现

image-20220804163242824

2、桥接模式

通过桥接类和行为接口,解决因为两维度导致的类爆炸问题,如手机的款式和品牌,导致新增一个系列而爆炸。

主要案例:spring sql驱动器

image-20220821154733515

image-20220821154741111

image-20220821154746802

从上面的图可以看到,品牌被抽离后聚合,两个维度被进行了拆分,任意添加都互不影响。

3、装饰模式

装饰器采用继承+组合的方式,完成套娃!解决单品、多次装饰的类爆炸问题!

主要案例:FilterInputStream

image-20220821161027830

image-20220821161032719

image-20220821161037823

装饰器就像是桥接模式又多了个继承!

4、组合模式

组合模式解决看似继承关系,实质为树形结构的类爆炸问题,通过继承+组合的形式。

技巧:叶子节点不需要的功能在抽象类中提前实现,如抛出个异常。

实例:hashmap

image-20220821163539811

image-20220821163546103

5、外观模式

类似建造者模式的1个指挥者+1个建造者,这里是1个外观搭配多个工具,封装调用细节!

比如新系统调用旧系统的可以用外观模式做一个封装,简化维护!

应用场景:mybatis中configuration中对metaobject

image-20220821165151493

image-20220821165156017

image-20220821170249618

6、享元模式

享元模式,共享对象模式,就是池技术,解决重复对象的内存浪费技术。

应用场景:数据库连接池、字符串池等等

源码:IntegerCache

image-20220821171520357

image-20220821171525772

7、代理模式

代理模式,解决对象增强的需求,如增加安全审计,在aop编程中常见,有三种形式

  • 静态代理,通过代码继承实现,缺点是比较僵硬
  • 动态代理,通过jdk的反射调用做增强
  • cglib代理,第三方jar,通过asm反编译,相比jdk代理不需要实现接口!更灵活,是spring的主要形式。

image-20220821173829769

image-20220821191803490

image-20220821191831738

行为型模式(11)

1、模板方法

类似建造者模式,这里主要关注行为,特别是模板算法流程套装,所以,stringbuilder也是不错的分析例子

通过模板规定算法大致流程,具体实现在子类中去实现

这是很好的架构师工具

image-20220821191843596

2、命令模式

价值:把发起请求和执行请求的对象解耦,发起请求者不需要知道具体执行者是谁

案例:Spring JDBCTemplate的query方法,调用的内部类

这应该也是架构师常用分解工作的好模式,方便定义任务便捷,类也不会爆炸

image-20220821194727672

image-20220821194733242

3、访问者模式

特定场景使用:数据结构不变,甚至不允许做任何修改,但又有很多操作,并且需要被解耦。

举例:UI、报表、拦截器、过滤器等。

image-20220823102012145

image-20220823102018845

image-20220823102024119

image-20220823102029455

image-20220823102034402

image-20220823102039725

注:这里有个特殊名词双分派,就是a调用b,将c作为参数传给b,然后b再将自己传给c。

4、迭代器模式

集合元素种类不同,遍历需要多种方式,还暴露元素内部结构。

通过实现迭代器接口,如ArrayList家族

image-20220823102955465

image-20220823103012869

image-20220823103030178

image-20220823103023344

image-20220823103038280

5、观察者模式

观察者模式目前比较类似的是MQ应用,SpringCloud注册中心的职责,通过提前注册,有消息时及时提醒。

标准是采用接口聚合的形式,jdk的源码中Observable是采用继承的形式完成消息的传递

image-20220823111345818

image-20220823111314411

image-20220823111320199

image-20220823111327114

image-20220823111332230

6、中介者模式

如springmvc中的控制层,对网状结构拆成三层,提供中间层,形成星型结构,达到解耦的效果!

image-20220823112320689

7、备忘录模式

对一个或多个对象,提供多个快照,并支持任意回滚、重做。

如事务回滚,ctrl+z/Y的回滚重做需要。

image-20220823113219199

8、解释器模式

特殊场景使用,如计算器、正则表达式等等,spring中对于注解写法有特有的解释器,如SpelExpressionParser

image-20220823120637985

image-20220823120644534

image-20220823120651086

image-20220823120656793

image-20220823120708156

9、状态模式

当状态特别多、状态还可以互换时,如果用传统if/else来判断,代码肯定爆炸,非常难维护。

通过抽取状态类来简化维护难度,缺点是状态类会很多。

优雅的设计:定义接口,用抽象类异常实现,在多子类中定义状态,然后组装。

image-20220823122448771

image-20220823122455716

image-20220823122501643

10、策略模式

if/else因策略判断多而爆炸,怎么优雅的扩展;事件有很多状态,状态间可以互转

定义策略接口,策略子类作为参数传递,实现效果,解耦

image-20220823123329593

image-20220823123335198

11、职责链模式

多级请求、审批流、字符流处理器等场景,减少不必要的代码爆炸

组成对象无环图DUG,类似链表的形式进行挨个处理,节点不能太多,不然影响性能

image-20220823124534705

image-20220823124526833

image-20220823124542580

image-20220823124549085

image-20220823124557401

image-20220823124603557

小结

设计模式有标准,实际使用可以灵活,往往很多优秀的代码有灵魂但不是完全照搬标准模式!

设计模式难学的原因:

  • 这是偏理论、哲学层米的设计思想,确实门槛比较高
  • 没有具体实操,面向所有语言,所以难有统一的java实现样例
  • 翻译的问题,中文很容易歧义

评论系统未开启,无法评论!