2023-02:设计模式

season-qd / 2023-08-31 / 原文

设计模式相关的代码已经提交到gitee中,可以进行下载,建议比照着源码,因为文字描述可能存在信息传递失真的情况发生,看文字不了解的地方可能一看源码就了解了,下载地址:https://gitee.com/season-gitee/design-pattern

设计模式已经初步编写完成,其实跟网上的教程差不多,但是这里也有我自己的思想和理解。主要是思想,推荐开一下原版的设计模式,毕竟那是最正宗纯粹的。设计模式主要使用了多态和继承,穿插着封装,很多模式使用上差不多,只是语义上的区别。建议温故而知新,每隔一个月或者一短时间看一下,每次查阅,每次都有新的发现。

我这里的代码不是最完美,但都是自己手敲的,自己认为正确可用的实例,在认知模糊的时候,建议看一下菜鸟教程和马士兵的视频(建议倍速)

设计模式的指导思想

  1. 可维护性(代码写完后,修改时,需要修改的代码少)
  2. 可复用性
  3. 可扩展性(在扩展时,需要修改的代码少,甚至没有)
  4. 灵活性

设计模式六大原则

  1. 单一职责
    1. 职责分工明确,比如一个汽车工厂中的所有的类都是跟汽车相关的,但是不能把这一个类变得很大,需要进行拆分,使得每个类都具有单一的职责
    2. 达成高内聚低耦合
  2. 开闭原则
    1. 多扩展开放,对修改关闭
    2. 多使用聚合,少使用继承
  3. 依赖倒置
    1. 依赖抽象,而不是具体,面向接口/抽象编程
    2. 具体实现,可以进行替换 Animal a = new Dog();
  4. 里氏替换原则
    1. 所有使用父类的地方,必须透明使用子类
    2. 不能改变父类的语义。比如说父类的eat()方法是自己进行吃饭,但是子类修改了相关的逻辑,变成了喂别人吃饭,这就是语义修改了
  5. 接口隔离原则
    1. 每一接口都应该承担独立的角色,不做该干的事情(一个接口继承的接口不应有不相干的、过多的需要实现的方法,过多的标准,会使得这个接口需要干的事情变多,让实现者变得很累。eg.水晶头除了需要RJ45,还需要USB3.0、TypeC等过多的接口规范,那么水晶头的制作成本上升,但是水晶头不需要这么多的功能)
    2. 不对客户暴漏不需要的数据
  6. 迪米特原则
    1. 降低类之间的耦合。由于每个类尽量减少对其他类的依赖,不和陌生人说话(降低类之间的耦合度,mediator、facade)
    2. 不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。

创建型模式,是用来创建新的对象的

  1. 单例模式(singleton)
    1. 单例模式,是用来保证只创建一个对象,增加了效率
    2. 需要将构造方法私有,提供 static getInstance()方法用来获取单例
    3. spring默认单例
    4. 编写方式
      1. 懒汉式:private static Singleton instance = new Singleton(),java虚拟机保证了在类加载过程中静态变量只初始化一次,但是不管用不用,单例都生成,造成资源浪费
      2. 饿汉式:用的时候才进行初始化(不用不会初始化,很懒)。增加判断,然后为了保证线程安全,加 synchronized;为了保证效率,外面添加if判断,通过instance是否为空进行判断需要生成实例,volatile保证内存可见且不可重排
      3. 内部类饿汉式:创建见静态内部类,实例是内部类的静态属性,只有在第一次调用时创建,较为完美
      4. 枚举:jvm规范保证了实例是单例,不可被反序列化(enum没构造方法)
  2. 构建者模式(builder)
    1. 用于分离复杂对象的构建和表示
    2. effective java,推荐使用builder模式
    3. 将复杂对象的构建,进行拆分,并且可设定默认值,这样子确保了对象在build的过程中,不会漏掉某些属性
    4. 将构造方法私有,提供build类用于创建对象,有默认值,避免了new漏掉某些属性
    5. 编写方式:Person p = Person.Builder().buildBasic(姓名,年龄,身高).buildLoc(城市名,经度,纬度).build();
  3. 原型模式(prototype)
    1. 也被叫为克隆模式,主要是用到了java的clone()方法,重载Object的clone()方法,直接super.clone()就行
    2. clone()的方法需要 implements cloneable,不然会报错。
    3. clone(),是native方法,拷贝的是地址,所以属性中的对象指向同一个引用,是浅拷贝
    4. 深拷贝,只需要将属性中的对象也调用clone()就行
    5. 对于String不需要clone(),因为String值发生修改时,改变的是对于字符串常量池的地址
  4. 工厂系列(简单工厂、静态工厂)
    1. 任何生产对象的方法或者类,都是工厂(单例也是工厂)
    2. 工厂模式,是用来创建一系列对象的工厂,相比于new,好处多多:
      1. 将创建对象的过程放在工厂中,如果需要修改,只需要在工厂中修改
      2. 灵活控制生产过程,可以在工厂中添加权限、修饰、日志等
      3. 工厂保证,生产的每一个对象,都进行相同操作
    3. 静态工厂是静态生成对象的,但是我不理解
    4. spring的ioc,是工厂模式,通过BeanFactory创建Bean,不止是new,还需要各种处理,比如说创建代理,用于事务相关、aop切面处理
  5. 抽象工厂
    1. 创建统一的抽象工厂,然后编写继承于抽象工厂的具体工厂
    2. 可以定义产品族,好处是灵活扩展产品一族,比如一键切换皮肤、风格
  6. 工厂方法(按照正常逻辑,应该先工厂方法,然后抽象工厂)
    1. 只生产一种产品的工厂,也就是工厂方法
    2. 方便产品扩展
  7. 总结:
    1. 原型模式,又被成为克隆模式,通过已知对象,进行克隆复制
    2. 单例、工厂、构建者,都是从无到有,单例、构建者,也可以看成工厂模式
    3. 构建者将复杂的创建过程,进行了拆分,分步骤构建
    4. 单例,通过构造方法私有,保证了只有唯一的实例,节省了资源、避免了资源同步问题
    5. 工厂模式,通过工厂创建对象,保证了生产过程的统一,解耦了对象的创建过程,对代码修改关闭,扩展开放,这样创建新的产品,只需要增加对应的工厂类即可

结构型模式,将现有类或对象组合在一起形成更强大的结构

  1. 装饰器模式(Detector)
    1. 动态地给一个对象添加一些额外的职责
    2. 在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为
    3. 举例:女孩子加装饰:头花、口红、小裙子、高跟鞋,不断增加装饰来装饰自己,此时不能不断继承且不能多继承,通过聚合代替继承,将不同装饰间的耦合度降低
    4. 这跟composite、flyweight,差别不大
    5. Java IO,就使用了装饰器模式io装饰器模式
  2. 组合模式(Composite)
    1. 主要用在树结构,比如二叉树(都是节点)、文件树(目录/文件)
    2. 将部分、整体组合在一起,使得用户对单个对象和组合对象的使用具有一致性。
    3. 举例:文件树,有一个isDirectory()方法,可以判断是否是目录,其他一样
  3. 享元模式(flyweight)
    1. 重复利用对象,用于减少创建对象的数量,以减少内存占用和提高性能
    2. GOF中介绍:word中有一个A(5号、黑体),每次使用都创建对象,太浪费性能。创建一个共享池,需要的时候只需要一个指针引用即可,不需要重复new了。其实,这与单例、spring单例差不多,都是不需要重复new对象
    3. 复杂一些:不同对象间可以相互组合,整体是不同部分组成的,这也像composite,但是更乱一些,会像一张图
    4. 整体思想:创建缓存池,有就直接用,没有就创建
    5. eg.缓存池、字符串常量池
  4. 代理模式(proxy)
    1. 为其他对象提供一种代理以控制对这个对象的访问
    2. 在不影响被代理对象的前提下,增加其他的逻辑,这符合开闭原则
    3. 为特定对象进行代理被称为静态代理,为所有对象进行代理,不需要专门写代码,被称为动态代理
    4. 举例:需要记录机器运行前后数据
      1. 最原始:在运行代码前后添加日志记录运行数据
      2. 进一步:创建被代理对象的继承类/子类,通过子类修改逻辑,但是违背了多聚合少继承
      3. 用代理:创建代理类,生成代理对象,在代理对象中对数据进行处理
      4. 多个代理相互调用,V2的继承方案无法实现多代理
      5. 因为需要为机器单独编写代理类的代码,改用动态代理
    5. jdk Proxy.newProxyInstance()方法
      1. 只支持单代理(无法套娃,因为方法是final,无法被修改)
      2. 并且被代理对象必须实现接口,通过接口调用(依赖导致)
      3. 代理类实现了接口的所有方法,且为final
      4. 所有方法,都调用了InvocationHandler的invoke(this, m2, args)方法(通过反编译代理类可看到)
    6. cglib code generate library
      1. 不需实现接口,基于继承(对于final的类,无法生成代理,当然asm可以去掉final)
      2. 依旧是单代理的
      3. 生成的方法是final的
      4. MehtodInterceptor的角色与Invocationhandler,一致
      5. 被代理的方法是,被代理类上的所有非final的方法
    7. 代理是基于asm实现的,asm能够以二进制形式修改已有类或者动态生成类
  5. 门面模式(facade)(或外观模式)
    1. 创建一个大管家,内部逻辑统一封装,将彼此间复杂关系统一处理
    2. 对外暴漏大管家,将内部逻辑统一处理,对调用者透明
    3. 举例:饭店下订单,需要服务员、初始、洗碗工共同配合,但是对外只暴露一个接口
  6. 桥接模式(bridge)
    1. 将抽象与具体进行分离(详见代码)
    2. 用聚合的方式(桥接),连接抽象与具体
  7. 适配器模式(adapter、wrapper)
    1. 接口转换器一样,最常见的是IO流的处理、JDBC-ODBC,海外电子设备需要一个转换器将220V转换为110V
    2. 过多的使用转换器对系统来说是灾难,只是用于紧急情况,系统设计的初衷应该是统一健壮,规范一样何来转换器
  8. 总结
    1. 不同的设计模式,只是语义上有区别,实际上很多都是多态
    2. asm,可以动态修改二进制代码,但是jvm层面的,所以门槛较高

行为型模式,关注模块之间的通信,用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配

  1. 访问者模式(Visitor)
    1. 在结构不变的情况下,动态改变对于内部元素的动作,即将权限交给访问者,而不是结构
    2. 用于可被访问的部件固定,而访问者很多,就像DI,控制反转
    3. 用到的地方不多,主要是在编译器上解析语法树的时候使用
    4. 示例:模拟装机店,不同的顾客不同需求,不同的需求对应的配置不一样,但是计算机配件都是那几种,不会变
  2. 解释器模式(Interpreter)
    1. 不常用,略过
  3. 迭代器模式(iterator)
    1. Collections的特有模式,用来对数据进行遍历
    2. 增强型for循环,本质是迭代器模式
    3. Collection类实现Iterable接口,public interface Collection<E> extends Iterable<E>
    4. Iterable主要方法:Iterator<T> iterator();
    5. Iterator主要方法为:
      1. boolean hasNext();
      2. E next();
      3. void remove();
  4. 模板方法模式(templateMethod),也被称为钩子函数
    1. 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中
    2. 比如说代理,只需要编写InvocationHandler的invoke()方法,其他的在Proxy中定义好了
    3. 示例:父类中大部分方法(框架)已经写好,只修改部分方法(动态适配)
    4. 示例:人类吃的方法都是一样的,咀嚼、消化、排泄,都是一样的,对于不同的人种,食物的种类、是否使用筷子,都是不一样的,这时可以用模板方法
  5. 策略模式(strategy)
    1. 定义一系列算法,把它们一个个封装起来,并且使它们可相互替换,他们被称为策略
    2. 封装间调用,取代了if-else,新的策略增加,不需要改变内部的逻辑
    3. Comparator是策略模式,使得排序方式灵活可控,但也是模板方法
    4. 主要用来解决,在有多种算法相似的情况下,使用if-else所带来的复杂和难以维护
  6. 观察者模式(observer)
    1. 定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。
    2. Observer、listener、hook、callback,均语义一致
    3. 举例:家中有小宝宝,爸妈都需要观察着他,当他哭了,妈妈需要喂奶,爸爸需要逗她乐
    4. jdk中提供了Observer模式:Observer是观察者角色,Observable是被观察目标(subject)角色。
    5. 版本变化:
      1. 在一段代码中添加判断,当发生变化了,执行相应的逻辑,这样新增观察的逻辑,代码变多、复杂,耦合高
      2. 将所有观察者抽离出来,将自己注册到被观察者那里,当被观察者数据发生变化的时候,通知所有的观察者
      3. jdk版本
    6. 要素:
      1. Observable,被观察者,也就是事件源
      2. Observer,观察者
      3. 马步兵的视频中说,需要一个event,事件类,记录事件信息
  7. 责任链模式(chain of responsibility、filter)
    1. 为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。
    2. 就像罐头厂,制作罐头的过程,需要进行,检查、清洗、切片、制作罐头、封装、储备
    3. 演化过程
      1. 在一个方法中编写串行的业务逻辑(未能解构)
      2. 将不同的逻辑,抽离出来到单独的方法、类中,这样可以做到解耦,list也能保证顺序(无法控制过程是否继续)
      3. 将逻辑代码中的list保存filter,修改为一个特定的类FilterChain中,控制过程
      4. 模拟Servlet中的Filter,public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)(通过异常控制过程)(等有时间从头捋一遍)
  8. 命令模式(command)
    1. 别名:transaction
    2. 可以实现这几个功能,但是要与别的模式结合
      1. 宏命令(多个命令组合在一起):composite
      2. 多次undo:chain of responsibility
      3. 事务回滚:memento
    3. 鸽一下 TODO
  9. 状态模式(state)
    1. 有点类似bridge模式,将具体与抽象分离,而状态就是抽象,将控制权交给state,这又有点像访问者模式,visitor负责逻辑,但是bridge模式更像一点吧
    2. 马步兵代码很像visitor,把visitor改成状态就行了
    3. 而菜鸟教程又是另外一种
    4. 不管哪种,用的不多,有机会看看gof的原版,再仔细学习
  10. 调停者模式(mediator)(中介者模式)
    1. 将A->B,解耦成A->mediator->B
    2. 就像一个大管家,管理内部对象之间的关系,外部的就是facade
    3. 样例没写,大名鼎鼎的mq,就是mediator模式
  11. 备忘录模式(memento)
    1. 保存一个对象的某个状态,以便在适当的时候恢复对象
    2. 实例:ctrl+z、游戏存档、IE后退、数据库事务
    3. 可以通过序列化反序列化、记录数据历史、栈来进行解决,马步兵的视频是使用的序列化反序列化来演示的
  12. 总结
    1. 观察者与责任链,长得差不多,责任链中的chain可以控制是否继续,但是所有观察者都有观察下去的权利
    2. 有些设计模式,没有遇到合适的例子,就没写,等遇到合适的例子再补上

设计模式,基本写完、复习完,后面需要补充的时候进行补充。
其实用的最多的是:代理模式、责任链模式、单例模式、模板模式、策略模式、观察者模式、构建者模式、装饰器模式等