Behavioral Design Patterns 行为模式
行为模式负责对象之间的高效沟通和职责委派,这些模式刻画了运行时难以跟踪的复杂控制流,从而把开发者的注意力从控制流转移到对象间的联系方式上。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。
行为类模式使用继承机制在类间分派行为,有模板方法(Template Method)和解释器(Interpreter)。
行为对象模式使用对象复合而不是继承。一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一个对象都无法单独完成的任务。这里一个重要的问题是对等的对象如何互相了解对方。对等对象可以保持显式的对对方的引用,但那会增加它们的耦合度。在极端情况下,每一个对象都要了解所有其他的对象。 中介者在对等对象间引入一个中介对象以避免这种情况的出现。 中介者提供了松耦合所需的间接性。
责任链模式提供更松的耦合。它让你通过一条候选对象链隐式的向一个对象发送请求。根据运行时刻情况任一候选者都可以响应相应的请求。候选者的数目是任意的,可以在运行时刻决定哪些候选者参与到链中。
观察者模式定义并保持对象间的依赖关系。
其他的行为对象模式常将行为封装在一个对象中并将请求指派给它。策略模式将算法封装在对象中,这样可以方便地指定和改变一个对象所使用的算法。 命令模式将请求封装在对象中,这样它就可作为参数来传递,也可以被存储在历史列表里,或者以其他方式使用。状态模式封装一个对象的状态,使得当这个对象的状态对象变化时,该对象可改变它的行为。访问者模式封装分布于多个类之间的行为,而迭代者模式则抽象了访问和遍历一个集合中的对象的方式。
Chain of Responsibility 责任链
问题
在线订购系统的访问限制,认证用户可创建订单,有管理权限的用户拥有所有订单的完全访问权限,这些检查依次进行,如何实现?
请求发送者和接收者的强耦合关系将使得后续调整检查步骤十分困难,并且复用部分检查步骤也很困难。
解决方案
使多个对象都有机会处理请求,从而避免发送者和接收者之间的耦合关系。
示意图
两种方式:
- 请求中包含正确的数据,所有处理者都执行自己的主要行为
- 如果某一个处理者能够处理,将不再传递请求
Demo代码
public class ChainOfResponsibilityPattern {
public static void main(String[] args) {
//组装责任链
Handler userAuth = new UserAuthenticator();
Handler adminAuth = new AdminAuthenticator();
Handler endHandler = new EndHandler();
userAuth.setNext(adminAuth);
adminAuth.setNext(endHandler);
//提交请求
string userName = args[1];
userAuth.handle(userName);
}
}
abstract class Handler {
private Handler next;
public void setNext(Handler next) {
this.next = next;
}
public abstract void handle(String request);
}
class UserAuthenticator extends Handler {
public void handle(String userName) {
if (!userExists(userName)) {
System.out.println("用户不存在。");
} else {
// 赋予用户权限
...
next.handle(userName);
}
}
}
class AdminAuthenticator extends Handler {
public void handle(String userName) {
if (adminExists(userName)) {
// 赋予管理者权限
...
next.handle(userName);
}
}
}
class EndHandler extends Handler {
public void handle(String userName) {
return;
}
}
适用场景
- 需要用不同方式处理不同种类需求,且请求类型和顺序预先未知
- 必须按顺序执行多个处理者
- 处理者和顺序必须在运行时改变
优缺点
优点
-
SRP:解耦触发和执行操作的类
-
OCP:在不修改客户端代码的情况下增加新的处理者
-
可以控制请求处理的顺序
缺点
- 部分请求可能未被处理
和其他模式的区别
- 和装饰模式的区别:结构很相似,都依赖于递归组合把要执行的操作传递给一系列对象;但责任链的管理者可以相互独立地执行一切操作和随时停止传递请求,装饰无法中断请求的传递但能在遵循接口的情况下扩展对象的行为。
Command Pattern 命令
问题
想象一下,你要把控制房间的灯的开关、变亮和变暗的请求设置到新买的遥控器上,你要怎么做呢?硬编码吗?如果你还想继续添加遥控器呢?如果你想把多个操作设置到一个按键呢?如果你以后也想用遥控器去操控其他电器呢?
又或者,让你去为一款编辑器开发不同按键的功能,你会怎么做呢?对不同的按键编写不同的类,在按键的类中集成该按键的操作吗?快捷键怎么办呢?从图形界面实现的相同操作怎么办呢?要怎么撤回操作呢?
解决方案
你说得对,但是命令模式是一款…… 将请求封装为包含所有和该请求相关的信息的独立的类,从而让你能够把请求参数化、延迟请求的执行或放入队列,并能实现可撤销操作的设计模式。
UML图
Invoker
不负责创建命令对象,它通过构造函数从客户端获取预先生成的命令;客户端把包括接收者实体在内的所有请求参数传给命令的构造参数,生成命令实体。
Demo代码
public interface Command {
void execute() {}
void unExecute() {}
}
public LightOn implements Command {
private Light light;
public LightOn(Light light) {
this.light = light;
}
void execute() {
this.light.on();
}
void unExecute() {
this.light.off();
}
}
public LightDown implements Command {
private Light light;
public LightOn(Light light) {
this.light = light;
}
void execute() {
this.light.down();
}
void unExecute() {
this.light.up();
}
}
public class Invoker {
private Command cmd;
void setCommand(Command cmd) {
this.cmd = cmd;
}
void execCommand() {
cmd.execute();
}
void unExecuteCommand() {
cmd.unExecute();
}
}
public class Light {
void on() {}
void off() {}
void down() {}
void up() {}
}
适用场景
- 需要把命令作为方法的参数进行传递、保存命令在其他对象,或运行时切换已连接的命令时
- 需要把操作放入队列、通过远程执行操作
- 需要实现操作回滚
优缺点
优点
- SRP:解耦触发和执行操作的类
- OCP:在不修改客户端代码的情况下创建新的命令
- 实现撤销和恢复功能
- 实现操作的延迟操作
- 可以把简单命令组合成复杂命令
缺点
- 在发送者和接收者之间增加了一个全新的层次,代码可能会变得更加复杂
和其他模式的区别
- 和策略模式的区别:命令能将任何操作转换为对象,策略则将实现同一目的的不同算法转换为对象
- 和备忘录模式的区别:都能实现撤销操作,命令通过执行逆操作,备忘录保存命令执行前的状态
- 命令、责任链、中介者、观察者的区别:都用于处理请求发送者和接收者之间不同的连接方式,命令在发送者和接收者之间建立单向连接,责任链按顺序传给一系列潜在接收者,中介者强制发送者和接收者通过一个中介对象间接沟通,观察者允许接收者动态订阅或取消接收请求。
Mediator 中介者
问题
一个创建和修改客户资料的对话框,由各种控件组成,如文本框、复选框(Checkbox)和按钮,某些表单元素可能会直接进行互动,如提交按钮会在保存数据前校验所有输入内容。
元素之间存在许多关联,直接在表单元素代码中实现业务逻辑将导致很难复用和修改。
解决方案
用一个中介对象来封装一系列的对象交互,使得各对象不需要显式地相互引用,从而实现解耦。
UML图
Demo代码
public class Mediator {
// 引用UI组件:
private List<JCheckBox> checkBoxList;
private JButton selectAll;
private JButton selectNone;
private JButton selectInverse;
public Mediator(List<JCheckBox> checkBoxList, JButton selectAll, JButton selectNone, JButton selectInverse) {
this.checkBoxList = checkBoxList;
this.selectAll = selectAll;
this.selectNone = selectNone;
this.selectInverse = selectInverse;
// 绑定事件:
this.checkBoxList.forEach(checkBox -> {
checkBox.addChangeListener(this::onCheckBoxChanged);
});
this.selectAll.addActionListener(this::onSelectAllClicked);
this.selectNone.addActionListener(this::onSelectNoneClicked);
this.selectInverse.addActionListener(this::onSelectInverseClicked);
}
// 当checkbox有变化时:
public void onCheckBoxChanged(ChangeEvent event) {
boolean allChecked = true;
boolean allUnchecked = true;
for (var checkBox : checkBoxList) {
if (checkBox.isSelected()) {
allUnchecked = false;
} else {
allChecked = false;
}
}
selectAll.setEnabled(!allChecked);
selectNone.setEnabled(!allUnchecked);
}
// 当点击select all:
public void onSelectAllClicked(ActionEvent event) {
checkBoxList.forEach(checkBox -> checkBox.setSelected(true));
selectAll.setEnabled(false);
selectNone.setEnabled(true);
}
// 当点击select none:
public void onSelectNoneClicked(ActionEvent event) {
checkBoxList.forEach(checkBox -> checkBox.setSelected(false));
selectAll.setEnabled(true);
selectNone.setEnabled(false);
}
// 当点击select inverse:
public void onSelectInverseClicked(ActionEvent event) {
checkBoxList.forEach(checkBox -> checkBox.setSelected(!checkBox.isSelected()));
onCheckBoxChanged(null);
}
}
适用场景
- 一些对象和其他对象紧密耦合以致于难以对其修改
- 组件过于依赖其他组件导致无法在不同应用中复用
- 为了能在不同情景复用一些基本行为导致需要创建大量组件子类时
优缺点
优点
- SRP:可以把多个组件的交流抽取到同一位置,使其更易理解和维护
- OCP:无需修改组件即可增加新的中介者
- 减轻组件间的耦合情况
- 可方便地复用各个组件
缺点
- 将交互的复杂性变为中介者的复杂性,可能会变得越来越难以维护
Memento 备忘录
问题
开发编辑器的撤销功能,采用记录快照的方式来恢复原来的状态,但快照所需要的信息很多,要么会暴露类的所有内部细节使其过于脆弱,要么会限制对其状态的访问权限而无法生成快照。
解决方案
把对象状态的副本存储在名为备忘录的特殊对象中,除了创建备忘录的对象外,任何对象不能访问备忘录的内容,其他对象必须使用受限接口和备忘录进行交互,即可以获取快照的元数据(创建时间、操作名称等),不能获取快照中原始对象的状态。
UML图
基于嵌套类的实现
备忘录类被嵌套在原发器中,从而原发器可访问备忘录的成员变量和方法;负责人对备忘录的访问权限很有限,只能在栈中保存备忘录,不能修改其状态。
封装更严格的实现
恢复方法被定义在备忘录类中,负责人明确禁止修改存储在备忘录中的状态。
Demo代码
public class Editor {
private Text text;
private Position pos;
private Length selectionWidth;
Snapshot createSnapshot() {
return new Snapshot(this, text, pos, selectionWidth);
}
class Snapshot {
private Text snapText;
private Position snapPos;
private Length snapSelectionWidth;
Snapshot(Text text, Position pos, Length selectionWidth) {
this.snapText = text;
this.snapPos.x = pos.x;
this.snapPos.y = pos.y;
this.snapSelectionWidth = selectionWidth;
}
void restore() {
text = snapText;
pos.x = this.snapPos.x;
pos.y = this.snapPos.y;
selectionWidth = this.snapSelectionWidth;
}
}
}
public class Command {
private Editor editor;
private Editor.Snapshot backup;
public Command(Editor editor) {
this.editor = editor;
}
public void makeBackup() {
backup = editor.createSnapshot();
}
public void undo() {
if (backup != null) {
backup.restore();
}
}
}
适用场景
- 需要创建对象状态快照来恢复之前的状态时
- 直接访问对象的成员变量、获取器或设置器将导致封装被突破时
优缺点
优点
- 可以在不破坏封装情况的前提下创建对象状态快照
- 可以通过让负责人维护原发器状态历史来简化原发器的代码
缺点
- 客户端过于频繁地创建备忘录,将消耗大量内存
- 负责人必须完整跟踪原发器的生命周期,以销毁弃用的备忘录
- 绝大部分动态编程语言无法保证备忘录中的状态不被修改