定义:
将一个请求封装为一个对象或者操作封装到一个对象中,从而使你可用不同的请求对客户进行参数化;对请求进行排队或记录请求日志,以及支持可撤销的操作。
是对命令的封装,把发出命令的责任和执行命令的责任分割开,委派给不同的对象,发命令者无需知道命令是怎么执行的(那是接受者执行的),而且如果命令需要修改、扩展,不用修改发命令者的代码,从而实现解耦,
比如我们去饭店吃饭,我们在点餐时,我们就是客户端,服务员是发送命令者,厨师是接受命令者,点完菜后,服务员将我们的菜单(命令集合,做某道菜就是一个命令)给厨师,具体这个菜怎么做,我们不需要知道。菜单就是一个命令队列,某道菜的食材没了,做不了了,厨师执行否决操作,某道菜不想吃了,让厨师别做了(vedo),执行撤销(Undo)操作,后悔又想吃了,执行再点菜的重做(redo)操作
类图:
客户角色(Client):创建一个具体命令对象并确定其接受者
命令角色(Command):声明了一个给所有具体命令类的抽象接口,这是一个抽象角色,通常由一个接口或抽象类实现
具体命令角色(ConcreteCommand):定义一个接受者和行为之间的弱耦合;实现execute()方法,负责调用接受者的相应操作
请求者角色(Invoker):负责调用命令对象执行请求
接受者角色(Receiver):负责具体实施和执行一个请求
public interface Commannd{
void execute();
}
public class ConcreteCommand implements Command{
private Receiver receiver;
public ConcreteCommand(Receiver receiver){
this.receiver=receiver;
}
public void exrcute(){
receiver.action();
}
}
public class Receiver{
public void action(){
/*
*根据接受到的命令执行相应操作
*/
}
}
public class Invoker{
private Command commad;
public Invoker(Command command){
this.command=command;
}
public void action(){
command.execute();
}
}
public class Cliet{
public static void main(String[] args){
Receiver receiver=new Receiver();
Command command=new ConcreteCommand(receiver);
invoker.action();
}
}
请求者不一定只是转发命令,也可以有部分实现操作(可以不用接受者做的事情)
文档处理器,有一个菜单栏,有如下菜单项:粘贴、保存用户点击这两个菜单项时:
客户角色:使用者
具体命令角色:粘贴、保存
请求中角色:菜单项
接受者角色:文档
同时该软件有一个历史记录器
(撤销、重做的实现可以理解为一个历史操作队列)
由于粘贴、保存命令大致一样,所以只列出粘贴功能的code
//命令角色
public interface Command {
public void Execute();
public void Unexecute();
public Boolean Reversible();//判断能不能重做,(有些命令是不能重做的)
public Boolean Vedo();//判断能不能做(否决),(假如给设定某一条见,未达到该条件或软件某一处出错的话,就否决)
}
//具体命令角色 粘贴
public class PasteCommand implements Command{
private Doc doc;//文档
public PasteCommand(Doc doc){
this.doc=doc;
}
public void Execute() {
doc.paste();
}
//Undo
public void Unexecute() {
//具体实现:可以保存执行命令前的状态,然后进行恢复,或者进行执行命令的逆操作等等
}
public Boolean Reversible() {
Boolean can=false;
/*
* code
*/
return can;
}
}
//历史记录队列,实现撤销、重做 应该将其实现为单例,所有命令共享一个历史记录队列, 这里为了简化就没实现
public class CommandManager{
private List<Command> undoCmds = new ArrayList();//实现撤销,每执行一个命令,将其添加到undo列表中
private List<Command> redoCmds = new ArrayList();//实现重做,每撤销一个命令,将其添加到redo列表中
public void addUndoCommands(Command cmd) {
if(cmd.Reversible()){//如果命令可撤销
undoCmds.add(cmd);
}
}
public void undo(){
if(undoCmds.size()<=0) return;
Command cmd=undoCmds.get(undoCmds.size()-1);
cmd.Unexecute();
undoCmds.remove(cmd);
redoCmds.add(cmd);
}
public void redo(){
if(redoCmds.size()<=0) return;
Command cmd=redoCmds.get(redoCmds.size()-1);
cmd.Execute();
redoCmds.remove(cmd);
undoCmds.add(cmd);
}
}
//请求者角色
public class PasteMenuItem{
private Command com;
private CommandManager cmdMan;
public PasteMenuItem(Command cmd,CommandManager cmdMan){
this.cmd=cmd;
this.cmdMan=cmdMan;
}
public void Clicked(){
com.Execute();
cmdMan.addUndoCommands(com);
}
}
//接受者角色
public class Doc{
public void paste(){
System.out.println("paste");
}
public void save(){
System.out.println("save");
}
}
//客户端角色
public class Client{
public static void main(String[] args){
Doc doc=new doc();
CommandManager cmdMan=new CommandManager();//保证是一个单例
Command paste=new PasteCommand(doc);
PasteMenuItem pasteMenuItem=new PasteMenuItem(paste,cmdMan);
pasteMenuItem.Clicked();
cmdMan.undo();
cmdMan.redo();
}
}
最后,还有一个宏命令功能, 比如我们最开始说的点餐,其实我们的实现是点一个菜,然后服务员给厨师,厨师做菜,然后我们再点一个。。。。 是一个一个执行的
现实应该是,我们点了好多菜,然后服务员再通知厨师一次性做完,(命令集合)
//宏命令接口
public interface MacroCommand extends Command{
void execute();
void remove(Command cmd);//命令集合中去除一个命令
void add(Command cmd)//命令集合中添加一个命令
}
public class MacroCommandImpl implements MacroCommand{
private List<Command> cmds=new ArrayList<>();
public void add(Command cmd){
cmds.add(cmd);
}
public void remove(Command cmd){
cmds.remove(cmd);
}
public void execute(){
Command cmd;
for(Command cmd:cmds){
cmd.execute();
}
}
}
总结:
优点:
命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开
容易实现撤销和重做、记录日志
命令类容易扩展(添加新命令不影响其他类)、修改
缺点:
使用命令模式会导致某些系统有过多的具体命令类。某些系统可能需要几十个,几百个甚至几千个具体命令类,这会使命令模式在这样的系统里变得不实际。
在下面的情况下应当考虑使用命令模式:
1、使用命令模式作为"CallBack"在面向对象系统中的替代。"CallBack"讲的便是先将一个函数登记上,然后在以后调用此函数。
2、需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在串形化之后传送到另外一台机器上去。
3、系统需要支持命令的撤消(undo)。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo()方法,把命令所产生的效果撤销掉。命令对象还可以提供redo()方法,以供客户端在需要时,再重新实施命令效果。
4、如果一个系统要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志里读回所有的数据更新命令,重新调用Execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。
5、一个系统需要支持交易(Transaction)。一个交易结构封装了一组数据更新命令。使用命令模式来实现交易结构可以使系统增加新的交易类型。