First we try, then we trust

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  183 随笔 :: 111 文章 :: 2983 评论 :: 339 引用

一、 命令(Command)模式

命令(Command)模式属于对象的行为模式【GOF95】。命令模式又称为行动(Action)模式或交易(Transaction)模式。命令模式把一个请求或者操作封装到一个对象中。命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。

命令模式是对命令的封装。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。

每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。


二、 命令模式的结构

命令模式的类图如下:

 

命令模式涉及到五个角色,它们分别是:

  • 客户(Client)角色:创建了一个具体命令(ConcreteCommand)对象并确定其接收者。
  • 命令(Command)角色:声明了一个给所有具体命令类的抽象接口。这是一个抽象角色。
  • 具体命令(ConcreteCommand)角色:定义一个接受者和行为之间的弱耦合;实现Execute()方法,负责调用接收考的相应操作。Execute()方法通常叫做执方法。
  • 请求者(Invoker)角色:负责调用命令对象执行请求,相关的方法叫做行动方法。
  • 接收者(Receiver)角色:负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。

 

三、 命令模式的示意性源代码

// Command pattern -- Structural example  
using System;

// "Command"
abstract class Command
{
  
// Fields
  protected Receiver receiver;

  
// Constructors
  public Command( Receiver receiver )
  
{
    
this.receiver = receiver;
  }


  
// Methods
  abstract public void Execute();
}


// "ConcreteCommand"
class ConcreteCommand : Command
{
  
// Constructors
  public ConcreteCommand( Receiver receiver ) :
    
base ( receiver ) {}

  
// Methods
  public override void Execute()
  
{
    receiver.Action();
  }

}


// "Receiver"
class Receiver
{
  
// Methods
  public void Action()
  
{
    Console.WriteLine(
"Called Receiver.Action()");
  }

}


// "Invoker"
class Invoker
{
  
// Fields
  private Command command;

  
// Methods
  public void SetCommand( Command command )
  
{
    
this.command = command;
  }


  
public void ExecuteCommand()
  
{
    command.Execute();
  }

}


/// <summary>
///  Client test
/// </summary>

public class Client
{
  
public static void Main( string[] args )
  
{
    
// Create receiver, command, and invoker
    Receiver r = new Receiver();
    Command c 
= new ConcreteCommand( r );
    Invoker i 
= new Invoker();

    
// Set and execute command
    i.SetCommand(c);
    i.ExecuteCommand();
  }

}

 

四、 玉帝传美猴王上天

命令模式不是新的发明,在美猴王大闹天宫之前就有了。那时玉帝命令太白金星召美猴王上天:"金星径入(水帘洞)当中,面南立定道:'我是西方太白金星,奉玉帝招安圣旨,下界请你上大,拜受仙录。'"玉帝是系统的客户端,太白金星是命令的发出者,猴王是命令的接收者,圣旨就是命令。玉帝的这一道命令就是要求猴王到上界报到。玉帝只管发出命令,而不管命令是怎样传达到美猴王的。太白金星负责将圣旨传到,可是美猴王怎么执行圣旨、何时执行圣旨是美猴王自己的事。果不然,个久美猴王就大闹了天宫。

这个模拟系统的设计如下:


五、 命令模式的实现

首先命令应当"重"一些还是"轻"一些。在不同的情况下,可以做不同的选择。如果把命令设计得"轻",那么它只是提供了一个请求者和接收者之间的耦合而己,命令代表请求者实现请求。

相反,如果把命令设计的"重",那么它就应当实现所有的细节,包括请求所代表的操作,而不再需要接收者了。当一个系统没有接收者时,就可以采用这种做法。

更常见的是处于最"轻"和最"重"的两个极端之间时情况。命令类动态地决定调用哪一个接收者类。

其次是否支持undo和redo。如果一个命令类提供一个方法,比如叫unExecute(),以恢复其操作的效果,那么命令类就可以支持undo和redo。具体命令类需要存储状态信息,包括:

1. 接收者对象实际上实施请求所代表的操作;
2. 对接收者对象所作的操作所需要的参数;
3. 接收者类的最初的状态。接收者必须提供适当的方法,使命令类可以通过调用这个方法,以便接收者类恢复原有状态。

如果只需要提供一层的undo和redo,那么系统只需要存储最后被执行的那个命令对象。如果需要支持多层的undo和redo,那么系统就需要存储曾经被执行过的命令的清单,清单能允许的最大的长度便是系统所支持的undo和redo的层数。沿着清单逆着执行清单上的命令的反命令(unExecute())便是undo;沿着清单顺着执行清单上的命令便是redo。


六、 命令模式的实际应用案例

下面的代码使用命令模式演示了一个简单的计算器,并允许执行undo与redo。注意:"operator"在C#中是关键词,所以在前面添加一个"@"将其变为标识符。

// Command pattern -- Real World example  
using System;
using System.Collections;

// "Command"
abstract class Command
{
  
// Methods
  abstract public void Execute();
  
abstract public void UnExecute();
}


// "ConcreteCommand"
class CalculatorCommand : Command
{
  
// Fields
  char @operator;
  
int operand;
  Calculator calculator;

  
// Constructor
  public CalculatorCommand( Calculator calculator,
    
char @operatorint operand )
  
{
    
this.calculator = calculator;
    
this.@operator = @operator;
    
this.operand = operand;
  }


  
// Properties
  public char Operator
  
{
    
set{ @operator = value; }
  }


  
public int Operand
  
{
    
set{ operand = value; }
  }


  
// Methods
  override public void Execute()
  
{
    calculator.Operation( @
operator, operand );
  }

 
  
override public void UnExecute()
  
{
    calculator.Operation( Undo( @
operator ), operand );
  }


  
// Private helper function
  private char Undo( char @operator )
  
{
    
char undo = ' ';
    
switch( @operator )
    
{
      
case '+': undo = '-'break;
      
case '-': undo = '+'break;
      
case '*': undo = '/'break;
      
case '/': undo = '*'break;
    }

    
return undo;
  }

}


// "Receiver"
class Calculator
{
  
// Fields
  private int total = 0;

  
// Methods
  public void Operation( char @operatorint operand )
  
{
    
switch( @operator )
    
{
      
case '+': total += operand; break;
      
case '-': total -= operand; break;
      
case '*': total *= operand; break;
      
case '/': total /= operand; break;
    }

    Console.WriteLine( 
"Total = {0} (following {1} {2})",
      total, @
operator, operand );
  }

}


// "Invoker"
class User
{
  
// Fields
  private Calculator calculator = new Calculator();
  
private ArrayList commands = new ArrayList();
  
private int current = 0;

  
// Methods
  public void Redo( int levels )
  
{
    Console.WriteLine( 
"---- Redo {0} levels ", levels );
    
// Perform redo operations
    forint i = 0; i < levels; i++ )
      
if( current < commands.Count - 1 )
        ((Command)commands[ current
++ ]).Execute();
  }


  
public void Undo( int levels )
  
{
    Console.WriteLine( 
"---- Undo {0} levels ", levels );
    
// Perform undo operations
    forint i = 0; i < levels; i++ )
      
if( current > 0 )
        ((Command)commands[ 
--current ]).UnExecute();
  }


  
public void Compute( char @operatorint operand )
  
{
    
// Create command operation and execute it
    Command command = new CalculatorCommand(
      calculator, @
operator, operand );
    command.Execute();

    
// Add command to undo list
    commands.Add( command );
    current
++;
  }

}


/// <summary>
/// CommandApp test
/// </summary>

public class Client
{
  
public static void Main( string[] args )
  
{
    
// Create user and let her compute
    User user = new User();

    user.Compute( 
'+'100 );
    user.Compute( 
'-'50 );
    user.Compute( 
'*'10 );
    user.Compute( 
'/'2 );

    
// Undo and then redo some commands
    user.Undo( 4 );
    user.Redo( 
3 );
  }

}

 


七、 在什么情况下应当使用命令模式

在下面的情况下应当考虑使用命令模式:

1、使用命令模式作为"CallBack"在面向对象系统中的替代。"CallBack"讲的便是先将一个函数登记上,然后在以后调用此函数。

2、需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在串形化之后传送到另外一台机器上去。

3、系统需要支持命令的撤消(undo)。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo()方法,把命令所产生的效果撤销掉。命令对象还可以提供redo()方法,以供客户端在需要时,再重新实施命令效果。

4、如果一个系统要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志里读回所有的数据更新命令,重新调用Execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。

5、一个系统需要支持交易(Transaction)。一个交易结构封装了一组数据更新命令。使用命令模式来实现交易结构可以使系统增加新的交易类型。


八、 使用命令模式的优点和缺点

命令允许请求的一方和接收请求的一方能够独立演化,从而且有以下的优点:

  • 命令模式使新的命令很容易地被加入到系统里。
  • 允许接收请求的一方决定是否要否决(Veto)请求。
  • 能较容易地设计-个命令队列。
  • 可以容易地实现对请求的Undo和Redo。
  • 在需要的情况下,可以较容易地将命令记入日志。
  • 命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
  • 命令类与其他任何别的类一样,可以修改和推广。
  • 你可以把命令对象聚合在一起,合成为合成命令。比如宏命令便是合成命令的例子。合成命令是合成模式的应用。
  • 由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很容易。

命令模式的缺点如下:

  • 使用命令模式会导致某些系统有过多的具体命令类。某些系统可能需要几十个,几百个甚至几千个具体命令类,这会使命令模式在这样的系统里变得不实际。


参考文献:
阎宏,《Java与模式》,电子工业出版社
[美]James W. Cooper,《C#设计模式》,电子工业出版社
[美]Alan Shalloway  James R. Trott,《Design Patterns Explained》,中国电力出版社
[美]Robert C. Martin,《敏捷软件开发-原则、模式与实践》,清华大学出版社
[美]Don Box, Chris Sells,《.NET本质论 第1卷:公共语言运行库》,中国电力出版社
http://www.dofactory.com/Patterns/Patterns.aspx 

posted on 2004-11-27 22:11 吕震宇 阅读(13185) 评论(29)  编辑 收藏 所属分类: 设计模式

评论

#1楼  2004-11-29 18:32 mill2002      
收藏~

谢谢!




  回复  引用  查看    

#2楼  2004-11-30 10:55 81      
鼓励一下,我收藏了全系列,比GoF写的好懂多了,是否以后往专业作家方面发展。
  回复  引用  查看    

#3楼 [楼主] 2004-11-30 22:27 吕震宇      
呵呵,惭愧,推荐买一本《Java与模式》,我这个系列中的大多数内容来自这本书。专业作家更不敢当。
  回复  引用  查看    

#4楼  2004-12-01 20:47 wayfarer      
和楼上的楼上差不多,我虽然没有收藏,但订阅了设计模式的RSS。

震宇兄别谦虚。像你写的《毛笔和蜡笔》就很好啊。希望这个系列能突破《Java与模式》,写出自己的见解来。目前来看,这个系列介绍的都很到位,不过如果能有些与实际联系比较紧密的例子就比较好了。

  回复  引用  查看    

#5楼 [楼主] 2004-12-02 08:47 吕震宇      
我正在考虑写一个系列,通过一个实例将设计模式与实际应用联系起来。但是现在时间很紧张,另外就怕才疏学浅,写了让人笑话。不过没有抛砖哪来引玉,我会在近期推出这个系列。

题目暂定《华容道与数据结构》、《华容道与设计模式》。初步设想:华容道想必大家都很熟悉,计算机解华容道问题已经不是什么新鲜的事情。我想站在面向对象的角度以及数据结构的角度重新设计华容道解题游戏。在数据结构中引入AVL平衡树作为检索手段,在设计模式方面应用Composite、Factory、FlyWeight、Builder等等模式(还没有想好),在层次上分成界面UI层、解题逻辑层、棋盘布局表示层以及底层基础算法层。但是现在遇到一个小问题,就是如何兼顾内存使用和运行效率。并且要使代码足够通用,最好使用泛型机制,在.NET 2003中还不支持。再经过一段时间思考后,我会着手来写的。希望不辜负大家的期望。
  回复  引用  查看    

#6楼  2004-12-02 09:10 wayfarer      
期待啊,看来震宇兄要来大动作了。
  回复  引用  查看    

#7楼  2005-01-23 12:22 andy sun [未注册用户]
core j2ee design pattern 也不错.
  回复  引用    

#8楼  2005-02-25 09:44 zhouaiping [未注册用户]
代码没有真正地转成C#的语法.
记得在c#中定义接口是用:public interface Command
  回复  引用    

#9楼 [楼主] 2005-02-25 20:34 吕震宇      
呵呵,代码是“拿来”的,原封不动的放上来了。
  回复  引用  查看    

#10楼  2005-07-26 11:19 cloud [未注册用户]
通过画设计模式的UML图,我受益非浅。在此谢谢了
  回复  引用    

http://www.dofactory.com/Patterns/PatternCommand.aspx

翻译的不错 ! 有一定的自己的见解!
  回复  引用    

#12楼 [楼主] 2005-08-30 19:56 吕震宇      
@jacky

其实我在:

http://www.cnblogs.com/zhenyulu/articles/41829.html

已经很坦白了,还是惹来如此多的嘲笑!:-(

  回复  引用  查看    

#13楼  2005-10-16 21:35 BillConan Studio      
恕我无知 我老觉得设计模式这个东西太虚 有时候解决不了什么实际问题

我是想实现我程序中的redo 和undo操作 找到这里的。 但是我的程序中的操作不是简单的可逆操作 我应该怎么办呢?

一个比较直观的例子就是如果你这个计算器程序是计算表达式的 也就是说用户不是一个一个数字输入到计算器的 而是一下子输入了一个表达式 比如1+2 然后计算出3 这时候我想undo 应该怎么办到呢 好比任意给了一个整数 告诉你它是两个整数的和 如何得到这两个整数。

在我的程序里 涉及到了求很多数的平均数及undo操作 头疼啊
  回复  引用  查看    

#14楼  2005-11-04 11:01 阿k [未注册用户]
感觉楼上的问题是具体算法的问题,而不在设计模式这一层
  回复  引用    

#15楼  2005-11-08 13:09 feifeifei [未注册用户]
Undo, 可以用原形模式实现,每一次输入都是一个对象,克隆几下,把当前的输入字符串记录下来就好
  回复  引用    

#16楼  2006-01-24 16:51 悠悠我心      
偶顶!
  回复  引用  查看    

#17楼  2006-04-24 11:10 磊 [未注册用户]
吕兄:我不能很好区分Command模式和Observer模式,原因是书籍中都讲到了swing中事件的处理,某个控件添加监听器,然后当控件发生改变时,监听器执行相应的功能。你是否也看过其他材料在讲着两个模式时都用了事件处理来举例,那么你是怎么区分的呢?还是着两个模式的关联很大。
  回复  引用    

UML图划错了,CLIENT应该和RECEIVER和COMMAND有依赖关系,而不是与INVOKER,改过来才和你的代码实例相符
不知道我理解的对不对
  回复  引用    

UML图划错了,CLIENT应该和RECEIVER和COMMAND有依赖关系,而不是与INVOKER,改过来才和你的代码实例相符
不知道我理解的对不对
  回复  引用    

#20楼  2006-08-13 20:58 Ring      
负责调用接收考的相应操作。Execute()方法通常叫做执方法。

负责调用接收方的相应操作。Execute()方法通常叫做执行方法。
  回复  引用  查看    

#21楼  2006-09-28 21:31 沙漠野狼      
模仿一下!我们可以写中文程序了,如果谁再把中文关键字编译器设计出来,那么就完全是中文的了,希望将来的某一天可以实现。

{
// "圣旨"
abstract class 圣旨
{
// 实体
protected 美猴王 美猴王实体;

// 具体内容
public 圣旨( 美猴王 美猴王实体 )
{
this.美猴王实体 = 美猴王实体;
}
// 动作
abstract public void 执行();
}

// "立即上天廷"
class 立即上天廷 : 圣旨
{
// 具体内容
public 立即上天廷( 美猴王 美猴王实体 ) : base ( 美猴王实体 )
{}

// 动作
public override void 执行()
{
美猴王实体.上天廷();
}
}

// "美猴王"
class 美猴王
{
// 动作
public void 上天廷()
{
Console.WriteLine("调用 美猴王.上天廷()");
}
}

// "太白金星"
class 太白金星
{
// 实体
private 圣旨 具体圣旨;

// 动作
public void 接受圣旨( 圣旨 具体圣旨 )
{
this.具体圣旨 = 具体圣旨;
}
public void 执行圣旨()
{
具体圣旨.执行();
}
}

class Program
{
public static void Main( string[] args )
{
// 创建 美猴王实体, 具体圣旨, 和 太白金星实体
美猴王 此刻的美猴王 = new 美猴王();
圣旨 此刻的圣旨 = new 立即上天廷( 此刻的美猴王 );
太白金星 此刻的太白金星 = new 太白金星();

// 接受 和 执行 具体圣旨
此刻的太白金星.接受圣旨(此刻的圣旨);
此刻的太白金星.执行圣旨();
}
}
}

  回复  引用  查看