一.简介
我们正常写程序的时候,要实现一个操作,都是直接调用相应对象的函数进行操作,这样,行为的请求者和行为的实现者就是一种紧耦合的关系。举个简单的例子,比如我们设计一个游戏,按A键是攻击,按B键是放大招,那么我们可以直接将这按键的功能写在相应按键的对应函数上,但是这并不是好的设计,因为我们操作的人物和输入系统发生了直接的耦合,这两者并没有什么直接的关系。比如我们要进行修改,更换键位,那么修改的内容可能就比较多了。更好的方式是将玩家的输入都看成命令,攻击命令,技能命令等等,玩家按下哪个键,就发出相应的命令。而如果我们需要更改键位,只需要修改命令和键位之间的映射表就可以了。这就是命令模式的一种简单的体现。不过,命令模式真正的好处并不仅限于此,使用命令模式,我们可以很容易的实现一套命令队列,将用户的请求排队处理,还可以将命令存储为日志,更进一步,我们还可以根据命令很容易的进行撤销重做的操作。可见,命令模式是一种灰常有用的模式!
命令模式的UML图如下:
二.命令模式的例子
就拿上面的例子,我们先看一个没有用命令模式的游戏,输入系统直接控制人物:
// C++Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
//玩家类
class Character
{
public:
void Attack()
{
cout<<"攻击!"<<endl;
}
void Skill()
{
cout<<"技能!"<<endl;
}
};
//输入系统
class InPutSystem
{
private:
Character* m_pCharacter;
public:
void Invoke(Character* character)
{
m_pCharacter = character;
}
void Option(string option)
{
if (option == "attack")
m_pCharacter->Attack();
else if(option == "skill")
m_pCharacter->Skill();
else
cout<<"error"<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
//输入系统直接控制玩家
Character* character = new Character();
InPutSystem* input = new InPutSystem();
input->Invoke(character);
input->Option("attack");
input->Option("skill");
system("pause");
return 0;
}
结果:
攻击!
技能!
请按任意键继续. . .
技能!
请按任意键继续. . .
我们的确是实现了功能,但是这个代码看起来总是有点儿别扭,输入系统直接控制人物,总赶脚耦合度忒高了点儿。于是我们进行一下简单的修改,增加一个命令中间层。
简单更改后的例子:
// C++Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
//玩家类
class Character
{
public:
void Attack()
{
cout<<"攻击!"<<endl;
}
void Skill()
{
cout<<"技能!"<<endl;
}
};
//Command基类
class Command
{
protected:
Character* m_receiver;
public:
void Invoke(Character* character)
{
m_receiver = character;
}
virtual void Execute() = 0;
};
//攻击命令
class AttackCommand : public Command
{
void Execute()
{
m_receiver->Attack();
}
};
//技能命令
class SkillCommand : public Command
{
void Execute()
{
m_receiver->Skill();
}
};
//输入系统
class InPutSystem
{
protected:
Command* m_command;
public:
void GetCommand(Command* command)
{
m_command = command;
}
void Notify()
{
m_command->Execute();
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Character* character = new Character();
InPutSystem* input = new InPutSystem();
Command* attack = new AttackCommand();
Command* skill = new SkillCommand();
attack->Invoke(character);
skill->Invoke(character);
input->GetCommand(attack);
input->Notify();
input->GetCommand(skill);
input->Notify();
system("pause");
return 0;
}
结果:
攻击!
技能!
请按任意键继续. . .
技能!
请按任意键继续. . .
进行修改之后,虽然整个系统变得复杂了,但是这样做还是有好处的。最直接的体现就是输入系统本身和人物解耦了,我们增加了一个中间层,就是Command,两边任意一边修改都不会过于影响对方。
三.命令模式的应用
命令模式真正有用的地方在于
可以很容易的记录我们所有的操作,实现redo和undo操作,还可以实现命令队列,将请求和执行在时间上分离!
// C++Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include "list"
#include <string>
using namespace std;
//玩家类
class Character
{
public:
void Attack()
{
cout<<"攻击!"<<endl;
}
void Skill()
{
cout<<"技能!"<<endl;
}
};
//Command基类
class Command
{
protected:
Character* m_receiver;
public:
void Invoke(Character* character)
{
m_receiver = character;
}
virtual void Execute() = 0;
};
//攻击命令
class AttackCommand : public Command
{
void Execute()
{
m_receiver->Attack();
}
};
//技能命令
class SkillCommand : public Command
{
void Execute()
{
m_receiver->Skill();
}
};
//输入系统
class InPutSystem
{
protected:
//命令队列
typedef std::list<Command*> CommandList;
CommandList m_CommandList;
public:
void GetCommand(Command* command)
{
m_CommandList.push_back(command);
}
void RemoveCommand(Command* command)
{
m_CommandList.remove(command);
}
void Notify()
{
for (CommandList::iterator it = m_CommandList.begin(); it != m_CommandList.end(); it++)
{
(*it)->Execute();
}
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Character* character = new Character();
InPutSystem* input = new InPutSystem();
Command* attack = new AttackCommand();
Command* skill = new SkillCommand();
attack->Invoke(character);
skill->Invoke(character);
//向命令队列中添加命令
input->GetCommand(attack);
input->GetCommand(skill);
input->GetCommand(skill);
//删除命令
input->RemoveCommand(attack);
//真正执行命令
input->Notify();
system("pause");
return 0;
}
结果:
技能!
技能!
请按任意键继续. . .
技能!
请按任意键继续. . .
这里,我们在输入系统中建立了一个命令队列,这样,我们在输入的时候就可以尽情的向队列里面放输入命令信息,而不用担心不会响应的问题。而这时候我们可以对命令队列进行相关处理,比如我们可以在插入命令的时候加入判定条件,将命令按照优先级排序,或者我们可以增加一个时间判定,甚至可以让这个命令在队列里面等上一会儿,实现定时发送的功能。等到我们需要执行的时候,调用Notify就可以执行所有的命令。
当然,我们也能够将命令添加一些标识信息,每次执行一条命令,我们都可以记录下来,然后根据标识重建命令,就可以完全重现系统之前的状态!
我们也可以实时的保存命令,将命令放在一个链表中,向前移动指针,进行命令的相反操作,就可以实现回退操作。
更进一步地分析,一个命令也可以有多个接收者,这样就有点儿类似观察者模式,我们可以通过一个命令,通知多个对象执行相关操作。
四.命令模式总结
1.
命令模式的优点
(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦, 调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
(2) 很容易扩展,如果我们想新加入一个命令,只需要继承Command基类就可以添加一个新的命令,修改很少。 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期。
(3) 很容易实现一个命令队列,可以将命令分类,排序,批处理。
(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦, 调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
(2) 很容易扩展,如果我们想新加入一个命令,只需要继承Command基类就可以添加一个新的命令,修改很少。 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期。
(3) 很容易实现一个命令队列,可以将命令分类,排序,批处理。
(4)较容易实现撤销和恢复操作,并可以根据日志重现之前的操作。
2. 命令模式的缺点
使用命令模式可能会有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提
供大量的具体命令类,这将影响命令模式的使用。