行为型设计模式(十一种):策略模式、模板方法模式、观察者模式、迭代模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
一、策略模式
策略模式是指定义了算法家族并分别封装起来,让他们之间可以互相替换,此模式使得算法的变化不会影响使用算法的用户。
策略模式的应用场景:
- 系统中有很多类,而他们的区别仅仅在于行为不同;
- 一个系统需要动态地在几种算法中选择一种
策略模式的类图:
这里我们就以 JDK 中常用的比较器 — Comparator
接口,常用的compare()
就是测了模式算法的抽象实现:
@FunctionalInterface
public interface Comparator<T> {
/**
* Compares its two arguments for order. Returns a negative integer.
* 比较两个对象的值,返回数字
*/
int compare(T o1, T o2);
}
Comparator
接口有非常多的实现类,在开发中经常将Comparator
接口作为传入参数实现排序策略,例如:
在 Arrays 工具类对数组进行排序时,可以传入指定的排序策略:
在TreeMap
类的构造方法中使用:
优点:
- 算法可以自由切换。
- 避免使用多重条件判断。
- 扩展性良好。
缺点:
- 策略类会增多。
- 所有策略类都需要对外暴露。
二、模板方法模式
模板方法模式是指定义一个算法骨架,并允许子类为一个或者多个步骤提供实现。模板模式使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。
模板方法模式适用场景:
- 一次性实现一个算法的不变部分,并将可变行为留给子类实现;
- 各个子类中公共的行为被提取出来并集中到一个公共的父类中,从而避免代码重复;
模板方法模式的类图:
模板方法模式算是一种比较简单的设计模式,而且应用在许多的框架代码中。比如,HttpServlet
中有三个方法:service()
、doGet()
、doPost()
,都是模板方法的抽象实现。还有 MyBatis 框架中的 BaseExecutor
类,它是一个基础的SQL执行类,实现了大部分 SQL 执行逻辑,然后把几个方法教给子类定制化完成,源码如下:
BaseExecutor
的类图:
可以看到子类都实现了这几个模板方法。
在 JUC 并发包下,AbstractQueuedSynchronizer
类就是典型的模板方法模式。下面类图可以看出,在ReentrantLock
和Semaphore
中都有内部类Sync
,而Sync
便实现了AbstractQueuedSynchronizer
的模板方法。
具体了解AQS中模板方法的细节,可以参考:
优点:
- 封装不变部分,扩展可变部分。
- 提取公共代码,便于维护。
- 行为由父类控制,子类实现。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
三、观察者模式
观察者模式定义了对象之间的一对多依赖,让多个观察者对象同时监听一个主体对象,当主体对象发生变化时,它的所有依赖者(观察者)都会收到通知并更新。观察者模式也被叫做发布订阅模式。
观察者模式主要用于在关联行为之间建立一套触发机制的场景。比如微信朋友圈动态通知、邮件通知、广播通知、桌面程序的事件响应等。
观察者模式的类图:
在 Java API 有内置的观察者模式。继承Observable
类,成为主题。实现Observer
接口,成为观察者。在 Observable
类中有一个Vector<Observer> obs
集合用于保存注册进来的观察者。
模拟老师给学生在线布置作业,Teacher
类继承自Observable
,Student
类实现Observer
接口。
Teacher
类:
public class Teacher extends Observable {
/** 作业内容 */
private String homeWork;
/** 给学生发布作业 */
public void pushWork(String homeWork) {
this.homeWork = homeWork;
// 向每一个学生发布作业
notifyObservers(homeWork);
}
@Override
public synchronized void addObserver(Observer o) {
super.addObserver(o);
}
@Override
public void notifyObservers() {
super.notifyObservers();
}
@Override
public void notifyObservers(Object arg) {
super.notifyObservers(arg);
}
}
Student
类:
public class Student implements Observer {
@Override
public void update(Observable o, Object arg) {
// 学生收到作业,打印作业内容
System.out.println("收到作业,作业内容: " + arg);
}
}
测试类:
public class ObserverMain {
public static void main(String[] args) {
// 创建几个学生
Student s1 = new Student();
Student s2 = new Student();
Student s3 = new Student();
Student s4 = new Student();
Teacher teacher = new Teacher();
teacher.addObserver(s1);
teacher.addObserver(s2);
teacher.addObserver(s3);
teacher.addObserver(s4);
teacher.pushWork("五三物理做完..");
System.out.println("当前观察者数量: " + teacher.countObservers());
}
}
小程序类图:
优点:
- 观察者和被观察者是抽象耦合的。
- 建立一套触发机制。
缺点:
- 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
四、迭代模式
迭代模式是指提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。简单来说就是对容器的遍历。
迭代模式类图:
Java 集合中迭代器就是迭代模式的使用。所有可以被迭代的集合都继承自Iterable
接口,并且会在集合内部实现一个继承自Iterator
接口的迭代器。通过调用iterator()
方法,来获取某个特定集合的迭代器。看一下 Java 中一些常用集合的类图就很容易明白了。
优点:
- 它支持以不同的方式遍历一个聚合对象。
- 迭代器简化了聚合类。
- 在同一个聚合上可以有多个遍历。
- 在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
五、责任链模式
责任链模式用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。
责任链模式类图:
Servlet 的过滤器链以及 Spring 中的拦截器链都采用了责任链模式。比如在 Spring AOP 中就会将通知方法转换为xxxInterceptor
拦截器,拦截器中的invoke()
就只负责自己的处理逻辑。而MethodInvocation
则是将这些拦截器与业务方法连接起来,统一成一条拦截器链。调用proceed()
方法,对责任链上的处理器方法进行执行。
拦截器链的调用逻辑,我在前面的适配器模式案例中已经提及到了。
Java设计模式之适配器模式
优点:
- 降低耦合度。它将请求的发送者和接收者解耦。
- 简化了对象。使得对象不需要知道链的结构。
- 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
- 增加新的请求处理类很方便。
缺点:
- 不能保证请求一定被接收。
- 系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
- 可能不容易观察运行时的特征,有碍于除错。
六、命令模式
命令模式将“请求”封装为对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
命令模式类图:
命令模式可以用于执行者调用接受者时操作过多,使用命令类来解耦执行者与接受者。将接受者的操作封装在命令类的execute()
方法中,执行者只需调用命令对象的方法即可。
比如:笔记本电脑的开机按钮,如果没有开机按钮这个命令,执行者需要打开笔记本显示器、风扇、音响等等部分。而开机按钮就相当于一个命令类,将这些操作都封装了起来,只给执行者提供了一个按钮(相当于execute()
)。
优点:
- 降低了系统耦合度。
- 新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
七、备忘录模式
备忘录模式在不破坏封闭的前提下,将对象当前的内部状态保存在对象之外,之后可以再次恢复到此状态。
备忘录类图:
在 Java 中可以使用序列化机制(Serialization)将某个对象的状态存储到硬盘中。
当然备忘录模式也可以配合上面讲解的命令模式,来实现对用户请求“撤销”功能的实现。
优点:
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
- 实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
八、状态模式
状态模式允许对象在内部状态改变时改变他的行为,对象看起来好像修改了它的类。
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
状态模式类图:
可以看到状态模式与策略模式非常类似,区别在于context
的行为随时可委托到某个状态对象中,当前状态在状态对象集合中游走改变,反应出 context
内部的状态, context
的客户对于状态对象的改变是不了解的。
而对于策略模式来说,是客户主动指定context
所要组合的策略对象是哪一个。
优点:
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
九、访问者模式
访问者模式是一种将数据操作和数据结构分离的设计模式。
访问者模式允许你对组合结构加入新的操作,而无需改变结构本身。要求组合结构是固定不变的。在实际的开发中使用率很少。
访问者模式类图:
- Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式;
- ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为;
- Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问;
- ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法;
- ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。
优点:
- 符合单一职责原则;
- 优秀的扩展性;
- 灵活性。
缺点:
- 具体元素对访问者公布细节,违反了迪米特原则;
- 具体元素变更比较困难。
十、中介者模式
中介者模式用来集中相关对象之间复杂的沟通和控制方式。
中介者模式使用场景:
- 系统中对象间存在较为复杂引用,导致依赖关系和结构混乱而无法复用的情况;
- 想通过一个中间类来封装多个类的行为,但是又不想要太多的子类。
中介者模式类图:
MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。
优点:
- 降低了类的复杂度,将一对多转化成了一对一;
- 各个类之间的解耦;
- 符合迪米特原则。
缺点:中介者会庞大,变得复杂难以维护。
十一、解释器模式
解释器模式为语言创建解释器。当需要实现一个简单语言时,就使用解释器模式定义语法的类,并用一个解释器解释句子。每个语法规则都用一个类代表。
解释器模式使用场景:
- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
- 一些重复出现的问题可以用一种简单的语言来进行表达。
- 一个简单语法需要解释的场景。
解释器模式类图:
设计模式六大原则
最后提一下设计模式的六大原则。
1.单一职责原则(SRP)
一个类别太大,负责单一的职责。
2.开闭原则(OCP)
对扩展开放,对修改关闭。尽量不修改原来代码的情况下进行扩展。抽象化,多态是开闭原则的关键。
3.里氏替换原则(LSP)
所有使用父类的地方,必须能够透明的使用子类对象。
4.依赖倒置原则(DIP)
依赖抽象,而不是依赖具体,面向接口编程。
5.接口隔离原则(ISP)
每一个接口应该承担独立的角色,不应该让多个接口合并,避免子类实现不需要实现的方法。对客户提供接口的时候,只需要暴露最小的接口。
6.迪米特原则(LOD)
尽量不要和陌生人说话,降低耦合,和其他类的耦合度变低。
在迪米特法则中,对于一个对象,非陌生人包括以下几类:
- 当前对象本身;
- 以参数形式传入到对象方法中的对象;
- 当前对象的成员对象;
- 如果当前对象的成员对象是一个集合,那么集合中的元素也是朋友;
- 当前对象所创建的对象。