《设计模式之美》是极客时间上的一个代码学习系列,在学习之后特在此做记录和总结。
设计模式要干的事情就是解耦,也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。
每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可以解决哪类问题;第二部分是解决方案,即这个模式的设计思路和具体的代码实现。不过,代码实现并不是模式必须包含的。如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉。
一、创建型
创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。
1)单例模式
单例设计模式(Singleton Design Pattern)是指一个类只允许创建一个对象(或者实例),那这个类就是一个单例类。
public class IdGenerator { private static IdGenerator instance; private IdGenerator() {} public static IdGenerator getInstance() { if (instance == null) { synchronized(IdGenerator.class) { // 此处为类级别的锁 if (instance == null) { instance = new IdGenerator(); } } } return instance; } }
(1)实战案例一:处理资源访问冲突
将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。
(2)实战案例二:表示全局唯一类
从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类、唯一递增 ID 号码生成器。
实现:要实现一个单例,需要关注的点无外乎下面几个:
(1)构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
(2)考虑对象创建时的线程安全问题;
(3)考虑是否支持延迟加载;
(4)考虑 getInstance() 性能是否高(是否加锁)。
问题:有些人认为单例是一种反模式(anti-pattern),并不推荐使用。
(1)单例对 OOP 特性的支持不友好,对于其中的抽象、继承、多态都支持得不好。
(2)单例会隐藏类之间的依赖关系,通过构造函数、参数传递等方式声明的类之间的依赖关系很容易分辨,但是,单例类不需要显示创建、不需要依赖参数传递。
(3)单例对代码的扩展性不友好,单例类只能有一个对象实例。如果未来某一天,需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。
(4)单例对代码的可测试性不友好,如果单例类依赖比较重的外部资源,比如 DB,由于单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
(5)单例不支持有参数的构造函数,比如创建一个连接池的单例对象,没法通过参数来指定连接池的大小。
为了保证全局唯一,除了使用单例,还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。
2)工厂模式
(1)简单工厂(Simple Factory)
大部分工厂类都是以“Factory”这个单词结尾的,工厂类中创建对象的方法一般都是 create 开头。
public class RuleConfigParserFactory { public static IRuleConfigParser createParser(String configFormat) { IRuleConfigParser parser = null; if ("json".equalsIgnoreCase(configFormat)) { parser = new JsonRuleConfigParser(); } else if ("xml".equalsIgnoreCase(configFormat)) { parser = new XmlRuleConfigParser(); } else if ("yaml".equalsIgnoreCase(configFormat)) { parser = new YamlRuleConfigParser(); } else if ("properties".equalsIgnoreCase(configFormat)) { parser = new PropertiesRuleConfigParser(); } return parser; } }
(2)工厂方法(Factory Method)
定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
如果非得要将 if 分支逻辑去掉,那么比较经典处理方法就是利用多态。工厂方法模式比起简单工厂模式更加符合开闭原则。
public interface IRuleConfigParserFactory { IRuleConfigParser createParser(); } public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new JsonRuleConfigParser(); } } public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new XmlRuleConfigParser(); } }
在工厂类的使用上,工厂类对象的创建逻辑又耦合进了 load() 函数中,跟最初的代码版本非常相似。
可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。
当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。
而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。
3)建造者模式
Builder 模式,即建造者模式、构建者模式或生成器模式。要解决下面这些问题,就需要建造者模式上场了。
(1)如果可配置项逐渐增多,变成了 8 个、10 个,那么在使用构造函数的时候,就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。
(2)如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。
(3)如果配置项之间有约束条件,那么校验逻辑就无处安放了。
(4)如果希望对象在创建好之后,就不能再修改内部的属性值,那么就不能暴露 set() 方法。
可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后再使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。
除此之外,把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样就只能通过建造者来创建 ResourcePoolConfig 类对象。并且,ResourcePoolConfig 没有提供任何 set() 方法,这样创建出来的对象就是不可变对象了。
public class ResourcePoolConfig { private String name; private int maxTotal; private ResourcePoolConfig(Builder builder) { this.name = builder.name; this.maxTotal = builder.maxTotal; } //...省略getter方法... //将Builder类设计成了ResourcePoolConfig的内部类。 //也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。 public static class Builder { private static final int DEFAULT_MAX_TOTAL = 8; private String name; private int maxTotal = DEFAULT_MAX_TOTAL; public ResourcePoolConfig build() { // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等 if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } return new ResourcePoolConfig(this); } public Builder setName(String name) { if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } this.name = name; return this; } public Builder setMaxTotal(int maxTotal) { if (maxTotal <= 0) { throw new IllegalArgumentException("..."); } this.maxTotal = maxTotal; return this; } } } // 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle ResourcePoolConfig config = new ResourcePoolConfig.Builder() .setName("dbconnectionpool") .setMaxTotal(16) .build();
工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。
简单地说,工厂模式是根据不同的条件生成不同类的对象,建造者模式是根据不同参数生成一个类的不同对象。
4)原型模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
原型模式的实现方式:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)。
浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord 对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。
实现深拷贝的两种方法:
(1)第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。
(2)第二种方法:先将对象序列化,然后再反序列化成新的对象。
二、结构型
结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。
1)代理模式
代理模式(Proxy Design Pattern)是指在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
为了将框架代码和业务代码解耦,代理模式就派上用场了。
UserController 类只负责业务功能。代理类 UserControllerProxy 负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码。
public interface IUserController { UserVo login(String telephone, String password); UserVo register(String telephone, String password); } public class UserController implements IUserController { } public class UserControllerProxy implements IUserController { private MetricsCollector metricsCollector; private UserController userController; public UserControllerProxy(UserController userController) { this.userController = userController; this.metricsCollector = new MetricsCollector(); } @Override public UserVo login(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); // 委托 UserVo userVo = userController.login(telephone, password); long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); return userVo; } }
为了让代码改动尽量少,在刚刚的代理模式的代码实现中,代理类和原始类需要实现相同的接口。而对于外部类的扩展,一般都是采用继承的方式。
public class UserControllerProxy extends UserController { }
所谓动态代理(Dynamic Proxy),就是不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
应用场景:
(1)在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。
(2)RPC 框架也可以看作一种代理模式,通过远程代理,将网络通信、数据编解码等细节隐藏起来。在 AOP 切面中完成接口缓存的功能。
2)桥接模式
桥接模式(Bridge Design Pattern)也叫桥梁模式,对于这个模式有两种不同的理解方式。
(1)将抽象和实现解耦,让它们可以独立变化。
(2)一个类存在两个(或多个)独立变化的维度,通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
针对 Notification 的代码,将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender 相关类)。其中,Notification 类相当于抽象,MsgSender 类相当于实现,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,可以动态地去指定(比如,通过读取配置来获取对应关系)。
public interface MsgSender { void send(String message); } public class TelephoneMsgSender implements MsgSender { private List<String> telephones; public TelephoneMsgSender(List<String> telephones) { this.telephones = telephones; } @Override public void send(String message) { //... } } public class EmailMsgSender implements MsgSender { // 与TelephoneMsgSender代码结构类似,所以省略... } public abstract class Notification { protected MsgSender msgSender; public Notification(MsgSender msgSender) { this.msgSender = msgSender; } public abstract void notify(String message); } public class SevereNotification extends Notification { public SevereNotification(MsgSender msgSender) { super(msgSender); } @Override public void notify(String message) { msgSender.send(message); } } public class UrgencyNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } public class NormalNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... }
3)装饰器模式
装饰器模式(Decorator Design Pattern)相对于简单的组合关系,有两个比较特殊的地方。
(1)装饰器类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰器类。
(2)装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。
代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
代理模式偏重业务无关,高度抽象和稳定性较高的场景。装饰器模式偏重业务相关,定制化诉求高,改动较频繁的场景。
4)适配器模式
适配器模式(Adapter Design Pattern)可将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。下面是使用的前提条件。
(1)如果 Adaptee 接口并不多,那两种实现方式都可以。
(2)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
(3)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那推荐使用对象适配器,因为组合结构相对于继承更加灵活。
应用场景:
适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。
(1)封装有缺陷的接口设计。对外部系统提供的接口进行二次封装,抽象出更好的接口设计。
(2)统一多个类的接口设计。将所有系统的接口适配为统一的接口定义。
(3)替换依赖的外部系统。
(4)兼容老版本接口。
(5)适配不同格式的数据。
代理、桥接、装饰器和适配器都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。
(1)代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
(2)桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
(3)装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
(4)适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
5)门面模式
门面模式(Facade Design Pattern)为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。子系统(subsystem)既可以是一个完整的系统,也可以是更细粒度的类或者模块。
App 客户端的响应速度比较慢,排查之后发现,是因为过多的接口调用过多的网络通信。针对这种情况,就可以利用门面模式,让后端服务器提供一个包裹 a、b、d 三个接口调用的接口 x。App 客户端调用一次接口 x,来获取到所有想要的数据,将网络通信的次数从 3 次减少到 1 次,也就提高了 App 的响应速度。
应用场景:
(1)解决易用性问题,比如,Linux 系统调用函数、Shell 命令。
(2)解决性能问题,如果门面接口特别多,并且很多都是跨多个子系统的,可将门面接口放到一个新的子系统中。
(3)解决分布式事务问题,比如在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。
与适配器模式的区别:
(1)适配器模式是做接口转换,解决的是原接口和目标接口不匹配的问题。在代码结构上主要是继承加组合。
(2)门面模式做接口整合,解决的是多接口调用带来的问题。在代码结构上主要是封装。
6)组合模式
组合模式(Composite Design Pattern)跟面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。它主要是用来处理树形结构数据,其中数据可理解为一组对象集合。
组合模式是将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(指代码的使用者)可以统一单个对象和组合对象的处理逻辑。
对照着例子,重新定义:
将一组对象(文件和目录)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(目录与子目录的嵌套结构)。组合模式让客户端可以统一单个对象(文件)和组合对象(目录)的处理逻辑(递归遍历)。
实际上,组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树,业务需求可以通过在树上的递归遍历算法来实现。
7)享元模式
所谓享元,顾名思义就是被共享的单元。享元模式(Flyweight Design Pattern)的意图是复用对象,节省内存,前提是享元对象是不可变对象。
当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。对于相似对象,也可以将它相同的部分(字段)提取出来,设计成享元。
“不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。
所有的 ChessBoard 对象共享这 30 个 ChessPieceUnit 对象(因为象棋中只有 30 个棋子)。在使用享元模式之前,记录 1 万个棋局,要创建 30 万(30*1 万)个棋子的 ChessPieceUnit 对象。利用享元模式,只需要创建 30 个享元对象供所有棋局共享使用即可,大大节省了内存。
// 享元类 public class ChessPieceUnit { private int id; private String text; private Color color; public ChessPieceUnit(int id, String text, Color color) { this.id = id; this.text = text; this.color = color; } public static enum Color { RED, BLACK } } public class ChessPieceUnitFactory { private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>(); static { pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK)); pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK)); //...省略摆放其他棋子的代码... } public static ChessPieceUnit getChessPiece(int chessPieceId) { return pieces.get(chessPieceId); } } public class ChessPiece { //棋子 private ChessPieceUnit chessPieceUnit; private int positionX; private int positionY; public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) { this.chessPieceUnit = unit; this.positionX = positionX; this.positionY = positionY; } } public class ChessBoard { //棋局 private Map<Integer, ChessPiece> chessPieces = new HashMap<>(); public ChessBoard() { init(); } private void init() { chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(1), 0,0)); chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(2), 1,0)); //...省略摆放其他棋子的代码... } public void move(int chessPieceId, int toPositionX, int toPositionY) { //...省略... } }
实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。
三、行为型
创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。
设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。
1)观察者模式
观察者模式(Observer Design Pattern)也叫发布订阅模式(Publish-Subscribe Design Pattern),在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。观察者模式就是将观察者和被观察者代码解耦。
public interface Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(Message message); } public interface Observer { void update(Message message); }
基于消息队列的实现方式,被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。
2)模板模式
模板模式(Template Method Design Pattern)全称是模板方法模式,可在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的“算法”,可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。
public abstract class AbstractClass { public final void templateMethod() { //... method1(); //... method2(); //... } protected abstract void method1(); protected abstract void method2(); } public class ConcreteClass1 extends AbstractClass { @Override protected void method1() {} @Override protected void method2() {} } public class ConcreteClass2 extends AbstractClass { @Override protected void method1() {} @Override protected void method2() {} }
模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。
(1)作用一:复用
模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。
(2)作用二:扩展
这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似之前讲到的控制反转。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。
3)策略模式
策略模式(Strategy Design Pattern)定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(使用算法的代码)。
策略模式解耦的是策略的定义、创建、使用这三部分。让每个部分都不至于过于复杂、代码量过多。
(1)策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。
(2)通过类型(type)来判断创建哪个策略。可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。
(3)运行时动态确定使用哪种策略,即在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。
利用策略模式避免分支判断。将不同类型订单的打折策略设计成策略类,并由工厂类来负责创建策略对象。
// 策略的定义 public interface DiscountStrategy { double calDiscount(Order order); } // 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码... // 策略的创建 public class DiscountStrategyFactory { private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>(); static { strategies.put(OrderType.NORMAL, new NormalDiscountStrategy()); strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy()); strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy()); } public static DiscountStrategy getDiscountStrategy(OrderType type) { return strategies.get(type); } } // 策略的使用 public class OrderService { public double discount(Order order) { OrderType type = order.getType(); DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type); return discountStrategy.calDiscount(order); } }
策略模式侧重“策略”或“算法”这个特定的应用场景,用来解决根据运行时状态从一组策略中选择不同策略的问题,而工厂模式侧重封装对象的创建过程,这里的对象没有任何业务场景的限定,可以是策略,但也可以是其他东西。
4)职责链模式
职责链模式(Chain Of Responsibility Design Pattern)是将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。
在职责链模式中,多个处理器(接收对象)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
public abstract class Handler { //模板模式 protected Handler successor = null; public void setSuccessor(Handler successor) { this.successor = successor; } public final void handle() { boolean handled = doHandle(); if (successor != null && !handled) { successor.handle(); } } protected abstract boolean doHandle(); } public class HandlerA extends Handler { @Override protected boolean doHandle() { boolean handled = false; //... return handled; } } public class HandlerB extends Handler { @Override protected boolean doHandle() { boolean handled = false; //... return handled; } } public class HandlerChain { private Handler head = null; private Handler tail = null; public void addHandler(Handler handler) { handler.setSuccessor(null); if (head == null) { head = handler; tail = handler; return; } tail.setSuccessor(handler); tail = handler; } public void handle() { if (head != null) { head.handle(); } } } public class Application { //使用举例 public static void main(String[] args) { HandlerChain chain = new HandlerChain(); chain.addHandler(new HandlerA()); chain.addHandler(new HandlerB()); chain.handle(); } }
职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。这种变体也有两种实现方式:用链表存储处理器和用数组存储处理器。
为什么非要使用职责链模式呢?这是不是过度设计呢?
(1)应对代码的复杂性,用职责链模式把各个敏感词过滤函数继续拆分出来,设计成独立的类,进一步简化了 SensitiveWordFilter 类,让 SensitiveWordFilter 类的代码不会过多,过复杂。
(2)满足开闭原则,当要扩展新的过滤算法时,只需要新添加一个 Filter 类,并且通过 addFilter() 函数将它添加到 FilterChain 中即可,其他代码完全不需要修改。
5)状态模式
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。
有限状态机(Finite State Machine,FSM),简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
(1)状态机实现方式一:分支逻辑法
参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以这种方法暂且命名为分支逻辑法。
(2)状态机实现方式二:查表法
把这两个二维数组存储在配置文件中,当需要修改状态机时,甚至可以不修改任何代码,只需要修改配置文件就可以了。
(3)状态机实现方式三:状态模式
如果要执行的动作是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),那么查表法就不合适了。
状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。
public interface IMario { //所有状态类的接口 void obtainMushRoom(); void obtainCape(); } public class SmallMario implements IMario { private MarioStateMachine stateMachine; public SmallMario(MarioStateMachine stateMachine) { this.stateMachine = stateMachine; } @Override public void obtainMushRoom() { stateMachine.setCurrentState(new SuperMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 100); } @Override public void obtainCape() { stateMachine.setCurrentState(new CapeMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 200); } } public class SuperMario implements IMario { private MarioStateMachine stateMachine; public SuperMario(MarioStateMachine stateMachine) { this.stateMachine = stateMachine; } @Override public void obtainMushRoom() { // do nothing... } @Override public void obtainCape() { stateMachine.setCurrentState(new CapeMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 200); } } public class MarioStateMachine { private int score; private IMario currentState; //不再使用枚举来表示状态 public MarioStateMachine() { this.score = 0; this.currentState = new SmallMario(this); } public void obtainMushRoom() { this.currentState.obtainMushRoom(); } public void obtainCape() { this.currentState.obtainCape(); } public void setScore(int score) { this.score = score; } public void setCurrentState(IMario currentState) { this.currentState = currentState; } }
6)迭代器模式
迭代器模式(Iterator Design Pattern)也叫游标模式(Cursor Design Pattern)用来遍历集合对象。
这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。
迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。
一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。
public interface Iterator<E> { boolean hasNext(); void next(); E currentItem(); }
总结下来就三句话:迭代器中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。
为什么还要用迭代器来遍历容器呢?为什么还要给容器设计对应的迭代器呢?
(1)复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。
(2)将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。
(3)容器和迭代器都提供了抽象的接口,方便在开发时基于接口而非具体的实现编程。
在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。
7)访问者模式
访问者者模式(Visitor Design Pattern)允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。
访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。
在不同的应用场景下,需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的。
8)备忘录模式
备忘录模式(Memento Design Pattern)也叫快照(Snapshot)模式,在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
备忘录模式主要是用来防丢失、撤销、恢复等。
其一,定义一个独立的类(Snapshot 类)来表示快照,而不是复用 InputText 类。这个类只暴露 get() 方法,没有 set() 等任何修改内部状态的方法。
其二,在 InputText 类中,把 setText() 方法重命名为 restoreSnapshot() 方法,用意更加明确,只用来恢复对象。
public class InputText { private StringBuilder text = new StringBuilder(); public String getText() { return text.toString(); } public void append(String input) { text.append(input); } public Snapshot createSnapshot() { return new Snapshot(text.toString()); } public void restoreSnapshot(Snapshot snapshot) { this.text.replace(0, this.text.length(), snapshot.getText()); } } public class Snapshot { private String text; public Snapshot(String text) { this.text = text; } public String getText() { return this.text; } } public class SnapshotHolder { private Stack<Snapshot> snapshots = new Stack<>(); public Snapshot popSnapshot() { return snapshots.pop(); } public void pushSnapshot(Snapshot snapshot) { snapshots.push(snapshot); } }
9)命令模式
命令模式(Command Design Pattern)将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
在大部分编程语言中,函数没法作为参数传递给其他函数,也没法赋值给变量。借助命令模式,可以将函数封装成对象。设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。
在策略模式中,不同的策略具有相同的目的、不同的实现、互相之间可以替换。比如,BubbleSort、SelectionSort 都是为了实现排序的,只不过一个是用冒泡排序算法来实现的,另一个是用选择排序算法来实现的。而在命令模式中,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相之间不可替换。
命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等。
10)解释器模式
解释器模式(Interpreter Design Pattern)只在一些特定的领域会被用到,比如编译器、规则引擎、正则表达式。它能为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
11)中介模式
中介模式(Mediator Design Pattern)定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。
假设有一个比较复杂的对话框,对话框中有很多控件,比如按钮、文本框、下拉框等。当对某个控件进行操作的时候,其他控件会做出相应的反应,比如,在下拉框中选择“注册”,注册相关的控件就会显示在对话框中。
public interface Mediator { void handleEvent(Component component, String event); } public class LandingPageDialog implements Mediator { private Button loginButton; private Button regButton; private Selection selection; private Input usernameInput; private Input passwordInput; private Input repeatedPswdInput; private Text hintText; @Override public void handleEvent(Component component, String event) { if (component.equals(loginButton)) { String username = usernameInput.text(); String password = passwordInput.text(); //校验数据... //做业务处理... } else if (component.equals(regButton)) { //获取usernameInput、passwordInput、repeatedPswdInput数据... //校验数据... //做业务处理... } else if (component.equals(selection)) { String selectedItem = selection.select(); if (selectedItem.equals("login")) { usernameInput.show(); passwordInput.show(); repeatedPswdInput.hide(); hintText.hide(); //...省略其他代码 } else if (selectedItem.equals("register")) { //.... } } } }
好处是简化了控件之间的交互,坏处是中介类有可能会变成大而复杂的“上帝类”(God Class)。所以,在使用中介模式的时候,要根据实际的情况,平衡对象之间交互的复杂度和中介类本身的复杂度。
中介模式和观察者模式的区别在哪里呢?
(1)在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。
(2)而中介模式正好相反。参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。