- 1. 设计原则
- 2. 设计模式
- 2.1 创建型
- 2.2 结构型
- 2.3 行为型
- 2.3.1 观察者 Observer Design Pattern 【发布订阅模式(Publish-Subscribe Design Pattern)】
- 2.3.2 模板模式 Template Method Design Pattern
- 2.3.3 策略模式 Strategy Design Pattern
- 2.3.4 责任链 Chain Of Responsibility Design Pattern
- 2.3.5 状态模式
- 2.3.6 迭代器模式 Iterator Design Pattern 游标模式(Cursor Design Pattern)
- 2.3.7 访问者模式 Visitor Design Pattern
- 2.3.8 备忘录模式 Memento Design Pattern 也称快照模式
- 2.3.9 命令模式 Command Design Pattern
- 2.3.10 解释器模式 Interpreter Design Pattern
- 2.3.11 中介模式 Mediator Design Pattern
- 总结
1. 设计原则
1. SOLID
- 单一职责(Single Responsibility Principle,SRP)
- 一个类或者模块只负责完成一个职责(或者功能)
- 单一职责可以使类也可以是模块,可以把模块看做粗颗粒度的代码块
- 例子:
- Spring中BeanFactory(获取bean的基础方法getBean Type Provider等) ParentBeanFactory(获取多Bean的方法)
- Serialization序列化和反序列化类,可以拆分成序列化类和反序列化类
- 开闭原则(Open Closed Principle,OCP)
- 对扩展开放、对修改关闭
- 添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
- 例子:Servlet中的init()、Spring中Bean的生命周期回调、
- 里氏替换原则(Liskov Substitution Principle,LSP)
- 在程序中,子类能够替换父类对象,并能保证程序的逻辑行为正确。
- 接口隔离原则(Interface Segregation Principle,ISP)
- 调用方不应该被强迫依赖它不需要的接口。
- 职责更单一,复用性更好
- 例子:Comparable接口、Serializable接口
依赖倒置(Dependency Inversion Principle,DIP)
Keep It Simple and Stupid.
- Keep It Short and Simple.
Keep It Simple and Straightforward.
1.3 YAGNI:You Ain’t Gonna Need It. (你不会需要它)
-
1.4 DRY :Don’t Repeat Yourself (不要重复你自己)
-
1.5 迪米特法则 LOD (最小知识原则,The Least Knowledge Principle。)
高内聚、松耦合:“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。
- 高内聚:指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
- 松耦合:类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。
- 每个模块只应该了解那些与它关系密切的模块的有限知识。或者说,每个模块只和自己的朋友“说话”,不和陌生人“说话”。
- 不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
2. 设计模式
设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。
2.1 创建型
2.1.1 单例模式
- Java中实现:
- 懒汉式:
- double-check:先判断是否为空,为空就进入同步代码块,然后再判断是否为空,为空就创建。最后返回实例对象
- 直接在方法上加锁:内部判断是否为空,为空就创建,不为空就返回实例对象
- 静态内部类:只有静态内部类被使用的时候才会被加载执行创建操作。
- 饿汉式:直接创建一个static final 的对象
代码示例
类型:简单工厂、工厂方法和抽象工厂
- 简单工厂:工厂类里面用一个静态方法创建对象,然后返回
- 工厂方法:利用多态,接口定义一个创建方法,然后类实现接口,对应不同类创建的过程。
- 工厂方法模式比起简单工厂模式更加符合开闭原则。
- 抽象工厂:一个工厂负责创建多个不同的类
代码示例:
用于创建复杂的对象,用Builder类传递参数,在build的时候可以先进行校然后创建对象。可以实现属性校验和对象分离。
代码示例
利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。有以下两种实现方式:
- 浅拷贝:Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。
- 深拷贝:
- 递归拷贝对象属性的实际值
- 序列化对象,然后反序列化对象
- Java中实现:
2.2 结构型
2.2.1 代理模式 (Wrapper)
- 它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
- 静态代理:需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。
- 动态代理:我们不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
- 常用实现方式:
- 继承(静态代理):通过super调用父类方法实现代理
- 组合(静态代理):通过传入被代理类然后调用对应方法来实现代理
- JDK动态代理(依赖反射):
- Cglib:字节码植入:
代码示例
将抽象和实现解耦,让它们可以独立变化。
- 一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
- 通过组合关系来替代继承关系,避免继承层次的指数级爆炸。
- 桥接就是面向接口编程的集大成者。面向接口编程只是说在系统的某一个功能上将接口和实现解藕,而桥接是详细的分析系统功能,将各个独立的纬度都抽象出来,使用时按需组合。
2.2.3 装饰器 (Wrapper)
- 装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
- 代码示例
- Collections: 通过 unmodifiableColletion() 静态方法,来创建 UnmodifiableCollection 类对象
- Java IO 类库
- Spring应用 Spring 使用到了装饰器模式。TransactionAwareCacheDecorator 增加了对事务的支持,在事务提交、回滚的时候分别对 Cache 的数据进行处理
- Java IO
2.2.4 适配器(Wrapper)
- 实现方式:类适配器和对象适配器。
- 类适配器使用继承关系来实现。接口多并且接口大部分相同
- 对象适配器使用组合关系来实现。接口多并且接口大部分不同
- 使用场景:
- 封装有缺陷的接口设计:对外部系统提供的接口进行二次封装,抽象出更好的接口设计。
- 统一多个类的接口设计:功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义;
- 替换依赖的外部系统:我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动;
- 兼容老版本接口:版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
- 适配不同格式的数据:用在不同格式的数据之间的适配。
- 代码示例
- Collections:Enumeration 类是适配器类,它适配的是客户端代码(使用 Enumeration 类)和新版本 JDK 中新的迭代器 Iterator 类
- Spring MVC: dispatcher从handlerMapping里面拿到Handler 然后执行handler拿到结果返回。Spring 定义了统一的接口 HandlerAdapter,并且对每种 Controller 定义了对应的适配器类。这些适配器类包括:AnnotationMethodHandlerAdapter、SimpleControllerHandlerAdapter、SimpleServletHandlerAdapter 等。
- Slf4j:对不同日志框架(log4j、logback)的接口进行二次封装,适配成统一的 Slf4j 接口定义。 此外 Slf4j 还提供了JCL的反向适配
2.2.5 代理、桥接、装饰器、适配器四者的差异
- 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
- 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
- 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
- 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
2.2.6 门面(外观)模式:Facade Design Pattern
- 门面模式让子系统更加易用
- 业务使用业务场景:
- 解决易用性问题:门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。
- Linux Shell 命令:封装系统调用,提供更加友好、简单的命令
- 解决性能问题:封装关联接口调用,如A->B->C三个接口依次调用,可以在此基础上再封装一个接口,减少网络调用次数,提升性能。
- 解决分布式事务问题:
- 解决易用性问题:门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。
- 与适配器比较:适配器模式注重的是兼容性,而门面模式注重的是易用性
- 适配器和门面模式的区别
- 适配器是做接口转换,解决的是原接口和目标接口不匹配的问题。注重兼容性
- 门面模式做接口整合,解决的是多接口调用带来的问题。注重易用性
2.2.7 组合模式(Composite Design Pattern,组合模式)
- 将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者。)可以统一单个对象和组合对象的处理逻辑。
- 一个示例:“将一组对象(员工和部门)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(部门与子部门的嵌套结构)。组合模式让客户端可以统一单个对象(员工)和组合对象(部门)的处理逻辑(递归遍历)。
代码示例
- Spring中的应用Cache:组合模式主要应用在能表示成树形结构的一组数据上。树中的结点分为叶子节点和中间节点两类。对应到 Spring 源码,EhCacheManager、SimpleCacheManager、NoOpCacheManager、RedisCacheManager 等表示叶子节点,CompositeCacheManager 表示中间节点。叶子节点包含的是它所管理的 Cache 对象,中间节点包含的是其他 CacheManager 管理器,既可以是 CompositeCacheManager,也可以是具体的管理器,比如 EhCacheManager、RedisManager 等。
2.2.7 享元模式
- Spring中的应用Cache:组合模式主要应用在能表示成树形结构的一组数据上。树中的结点分为叶子节点和中间节点两类。对应到 Spring 源码,EhCacheManager、SimpleCacheManager、NoOpCacheManager、RedisCacheManager 等表示叶子节点,CompositeCacheManager 表示中间节点。叶子节点包含的是它所管理的 Cache 对象,中间节点包含的是其他 CacheManager 管理器,既可以是 CompositeCacheManager,也可以是具体的管理器,比如 EhCacheManager、RedisManager 等。
顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
- 当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。
- 享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建好的享元对象,以达到复用的目的。
代码示例
Integer : -128~127 之间的整型对象复用;IntegerCache 相当于,我们上一节课中讲的生成享元对象的工厂类,只不过名字不叫 xxxFactory 而已。
//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255
String:常量字符串复用;字符串常量池,它并非事先创建好需要共享的对象,而是在程序的运行期间,根据需要来创建和缓存字符串常量。
- 单例和享元模式:
- 在单例模式中,一个类只能创建一个对象,
- 在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。
- 对象池、连接池、线程池、享元模式都是为了复用,但是前者不是共享,享元是共享!
注意:享元模式的实现 垃圾回收 不友好,内存不会被回收;可达性分析中属于静态常量引用 属于ROOT
2.3 行为型
2.3.1 观察者 Observer Design Pattern 【发布订阅模式(Publish-Subscribe Design Pattern)】
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
- 被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。
- Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener
- 而基于消息队列的实现方式,被观察者和观察者解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。
代码示例
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
- 复用和扩展:复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
- 代码示例
- Collections 类 :Collections.sort()
- Java Servlet、
- JUnit TestCase、
- Java InputStream、
- Java AbstractList
- Spring 中的应用:Bean创建两步:对象的创建和对象的初始化。利用模板模式,Spring 能让用户定制 Bean 的创建过程。
这里的模板模式的实现,并不是标准的抽象类的实现方式,而是有点类似我们前面讲到的 Callback 回调的实现方式,也就是将要执行的函数封装成对象(比如,初始化方法封装成 InitializingBean 对象),传递给模板(BeanFactory)来执行。- Spring中带Template后缀的JdbcTemplate、RedisTemplate 等
模板模式 VS 回调
策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。
- “运行时动态确定”才是策略模式最典型的应用场景。
代码示例
将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。
- 代码示例
- Java Servlet 中的 Filter
- Spring MVC 中的 interceptor
- Dubbo Filter
AOP 和 FIlter
前置:有限状态机
- 有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
迭代器模式,也叫游标模式。它用来遍历集合对象。这里说的“集合对象”,我们也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如,数组、链表、树、图、跳表。
- 迭代器的优势
- 迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
- 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
- 迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。
使用迭代器的原因
- 类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。
- 代码示例
- Java 中 Iterator 迭代器
- ArrayList中的modCount(fail-fast 解决方式,让遍历操作直接抛出运行时异常):ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1。当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于 expectedModCount,也就是看,在创建完迭代器之后,modCount 是否改变过。
- 如果一个ArrayList调用iterator()两次,会生成两个迭代器,互不影响,但是同时去执行做操就会触发checkForComodification()的检查,就会触发ConcurrentModificationException异常。
- 使用Java中iterator时 它只能删除游标指向的前一个元素,而且一个 next() 函数(返回下一个元素,并将游标后移)之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错。
2.3.7 访问者模式 Visitor Design Pattern
- ArrayList中的modCount(fail-fast 解决方式,让遍历操作直接抛出运行时异常):ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1。当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于 expectedModCount,也就是看,在创建完迭代器之后,modCount 是否改变过。
- Java 中 Iterator 迭代器
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身
- 访问者模式针对的是一组类型不同的对象。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
双分派与单分派
在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
2.3.9 命令模式 Command Design Pattern
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
- 命令模式的主要作用和应用场景:用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,
策略模式与命令模式
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
- 代码示例
- Spring中的 SpEL,定义的语法规则,然后解析
2.3.11 中介模式 Mediator Design Pattern
- 中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
- 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。、
总结
创建型
- 创建型设计模式包括:单例模式、工厂模式、建造者模式、原型模式。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。