前言
行为型设计模式是描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务
前面的模板方法模式是给出一个操作流程骨架,将一些特定的步骤延迟到子类中实现
(请客过程中吃什么由子类描述,父类只描述这个请客过程:点单-》吃-》买单)
下面是命令模式
现实中的问题
软件开发系统中,常常出现“方法的请求者”与“方法的实现者”之间存在紧密的耦合关系(比如一个类调用另一个类的方法,这样操作耦合高)。这不利于软件功能的扩展与维护。
思考一下,如果是耦合关系,想对多个行为进行“撤销、重做、记录”等处理,该怎么操作?
是不是对每一种操作都需要记录,会多出很多操作
因此“如何将方法的请求者与方法的实现者解耦”变得很重要
举个栗子
这是一个多用遥控器,不仅仅是开灯关灯功能,还要有撤销
运用命令模式就能较好的解决
命令模式
命令模式(Command Pattern):将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式
命令模式的模式动机:命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
命令模式结构:
模式角色:
- Command: 抽象命令类,声明执行命令的接口
- ConcreteCommand: 具体命令类,抽象命令类的实现,拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作
- Invoker: 请求者,请求的发送者,通常拥有很多的命令对象(可以用集合存储),并通过访问命令对象来执行相关请求,它不直接访问接收者
- Receiver: 接收者:业务的真正的执行者,执行具体命令类的命令
命令模式案例
通过命令模式模拟遥控器,通过遥控器按钮控制灯的开关
package com.company.Behavioral.Command;
import java.util.ArrayList;
import java.util.List;
//动作执行对象
class LightReceiver{
public void on(){
System.out.println("电灯打开了。。。");
}
public void off(){
System.out.println("电灯关闭了。。。");
}
}
//抽象命令
interface Command {
//执行动作
public void execute();
//撤销动作
public void undo();
}
//具体命令对象:开灯命令对象
class LightOnCommend implements Command{
//聚合LightReceiver
private LightReceiver lightReceiver;
public LightOnCommend(LightReceiver lightReceiver) {
this.lightReceiver = lightReceiver;
}
//当前是开灯命令,执行动作是开灯
@Override
public void execute() {
lightReceiver.on();
}
//当前是开灯命令,撤销动作是关灯
@Override
public void undo() {
lightReceiver.off();
}
}
//具体命令对象:关灯命令对象
class LightOffCommend implements Command{
//聚合LightReceiver
private LightReceiver lightReceiver;
public LightOffCommend(LightReceiver lightReceiver) {
this.lightReceiver = lightReceiver;
}
//当前是关灯命令,执行动作是关灯
@Override
public void execute() {
lightReceiver.off();
}
//当前是关灯命令,撤销动作是关开灯
@Override
public void undo() {
lightReceiver.on();
}
}
//空命令对象:空执行,用于初始化每个按钮
//空对象模式,可以省掉对空的判断
class NoCommend implements Command{
@Override
public void execute() {
}
@Override
public void undo() {
}
}
//遥控器
class RemoteController{
//存储命令
private List<Command> onCommands ;
private List<Command> offCommands ;
//用于撤销的命令,需要记录执行命令
private Command undoCommand;
//构造器,初始化
public RemoteController() {
onCommands = new ArrayList<>();
offCommands = new ArrayList<>();
//按照遥控器的按钮数,进行初始化,直接加入空命令即可
for (int i=0;i<2;i++){
onCommands.add(i,new NoCommend());
offCommands.add(i,new NoCommend());
}
}
//给按钮设置需要的命令
public void setCommand(int no,Command onCommand,Command offCommand){
onCommands.add(no,onCommand);
offCommands.add(no,offCommand);
}
//如果按下开按钮
public void onButtonPush(int no){
//找到按下的按钮,执行该按钮的执行方法
onCommands.get(no).execute();
//记录这次操作,用于撤销
undoCommand = onCommands.get(no);
}
//如果按下关按钮
public void offButtonPush(int no){
//找到按下的按钮,执行该按钮的执行方法
offCommands.get(no).execute();
//记录这次操作,用于撤销
undoCommand = offCommands.get(no);
}
//如果按下撤销按钮
public void undoButtonPush(int no){
//找到按下的按钮,执行该按钮的执行方法
undoCommand.undo();
}
}
//客户
class Client{
public static void main(String[] args) {
//使用命令模式,通过遥控器,控制灯
//创建电灯对象(真正的操作执行者)
LightReceiver lightReceiver = new LightReceiver();
//创建电灯的开关命令
LightOnCommend lightOnCommend = new LightOnCommend(lightReceiver);
LightOffCommend lightOffCommend = new LightOffCommend(lightReceiver);
//遥控器
RemoteController remoteController = new RemoteController();
//给遥控器设置相关命令
//no = 0 是电灯的开关操作
remoteController.setCommand(0,lightOnCommend,lightOffCommend);
//no = 1 是电视的开关操作(还未实现)
//按灯的按钮
System.out.println("--------按下灯的开按钮-----------");
remoteController.onButtonPush(0);
System.out.println("--------按下灯的关按钮-----------");
remoteController.offButtonPush(0);
System.out.println("---------按下撤销按钮------------");
remoteController.undoButtonPush(0);
}
}
通过命令模式实现了上面的遥控器,每一个遥控器按钮都赋予了一个命令对象,每一次按按钮都会调用该命令对象中的执行操作,且记录下来,可用于撤销操作
模式分析
命令模式实现了请求者与执行者的完全解耦,但是付出的代价是系统中类爆炸
- 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
- 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。
- 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的
- 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
- 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联
对于上述案例,命令模式的执行过程:我们可以用一个时序图表示
命令模式的优缺点
优点
- 降低系统的耦合度,将命令请求者和执行者完全解耦
- 新的命令可以很容易地加入到系统中,如果想要加入一个电视的控制,需要设置一个电视控制执行者、两个具体开关命令类
- 可以比较容易地设计一个命令队列和宏命令(组合命令),毕竟在遥控器(请求者)中通过维护一个集合(ArrayList)存储命令对象,只需要设计一个队列即可
- 可以方便地实现对请求的Undo和Redo,因为命令是一个对象,可以方便的存储记录
缺点
致命缺点:使用命令模式会导致某些系统有过多的具体命令类,类爆炸
就前面设计的一个简单的案例,用了一个命令接口、两个具体命令类、一个命令执行者、一个命令请求者、一个客户端
(简直要命,这么多类。。。)
如果要添加一个电视开关,需要加入一个命令执行者、两个具体命令类
虽然耦合度低了,但是付出了系统资源消耗的代价
适用场景
- 系统需要将请求者和执行者解耦,使得执行者和请求者不直接交互
- 系统需要在不同的时间指定请求、将请求排队和执行请求
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
- 系统需要将一组操作组合在一起,即支持宏命令
很明显,选择是否使用命令模式,看你对系统资源、编程复杂和 耦合度 的抉择
真实使用
- Java语言使用命令模式实现AWT/Swing GUI的委派事件模型 (Delegation Event Model, DEM)
- Spring框架JdbcTemplate应用
- 很多系统都提供了宏命令功能,如UNIX平台下的Shell编程,可以将多条命令封装在一个命令对象中,只需要一条简单的命令即可执行一个命令序列
宏命令
其实我们都应该接触过宏命令:例如鼠标宏等
宏命令包含了一组命令,它充当了具体命令与调用者的双重角色,执行它时将递归调用它所包含的所有命令
这是命令模式和组合模式的使用
有没有注意到,我们前面使用请求调用者的时候使用了两个ArrayList,一个用于保存开命令onCommend,一个用于保存命令OffCommend
如果这个请求调用者本身也是一个命令(实现Commend接口),它的执行方法设置成执行ArrayList中的各个命令的执行,这不就是我们组合模式中的容器对象么
(感觉也有一点外观模式的思想,但是主要侧重与“整体与部分”和执行者与请求者的解耦合)
不清楚组合模式可以看:设计模式(12)结构型模式 - 组合模式
宏命令又称为组合命令,它的结构图:
CompositeCommand中的执行方法execute中是foreach循环调用ArrayList中的命令的execute方法
总结
- 命令模式:将请求封装成一个对象,我们可以使用不同的请求对象对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作
- 命令模式有四个角色:抽象命令类、具体命令类、命令执行者、命令请求者
- 抽象命令类(接口)规范了命令中应该有的方法
- 具体命令类实现了抽象命令者,聚合了执行者,实现了抽象命令类的方法(实际是执行者执行的)
- 执行者:执行命令的操作,具体的业务执行者
- 命令请求者:请求的发送者,通过命令对象执行请求(与执行者解耦合)
- 命令模式优点:将命令请求者和执行者接耦合,扩展性强,可以设计宏命令,可以简单的实现撤回操作
- 命令模式缺点:使系统会有过多的具体命令类
- 命令模式适用于:需要将命令请求者和命令执行者解耦,使得请求者和执行者不直接交互;需要在不同的时间指定请求、将请求排队和执行请求;需要支持命令的撤销操作和恢复操作;需要将一组操作组合在一起,即支持宏命令
- 宏命令是命令模式与组合模式的联合使用,是使得命令请求者也实现抽象命令类(本身也是命令),维护一些集合存储命令对象,它的执行方法是调用集合中命令对象的执行方法