设计模式(十五)----命令模式

概述

命令模式(Command Pattern),将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
该模式是一种对象行为型模式,别名为动作(Action)模式或事务(Transaction)模式。
命令模式是一种数据驱动的设计模式,请求以命令的形式包裹在对象中,并传给调用对象,调用对象寻找可处理该命令的对象,将命令传给该对象,再由该对象执行命令。

软件开发当中,经常需要向某些对象发送请求(调用其中的某个或某些方法),但并不知道请求的具体接收者是谁。这时,希望能用一种松耦合的方式设计软件,使得请求发送者与请求接收者消除彼此间的耦合,使对象间的调用关系更加灵活,可灵活指定请求接收者和被请求的操作(命令)之间的关系。命令模式为此类问题提供了较为完美的解决方案。
命令模式可将请求发送者和请求接收者完全解耦,使两者没有直接的依赖关系,发送请求的对象只需知道如何发送请求,无需知道如何接收请求以及如何对请求进行处理。

命令模式的核心在于引入了命令类,通过命令类来降低请求发送者和请求接收者之间的耦合度,请求发送者只需指定一个命令对象,通过命令对象来调用请求接收者的处理方法。
命令模式主要包含下面几个角色
Command(抽象命令类),一般是抽象类或接口,其中声明用于执行请求的execute等方法,通过这些方法可调用请求接收者的相应处理方法。

ConcreteCommand(具体命令类),抽象命令类的子类,实现了在抽象命令类中声明的方法。它对应具体的接收者对象,将接收者对象绑定在该类(作为该类的一个对象型变量)。实现execute()方法时,调用接收者对象的处理方法。

Invoker(请求发送者),调用者,通过命令对象类执行请求。一个调用者在设计时不需要知道其接收者,因此它只与抽象命令类间存在关系。在程序运行时可将一个具体的命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关处理方法。

Receiver(请求接收者),执行与请求相关的操作,具体实现处理方法。

命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发送命令的操作和执行命令的操作分隔开来。每个命令都是一个操作:请求一方发出请求要求执行某个操作,接收方收到请求,具体执行该操作。命令模式使得请求者不必知道接收者,也不必知道请求是如何被接收者接收、是否会被接收以及接收后对命令具体是如何执行的。
命令模式关键在于引入抽象命令类,请求发送者针对抽象命令类进行编程,只有实现了抽象命令类的具体命令类才与请求接收者相关联(解除了请求发送者和接收者的关联关系)。

实例

cs可是一款经典的射击游戏,记得以前刚接触电脑,鼠标还不会用的时候就开始玩这个游戏,游戏默认的WSAD键分别对应着控制人物前进后退左移和右移,在Esc设置界面可以对键盘按键进行重新绑定,现在我想把前进后退的两个按键颠倒过来,W后退,S前进:)
命令模式实现,在抽象命令类中只定义一个execute()方法,每个具体命令类中引用一个请求接收者类型的变量,不同的具体命令类提供了execute()方法不同的实现,也就是调用不同的接收者去实现命令。
定义抽象命令类

public abstract class Command {
	public abstract void execute();
}

定义具体命令类
前进命令类

public class GoForwardCommand extends Command {
	//引用前进命令执行类类型的对象
	private GoForwardHandler handler;

	public GoForwardCommand() {
		handler = new GoForwardHandler();
	}

	//命令执行方法,调用请求接受者的处理方法
	@Override
	public void execute() {
		handler.goForward();
	}
}

后退命令类

public class GoBackCommand extends Command {
	//引用后退命令执行类类型的对象
	private GoBackHandler handler;

	public GoBackCommand() {
		handler = new GoBackHandler();
	}

	//命令执行方法,调用请求接收者的处理方法
	@Override
	public void execute() {
		handler.goBack();
	}
}

在上面的代码中,具体命令类继承了抽象命令类,它与请求接收者相关联(引用请求接收者类型的变量),实现了在抽象命令类中声明的execute()方法,在实现方法中调用请求接收者的处理方法。
请求发送者
功能按键类

按下键盘上的某个键,发送某个命令

//功能键类
public class FunctionKey {
	//功能键名称
	private String name;
	//引用抽象命令类类型对象作为变量
	private Command command;

	public FunctionKey(String name) {
		this.name = name;
	}

	public String getName() {
		return this.name;
	}

	//为按键注入命令
	public void setCommand(Command command) {
		this.command = command;
	}

	//按下按键,发送请求(比如想要使人物后退按下W键)
	public void onClick() {
		System.out.println("点击" + name);
		//发送命令
		command.execute();
		System.out.println("------------------------------");
	}
}

请求接收者
前进命令执行类

public class GoForwardHandler {
	public void goForward() {
		System.out.println("正在前进!!");
	}
}

后退命令执行类

public class GoBackHandler {
	public void goBack() {
		System.out.println("正在后退!!");
	}
}

请求接收者类具体实现对请求命令的处理,它提供了实现方法,用于执行与请求相关的操作。
按键绑定设置界面类

public class KeyBindingWindow {
	//窗口标题
	private String title;
	//定义一个集合用来存储按键
	private ArrayList<FunctionKey> list = new ArrayList<FunctionKey>();

	public KeyBindingWindow(String title) {
		this.title = title;	
	}

	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	
	//向该设置窗口添加按键设置
	public void addFunctionKey(FunctionKey key) {
		list.add(key);
	}
	
	//显示窗口名称及该窗口中可以设置的按键
	public void display() {
		System.out.println("窗口名称:" + title);
		System.out.println("可以设置的功能按键:");
		for(FunctionKey key : list) {
			System.out.println(key.getName());
		}
	}
}

为提高系统灵活性和扩展性,将具体命令类的路径存储在配置文件中,并通过工具类XMLUtil来读取配置文件并通过反射生成对象
XMLUtil类代码如下

//该类需要的jar包有org.dom4j包和jaxen包
public class XMLUtil {
	//从配置文件中提取具体类类名,并返回一个实例对象
	public static Object getBean(String name) throws Exception {
		SAXReader reader = new SAXReader();
		String path = XMLUtil.class.getClassLoader().getResource("com/env/command/config.xml").getPath();
		Document document = reader.read(new File(path));
		//类名
		String cName = null;
		if(name.equals("goForwardCommand")) {
			cName = document.selectSingleNode("/config/goForwardCommand").getText();
		} else if (name.equals("goBackCommand")) {
			cName = document.selectSingleNode("/config/goBackCommand").getText();
		}
		//通过类名生成实例对象并将其返回
		Class<?> c = Class.forName(cName);
		Object object = c.getInstance();
		return object;
	}
}

配置文件代码如下

<?xml version="1.0" encoding="UTF-8"?>
<config>
	<goForwardCommand>com.env.command.GoForwardCommand</goForwardCommand>
	<goBackCommand>com.env.command.GoBackCommand</goBackCommand>
</config>

客户端测试类

public class CommandPatternDemo {
	public static void main(String[] args) {
		//新建按键绑定设置窗口
		KeyBindingWindow window = new KeyBindingWindow("按键绑定设置窗口");
		//前进键
		FunctionKey goForwardKey = new FunctionKey("前进键(S)");
		//后退键
		FunctionKey goBackKey = new FunctionKey("后退键(W)");

		//通过读取配置文件使用反射生成命令对象
		Command command1 = (Command)XMLUtil.getBean("goForwardCommand");
		Command command2 = (Command)XMLUtil.getBean("goBackCommand");

		//将命令对象注入功能键(为按键绑定命令)
		goForwardKey.setCommand(command1);
		goBackKey.setCommand(command2);

		//在按键绑定设置窗口中添加前进和后退按键设置
		window.addFunctionKey(goForwardKey);
		window.addFunctionKey(goBackKey);

		//调用显示窗口名称和可以设置的按键方法
		window.display();
		System.out.println("============================");

		//调用功能按键的业务方法(按下按键)
		goForwardKey.onClick();
		goBackKey.onClick();
	}
}

输出结果
在这里插入图片描述

倘若我想把前进后退键改回来,按W键前进,按S键后退,只需在客户端中修改命令对象注入功能键的代码即可

goForwardKey.setCommand(command2);
goBackKey.setCommand(command1);

这时,我又想修改一个按键动作之间的绑定,默认的按Ctrl下蹲我用不习惯,通常要改成Shift键
只需如下几步
新建一个具体命令类下蹲命令类

public class SquatCommand extends Command {
	//引用下蹲命令执行类类型的对象
	private SquatHandler handler;

	public SquatCommand(SquatHandler handler) {
		this.handler = handler;
	}
	
	//命令执行方法,调用请求接收者的处理方法
	@Override
	public void execute() {
		handler.squat();
	}
}

新建一个请求接收者下蹲命令执行类

public class SquatHandler {
	public void squat() {
		System.out.println("已蹲下!!");
	}
}

config.xml配置文件中新建一个节点

<squatCommand>com.env.command.SquatCommand</squatCommand>

XMLUtil类中新加一个else if判断

else if (name.equals("squatCommand")) {
	cName = document.selectSingleNode("/config/squatCommand").getText();
}

客户端测试类添加如下几句代码

//新建一个功能键下蹲键
FunctionKey squatKey = new FunctionKey("下蹲键(Shift)");
//通过读取配置文件利用反射生成命令对象
Command command3 = (Command)XMLUtil.getBean("squatCommand");
//将命令绑定到按键上
squatKey.setCommand(command3);
//在按键绑定设置窗口添加下蹲按键设置
window.addFunctionKey(squatKey);
//按下下蹲键
squatKey.onClick();

执行测试方法,打印结果变成
在这里插入图片描述
在此过程中,每个具体命令类对应一个接收者,通过向请求发送者(键盘按键)注入不同的具体命令对象,使得同一个发送者可对应不同的接收者,从而实现将一个请求封装为一个对象,用不同的请求对客户进行参数化。
客户端只需将具体命令对象作为参数注入请求发送者(为按键绑定命令),无需直接操作请求接收者。

总结

优点
降低系统耦合度,将请求发送者和接收者解耦,调用者实现功能时只需调用Command抽象类的execute()方法即可,不需知道具体是哪个接收者执行;具体命令类可像其他对象一样被操纵和扩展;增加一个新的具体命令类无需修改已有的代码,符合"开闭原则"。
缺点
使用命令模式可能会导致系统存在过多的具体命令类,造成资源占用。

使用场景
系统需将请求调用者和接收者解耦,使得两者不直接交互;系统需在不同时间指定请求,将请求进行排队(如线程池+工作队列)和执行请求;系统需支持命令的撤销(Undo)操作和恢复(Redo)操作;系统需将一组操作结合在一起,也就是支持宏命令。

更多用途
队列请求:将命令排成一个队列打包,逐个调用execute()方法,如线程池的任务队列,线程不关心任务队列中是读取IO还是进行计算,亦或是其他操作,只取出命令后执行,接着进行下一个。
日志请求:某些应用需将所有动作记录在日志中,然后在系统死机或是出现其他状况后,重新调用这些动作恢复到之前的状态,如数据库事务。
撤销操作:命令模式中,可通过调用一个命令对象的execute()方法实现对请求的处理,若需撤销请求,可通过在命令类中增加一个逆向操作来实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值