场景
欲开发一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。
用户可以将功能键和相应功能绑定在一起,还可以根据需要来修改功能键的设置,而且系统在未来可能还会增加一些新的功能或功能键。
如果不使用命令模式,可能这样实现。
功能键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler充当请求的接收者,在发送者FunctionButton的onClick()
方法中将调用接收者HelpHandler的display()方法。
FunctionButton:
public class FunctionButton {
//帮助文档处理类,请求接收者
private HelpHandler helperHandler;
public void onClick(){
helperHandler = new HelpHandler();
//显示帮助文档
helperHandler.display();
}
}
HelpHandler:
public class HelpHandler {
public void display(){
System.out.println("显示帮助文档");
}
}
以上存在的问题:
(1) 由于请求发送者和请求接收者之间存在方法的直接调用,耦合度很高,更换请求接收者必须修改发送者的源代码,
如果需要将请求接收者HelpHandler改为WindowHanlder(窗口处理类),则需要修改FunctionButton的源代码,违背了“开闭原则”。
(2) FunctionButton类在设计和实现时功能已被固定,如果增加一个新的请求接收者,如果不修改原有的FunctionButton类,
则必须增加一个新的与FunctionButton功能类似的类,这将导致系统中类的个数急剧增加。
由于请求接收者HelpHandler、WindowHanlder等类之间可能不存在任何关系,它们没有共同的抽象层,
因此也很难依据“依赖倒转原则”来设计FunctionButton。
(3) 用户无法按照自己的需要来设置某个功能键的功能,一个功能键类的功能一旦固定,在不修改源代码的情况下无法更换其功能,
系统缺乏灵活性。
命令模式概述
在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,
也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者
能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。
命令模式为此类问题提供了一个较为完美的解决方案。
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,
而不必知道如何完成请求。
命令模式(Command Pattern):
将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
命令模式结构图
命令模式包含的各角色:
Command(抽象命令类):
抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,
通过这些方法可以调用请求接收者的相关操作。
ConcreteCommand(具体命令类):
具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,
将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
Invoker(调用者):
调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,
因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,
再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
Receiver(接收者):
接收者执行与请求相关的操作,它具体实现对请求的业务处理。命令模式的本质是对请求进行封装,
一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:
请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。
命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,
更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
注:
博客:
霸道流氓气质_C#,架构之路,SpringBoot-CSDN博客
实现
使用命令模式实现以上用户自定义功能键。
1、新建抽象命令类
//抽象命令类
abstract class Command {
public abstract void execute();
}
2、新建具体命令类:帮助命令类
//帮助命令类:具体命令类
public class HelpCommand extends Command{
//维持对请求接收者的引用
private HelpHandler helpHandler;
public HelpCommand(){
helpHandler = new HelpHandler();
}
//命令执行方法,将调用请求接收者的业务方法
public void execute() {
helpHandler.display();
}
}
其中维持着对请求接收者的引用。
3、新建请求接收者,帮助文档的处理类
//帮助文档处理类:请求接受者
public class HelpHandler {
public void display(){
System.out.println("显示帮助文档");
}
}
4、同理新建具体命令类:最小化命令类
//最小化命令类:具体命令类
public class MinimizeCommand extends Command{
//维持对请求接收者的引用
private WindowHandler windowHandler;
public MinimizeCommand(){
windowHandler = new WindowHandler();
}
//命令执行方法,将调用请求接收者的业务方法
public void execute() {
windowHandler.minimize();
}
}
其中维持着对请求接收者最小化窗口处理类的引用
5、新建最小化窗口处理类
//窗口处理类:请求接收者
public class WindowHandler {
public void minimize(){
System.out.println("将窗口最小化");
}
}
6、新建请求发送者:功能键类
//功能键类:请求发送者
public class FunctionButton {
//功能键名称
private String name;
//维持一个抽象命令对象的引用
private Command command;
public FunctionButton(String name) {
this.name = name;
}
public String getName(){
return this.name;
}
//为功能键注入命令
public void setCommand(Command command){
this.command = command;
}
public void onClick(){
System.out.println("点击功能键:");
command.execute();
}
}
7、新建功能键设置窗口类
import java.util.ArrayList;
//功能键设置窗口类
public class FBSettingWindow {
//窗口标题
private String title;
//定义一个ArrayList来存储所有功能键
private ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();
public FBSettingWindow(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public void addFunctionButton(FunctionButton fb){
functionButtons.add(fb);
}
public void removeFunctionButton(FunctionButton fb){
functionButtons.remove(fb);
}
//显示窗口及功能键
public void display(){
System.out.println("显示窗口:"+this.title);
System.out.println("显示功能键:");
for (Object obj : functionButtons) {
System.out.println(((FunctionButton)obj).getName());
}
System.out.println("-------------------------");
}
}
8、客户端调用方式
public class Client {
public static void main(String[] args) {
FBSettingWindow fbsw = new FBSettingWindow("功能键设置");
FunctionButton fb1,fb2;
fb1 = new FunctionButton("功能键1");
fb2 = new FunctionButton("功能键2");
Command command1,command2;
//通过读取配置文件或其它方式生成具体命令对象
command1 = new HelpCommand();
command2 = new MinimizeCommand();
//将命令对象注入功能键
fb1.setCommand(command1);
fb2.setCommand(command2);
fbsw.addFunctionButton(fb1);
fbsw.addFunctionButton(fb2);
fbsw.display();
//调用功能键的业务方法
fb1.onClick();
fb2.onClick();
}
}
9、总结
如果需要修改功能键的功能,例如某个功能键可以实现“自动截屏”,只需要对应增加一个新的具体命令类,
在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入到某个功能键即可,
原有代码无须修改,符合“开闭原则”。在此过程中,每一个具体命令类对应一个请求的处理者(接收者),
通过向请求发送者注入不同的具体命令对象可以使得相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,
用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者。
命令模式的主要优点:
(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,
相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
(2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,
无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
(3) 可以比较容易地设计一个命令队列或宏命令(组合命令)。
(4) 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
命令模式的主要缺点如下:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,
因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
适用场景
在以下情况下可以考虑使用命令模式:
(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,
接收者也无须关心何时被调用。
(2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,
换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,
而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
(3) 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
(4) 系统需要将一组操作组合在一起形成宏命令。