第十五章 命令模式 - 请求发送者和接收者解耦
一、命令模式
命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式是一种对象行为型模式,又称为动作模式或事务模式。
命令模式中包含的几个角色:
- Command - 抽象命令类
- 声明了用于执行请求的 execute() 等方法,通过这些方法可以调用请求接收者的相关操作
- ConcreteCommand - 具体命令类
- 实现抽象命令类中声明的方法
- 对应具体的接收者对象,将接收者对象的动作绑定其中
- 在实现execute() 方法时,调用接收者对象的相关操作
- Invoker - 调用者
- 请求的发送者,通过命令对象来执行请求
- 调用者并不需要在设计时确定其接收者,因此它只与抽象命令类保持关联
- 在程序运行时,注入一个具体命令对象,再调用具体命令对象的execute() 方法
- 从而间接调用请求接收者的相关操作
- Receiver - 接收者
- 接收者执行与请求相关的操作,它具体实现对请求的业务处理
命令模式的本质是对请求进行封装,一个请求对应一个命令,将发出命令的责任和执行命令的责任分隔开。
二、命令模式的关键 - 抽象命令类
请求发送者只需要针对抽象命令类编程即可。
// 抽象命令类
abstract class Command {
// 声明公共的执行方法
public abstract void execute();
}
// 请求发送者 - 调用者
class Invoker {
// 请求发送者只需要针对 Command 编程即可,具体命令类在运行时指定
private Command command;
// 通过构造函数或者setter方法注入Command
public Invoker(Command command){
this.command = command;
}
// 用于调用命令类的 execute 方法
public void call(){
command.execute();
}
}
// 具体命令类
class ConcreteCommand extends Command {
// 维持一个对请求接收者对象的引用
private Receiver receiver; // 如果需要多个接收者,可以将该对象改为List等集合对象
public void execute(){
// 调用请求接收者的业务处理方法
receiver.action();
}
}
class Receiver {
public void action(){
// 具体操作
}
}
三、完整实现
// 功能键设计窗口类
class FBSettingWindow{
// 窗口标题
private String title;
// List集合,存储所有的功能按键
private List<FunctionButton> functionButtons = new ArrayList<>();
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 (FunctionButton functionButton : functionButtons) {
System.out.println(functionButton.getName());
}
}
}
// 功能按键 - 请求发送者
class FunctionButton {
// 按键名称
private String name;
// 维持一个抽象命令的引用
private Command command;
public String getName() {
return name;
}
public Command getCommand() {
return command;
}
public void setName(String name) {
this.name = name;
}
public void setCommand(Command command) {
this.command = command;
}
public void onClick(){
System.out.println("点击按键:");
command.execute();
}
}
// 抽象命令类
abstract class Command {
public abstract void execute();
}
// 帮助命令类 - 具体命令类
class HelpCommand extends Command {
private HelpHandler helpHandler;
public HelpCommand() {
helpHandler = new HelpHandler();
}
@Override
public void execute() {
helpHandler.action();
}
}
// 最小化命令类 - 具体命令类
class MinimizeCommand extends Command {
private MinimizeHandler handler;
public MinimizeCommand() {
handler = new MinimizeHandler();
}
@Override
public void execute() {
handler.action();
}
}
// 帮助处理类 - 请求接收者
class HelpHandler {
public void action(){
System.out.println("显示文档");
}
}
// 最小化处理类 - 请求接收者
class MinimizeHandler {
public void action (){
System.out.println("最小化!");
}
}
public class CommandPattern {
public static void main(String[] args) {
final FBSettingWindow window = new FBSettingWindow();
window.setTitle("功能键设置");
final FunctionButton button1 = new FunctionButton();
button1.setName("按钮1");
final FunctionButton button2 = new FunctionButton();
button2.setName("按钮2");
final HelpCommand helpCommand = new HelpCommand();
final MinimizeCommand minimizeCommand = new MinimizeCommand();
button1.setCommand(helpCommand);
button2.setCommand(minimizeCommand);
window.addFunctionButton(button1);
window.addFunctionButton(button2);
window.display();
button1.onClick();
button2.onClick();
}
}
如果需要增加一个新的命令,只需要增加一个具体命令类,将命令类与具体的处理类进行关联,并注入到某个功能键即可。原有代码无需修改,符合“开闭原则”。
四、命令队列
当我们点击一个按钮后,需要执行多次命令操作,且命令是可重用的。此时我们可以使用“命令队列”的方式来设计,此时,请求发送者不再维护单独的一个Command,而是一个CommandQueue,存储多个命令对象:
class FunctionButton {
private String name;
private CommandQueue commandQueue;
}
class CommandQueue {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command command){
commands.add(command);
}
public void removeCommand(Command command){
commands.remove(command);
}
public void display(){
for(Command cd:commands){
cd.execute();
}
}
}
命令队列类似于“批处理”的概念,可以对一组对象(命令)进行批量操作。如果请求接收者没有严格的先后次序,还可以通过多线程技术来并发调用对象的execute方法,从而提高程序执行效率。
五、撤销操作的实现
在命令模式中,可以通过调用命令对象的 execute() 方法来实现对请求的处理,如果需要撤销操作,可以在命令类中增加一个逆向操作来实现。
也可以通过保存对象的历史状态来实现撤销,如“备忘录模式”。
我们可以在具体的Handler 中,保存上次执行时的状态,并提供一个方法以便恢复至保存的状态。
class AdderHandler{
private int number;
public int add(int i){
number += i;
return number;
}
}
class AdderCommand extends Command {
private AdderHandler handler;
private int history;
public AdderCommand(AdderHandler handler){
this.handler = handler;
}
@Override
public void add(int value){
int result = handler.add(value);
history = value;
System.out.print("加法结果为:"+result);
}
@Override
public void undo(){
// 使用保存的状态实现撤销操作
int result = handler.add(-history);
System.out.print("撤销后的结果为:"+result);
}
}
注,该撤销操作只能执行一次,如果需要执行多次,可以使用集合来保存
histroy
六、宏命令
宏命令(Macro Command)又称为组合命令,是组合模式和命令联合的产物。
宏命令是一个具体的命令类,拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。
当调用宏命令的 execute 方法时,会递归调用它的每个成员命令的 execute 方法,成员既可以是一个简单命令,也可以是宏命令。
七、总结
命令模式是一种 使用频率非常高
的设计模式,可以将请求发送者和接收者解耦。发送者通过命令对象间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。
优点
- 降低系统耦合,请求发送者与接收者不直接引用,相互独立
- 方便扩展,增加新的命令不会影响其他类,无需修改系统代码,符合“开闭原则”
- 可以比较简单的设计一个命令队列或组合命令(宏命令)
- 为请求的撤销、恢复操作提供了一种设计和实现方案
缺点
- 可能会导致系统中存在过多的命令类,因为每个接收者都需要设计一个具体命令类来调用
适用场景
- 系统需要请求发送者与接收者解耦,彼此互不影响,互不相知
- 系统需要使用一组命令或者宏命令
- 系统需要支持撤销、恢复操作
- 系统需要在不同的时间指定请求、将请求排队和执行请求
八、练习
命令模式
Sunny软件公司欲开发一个基于Windows平台的公告板系统。
该系统提供了一个主菜单(Menu),
在主菜单中包含了一些菜单项(MenuItem),
可以通过Menu类的addMenuItem()方法增加菜单项。
菜单项的主要方法是click(),
每一个菜单项包含一个抽象命令类,
具体命令类包括OpenCommand(打开命令),CreateCommand(新建命令),EditCommand(编辑命令)等,
命令类具有一个execute()方法,
用于调用公告板系统界面类(BoardScreen)的open()、create()、edit()等方法。
试使用命令模式设计该系统,
以便降低MenuItem类与BoardScreen类之间的耦合度
/**
* 命令模式
* Sunny软件公司欲开发一个基于Windows平台的公告板系统。
* 该系统提供了一个主菜单(Menu),
* 在主菜单中包含了一些菜单项(MenuItem),
* 可以通过Menu类的addMenuItem()方法增加菜单项。
* 菜单项的主要方法是click(),
* 每一个菜单项包含一个抽象命令类,
* 具体命令类包括OpenCommand(打开命令),CreateCommand(新建命令),EditCommand(编辑命令)等,
* 命令类具有一个execute()方法,
* 用于调用公告板系统界面类(BoardScreen)的open()、create()、edit()等方法。
* 试使用命令模式设计该系统,
* 以便降低MenuItem类与BoardScreen类之间的耦合度
*/
// 主菜单 - 类似于之前的窗口界面,包含多个按钮(菜单项)
class Menu{
private List<MenuItem> menuItems = new ArrayList<>();
public void addItem(MenuItem item){
menuItems.add(item);
}
public void display(){
System.out.println("------显示菜单:-----");
for (MenuItem menuItem : menuItems) {
System.out.println(menuItem.getName());
}
}
}
// 菜单项 - 请求发送者
class MenuItem {
private String name;
private Command command;
public MenuItem(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setCommand(Command command){
this.command = command;
}
public void create(){
command.create();
}
public void edit(){
command.edit();
}
public void open(){
command.open();
}
}
// 抽象命令类
abstract class Command {
abstract void create();
abstract void edit();
abstract void open();
}
// 宏命令 - 一组命令的集合,宏命令一般不直接操作接收者,而是通过集合中的对象属性来操作
class MacroCommand extends Command {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command command){
this.commands.add(command);
}
@Override
void create() {
for (Command command : commands) {
command.create();
}
}
@Override
void edit() {
for (Command command : commands) {
command.edit();
}
}
@Override
void open() {
for (Command command : commands) {
command.open();
}
}
}
// 具体命令类
class BoardScreenCommand extends Command{
private BoardScreenHandler handler;
public BoardScreenCommand(){
handler = new BoardScreenHandler();
}
@Override
void create() {
handler.create();
}
@Override
void edit() {
handler.edit();
}
@Override
void open() {
handler.open();
}
}
// 请求接收者,也就是具体的处理类
class BoardScreenHandler {
public void open(){
System.out.println("打开公告板");
}
public void create(){
System.out.println("创建公告板");
}
public void edit(){
System.out.println("修改公告板");
}
}
public class CommandPattern {
public static void main(String[] args) {
final Menu menu = new Menu();
final MenuItem menuItem = new MenuItem("公告板管理");
final MenuItem menuItem1 = new MenuItem("高噶");
final Command command = new BoardScreenCommand();
final MacroCommand command1 = new MacroCommand();
command1.addCommand(command);
command1.addCommand(command);
menuItem.setCommand(command);
menuItem1.setCommand(command1);
menu.addItem(menuItem);
menu.addItem(menuItem1);
menu.display();
menuItem.create();
menuItem.open();
System.out.println("----宏命令执行------");
menuItem1.create();
menuItem1.edit();
menuItem1.open();
}
}