行为模式之命令模式

模式定义

  命令模式(Command Pattern):将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

模式动机

  在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。

  命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。

模式结构

  • Command: 抽象命令类
  • ConcreteCommand: 具体命令类
  • Invoker: 调用者
  • Receiver: 接收者
  • Client:客户类
    image.png

应用实例

  1. 电视机遥控器
      电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。显然,电视机遥控器就是一个典型的命令模式应用实例。
    image.png

代码分析

  模拟了一个网络通信的案例。在网络中服务器每次只能处理一个请求, 当请求过多,服务器会把请求放入任务队列逐一取出处理。服务器与客户端在建立好 SOCKET 连接后,会发送一系列的协议用于告诉服务器请求的是那些服务,HandleClientProtocol 类内部封装了一些列用于处理客户端不同协议的不同处理方法,例如添加金币、钻石等。 但是服务器如何将这些函数封装进入任务队列呢?函数指针?可以选择将这些请求参数化,即封装成一个对象,每个对象都有一个抽象的接口 AbstractCommand,针对与每一个不同的请求,都将协议处理的总方法HandleClientProtocol作为一个成员参数,该请求调用需要调用的请求即可。同时将该请求的这个具体类放入任务队列中即可。

//请求处理方法
class HandleClientProtocol {
public:
	void AddMoney() { cout << "给玩家增加金币!" << endl; }
	void AddDiamond() { cout << "给玩家增加钻石!" << endl; }
	void AddEipment() { cout << "给玩家增加装备!" << endl; }
	void AddLevel() { cout << "给玩家升级!" << endl; }
};

//命令接口
class AbstractCommand {
public:
	virtual void handle() = 0;
};

//针对每一个请求,具体化对象,封装成一个类,统一任务接口
class AddMoneyCommand :public AbstractCommand {
public:
	AddMoneyCommand(HandleClientProtocol* protocol) :pProtocol(protocol) {}
	virtual void handle() { pProtocol->AddMoney(); }
private:
	HandleClientProtocol* pProtocol;
};

class AddDiamondCommand :public AbstractCommand {
public:
	AddDiamondCommand(HandleClientProtocol* protocol) :pProtocol(protocol) {}
	virtual void handle() { pProtocol->AddDiamond(); }
private:
	HandleClientProtocol* pProtocol;
};

class AddEipmentCommand :public AbstractCommand {
public:
	AddEipmentCommand(HandleClientProtocol* protocol) :pProtocol(protocol) {}
	virtual void handle() { pProtocol->AddEipment(); }
private:
	HandleClientProtocol* pProtocol;
};

class AddLevelCommand :public AbstractCommand {
public:
	AddLevelCommand(HandleClientProtocol* protocol) :pProtocol(protocol) {}
	virtual void handle() { pProtocol->AddLevel(); }
private:
	HandleClientProtocol* pProtocol;
};

//模拟服务器
class Server {
public:
	void addRequest(AbstractCommand* command) { mCommands.push(command); } //添加任务
	void StartHandle() {  //处理任务
		while (!mCommands.empty()) {
			AbstractCommand* nowCommand = mCommands.front();
			mCommands.pop();
			nowCommand->handle();
			delete nowCommand;
			nowCommand = nullptr;
			Sleep(2000); //模拟服务器处理,每两秒处理一个
		}
	}
	~Server() {
		while (!mCommands.empty()) {
			AbstractCommand* nowCommand = mCommands.front();
			mCommands.pop();
			delete nowCommand;
			nowCommand = nullptr;
		}
	}
private:
	queue<AbstractCommand*> mCommands; //任务调度队列
};

void test1() {
	Server* server = new Server;
	HandleClientProtocol* protocol = new HandleClientProtocol;
	AbstractCommand *addMoney = new AddMoneyCommand(protocol);
	AbstractCommand* addDiamond = new AddDiamondCommand(protocol);
	AbstractCommand* addeipment = new AddEipmentCommand(protocol);
	AbstractCommand* addlevel = new AddLevelCommand(protocol);

	server->addRequest(addMoney);
	server->addRequest(addDiamond);
	server->addRequest(addeipment);
	server->addRequest(addlevel);

	server->StartHandle();
}

  针对命令模式的定义,需要对任务有撤销操作,目前想到的方法是再添加一个栈,队列一边弹,栈一边入,找到以后把栈里面的元素倒回去,时间复杂度很高,比较简单就不实现了,肯定有更好的方法。

命令模式的优缺点

优点

  • 降低系统的耦合度
  • 新的命令可以很容易地加入到系统中
  • 可以比较容易地设计一个命令队列和宏命令(组合命令)
  • 可以方便地实现对请求的 Undo(撤销) 和 Redo(恢复)。

缺点

  • 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用

适用环境

  1. 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
  2. 系统需要在不同的时间指定请求、将请求排队和执行请求
  3. 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
  4. 系统需要将一组操作组合在一起,即支持宏命令

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://neo00.top/archives/行为模式之命令模式