一、命令(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 @operator, int 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 @operator, int 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
for( int 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
for( int i = 0; i < levels; i++ )
if( current > 0 )
((Command)commands[ --current ]).UnExecute();
}
public void Compute( char @operator, int 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。
在需要的情況下,可以較容易地將命令記入日志。
命令模式把請求一個操作的對象與知道怎麼執行一個操作的對象分割開。
命令類與其他任何別的類一樣,可以修改和推廣。
你可以把命令對象聚合在一起,合成為合成命令。比如宏命令便是合成命令的例子。合成命令是合成模式的應用。
由於加進新的具體命令類不影響其他的類,因此增加新的具體命令類很容易。
命令模式的缺點如下:
使用命令模式會導致某些系統有過多的具體命令類。某些系統可能需要幾十個,幾百個甚至幾千個具體命令類,這會使命令模式在這樣的系統裡變得不實際。