.NET设计模式(17):命令模式(Command Pattern)

命令模式(Command Pattern

——.NET设计模式系列之十七

TerryLee20067

概述

在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合[李建忠]。这就是本文要说的Command模式。

意图

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。[GOF 《设计模式》]

结构图

Command模式结构图如下:

1  Command模式结构图

生活中的例子

Command模式将一个请求封装为一个对象,从而使你可以使用不同的请求对客户进行参数化。用餐时的账单是Command模式的一个例子。服务员接受顾客的点单,把它记在账单上封装。这个点单被排队等待烹饪。注意这里的"账单"是不依赖于菜单的,它可以被不同的顾客使用,因此它可以添入不同的点单项目。

2  使用用餐例子的Command模式对象图

Command模式解说

在众多的设计模式中,Command模式是很简单也很优雅的一种设计模式。Command模式它封装的是命令,把命令发出者的责任和命令执行者的责任分开。我们知道,一个类是一组操作和相应的一些变量的集合,现在有这样一个类Document,如下:

3

示意性代码:

/// <summary>

/// 文档类

/// </summary>


public class Document

{
    
/// <summary>

    
/// 显示操作

    
/// </summary>


    
public void Display()

    
{
        Console.WriteLine(
"Display");
    }
 

    
/// <summary>

    
/// 撤销操作

    
/// </summary>


    
public void Undo()

    
{
        Console.WriteLine(
"Undo");
    }


    
/// <summary>

    
/// 恢复操作

    
/// </summary>


    
public void Redo()

    
{
        Console.WriteLine(
"Redo");
    }

}

一般情况下我们使用这个类的时候,都会这样去写:

class Program

{
    
static void Main(string[] args)

    
{
        Document doc 
= new Document();

        doc.Display();

        doc.Undo();

        doc.Redo();
    }

}

这样的使用本来是没有任何问题的,但是我们看到在这个特定的应用中,出现了Undo/Redo的操作,这时如果行为的请求者和行为的实现者之间还是呈现这样一种紧耦合,就不太合适了。可以看到,客户程序是依赖于具体Document的命令(方法)的,引入Command模式,需要对Document中的三个命令进行抽象,这是Command模式最有意思的地方,因为在我们看来Display()Undo()Redo()这三个方法都应该是Document所具有的,如果单独抽象出来成一个命令对象,那就是把函数层面的功能提到了类的层面,有点功能分解的味道,我觉得这正是Command模式解决这类问题的优雅之处,先对命令对象进行抽象:

4

示意性代码:

/// <summary>

/// 抽象命令

/// </summary>


public abstract class DocumentCommand

{
    Document _document;

    
public DocumentCommand(Document doc)

    
{
        
this._document = doc;
    }


    
/// <summary>

    
/// 执行

    
/// </summary>


    
public abstract void Execute();

}

其他的具体命令类都继承于该抽象类,如下:


5

示意性代码:

/// <summary>

/// 显示命令

/// </summary>


public class DisplayCommand : DocumentCommand

{
    
public DisplayCommand(Document doc)

        : 
base(doc)
    
{    

    }


    
public override void Execute()

    
{
        _document.Display();   
    }

}



/// <summary>

/// 撤销命令

/// </summary>


public class UndoCommand : DocumentCommand


    
public UndoCommand(Document doc)

        : 
base(doc)
    
{   

    }


    
public override void Execute()

    
{
        _document.Undo();   
    }

}



/// <summary>

/// 重做命令

/// </summary>


public class RedoCommand : DocumentCommand

{
    
public RedoCommand(Document doc)

        : 
base(doc)
    


    }


    
public override void Execute()

    
{
        _document.Redo();   
    }
 
}

现在还需要一个Invoker角色的类,这其实相当于一个中间角色,前面我曾经说过,使用这样的一个中间层也是我们经常使用的手法,即把AB的依赖转换为AC的依赖。如下:

6

示意性代码:

/// <summary>

/// Invoker角色

/// </summary>


public class DocumentInvoker

{
    DocumentCommand _discmd;

    DocumentCommand _undcmd;

    DocumentCommand _redcmd;

    
public DocumentInvoker(DocumentCommand discmd,DocumentCommand undcmd,DocumentCommand redcmd)
    
{

        
this._discmd = discmd;

        
this._undcmd = undcmd;

        
this._redcmd = redcmd;

    }


    
public void Display()

    
{
        _discmd.Execute();
    }


    
public void Undo()

    
{
        _undcmd.Execute();
    }


    
public void Redo()

    
{
        _redcmd.Execute();
    }

}

现在再来看客户程序的调用代码:

class Program

{
    
static void Main(string[] args)

    
{

        Document doc 
= new Document();


        DocumentCommand discmd 
= new DisplayCommand(doc);

        DocumentCommand undcmd 
= new UndoCommand(doc);

        DocumentCommand redcmd 
= new RedoCommand(doc);


        DocumentInvoker invoker 
= new DocumentInvoker(discmd,undcmd,redcmd);

        invoker.Display();

        invoker.Undo();

        invoker.Redo();

    }

}

可以看到:

1.在客户程序中,不再依赖于DocumentDisplay()Undo()Redo()命令,通过Command对这些命令进行了封装,使用它的一个关键就是抽象的Command类,它定义了一个操作的接口。同时我们也可以看到,本来这三个命令仅仅是三个方法而已,但是通过Command模式却把它们提到了类的层面,这其实是违背了面向对象的原则,但它却优雅的解决了分离命令的请求者和命令的执行者的问题,在使用Command模式的时候,一定要判断好使用它的时机。

2.上面的Undo/Redo只是简单示意性的实现,如果要实现这样的效果,需要对命令对象设置一个状态,由命令对象可以把状态存储起来。

.NET中的Command模式

ASP.NETMVC模式中,有一种叫Front Controller的模式,它分为HandlerCommand树两个部分,Handler处理所有公共的逻辑,接收HTTP PostGet请求以及相关的参数并根据输入的参数选择正确的命令对象,然后将控制权传递到Command对象,由其完成后面的操作,这里面其实就是用到了Command模式。

7  Front Controller 的处理程序部分结构图

8 Front Controller的命令部分结构图

Handler 类负责处理各个 Web 请求,并将确定正确的 Command 对象这一职责委派给 CommandFactory 类。当 CommandFactory 返回 Command 对象后,Handler 将调用 Command 上的 Execute 方法来执行请求。具体的实现如下

Handler类:

/// <summary>

/// Handler类

/// </summary>


public class Handler : IHttpHandler

{
    
public void ProcessRequest(HttpContext context)

    
{

        Command command 
= CommandFactory.Make(context.Request.Params);

        command.Execute(context);

    }


    
public bool IsReusable

    
{
        
get

        
{
            
return true;
        }

    }

}

Command接口:

/// <summary>

/// Command

/// </summary>


public interface Command

{
    
void Execute(HttpContext context);
}

CommandFactory类:

/// <summary>

/// CommandFactory

/// </summary>


public class CommandFactory

{
    
public static Command Make(NameValueCollection parms)

    
{

        
string requestParm = parms["requestParm"];

        Command command 
= null;

        
//根据输入参数得到不同的Command对象

        
switch (requestParm)

        
{
            
case "1":

                command 
= new FirstPortal();

                
break;

            
case "2":

                command 
= new SecondPortal();

                
break;

            
default:

                command 
= new FirstPortal();

                
break;
        }


        
return command;

    }

}

RedirectCommand类:

public abstract class RedirectCommand : Command

{
    
//获得Web.Config中定义的key和url键值对,UrlMap类详见下载包中的代码

    
private UrlMap map = UrlMap.SoleInstance;

    
protected abstract void OnExecute(HttpContext context);

    
public void Execute(HttpContext context)

    
{
        OnExecute(context);

        
//根据key和url键值对提交到具体处理的页面

        
string url = String.Format("{0}?{1}", map.Map[context.Request.Url.AbsolutePath], context.Request.Url.Query);

        context.Server.Transfer(url);

    }

}

FirstPortal类:

public class FirstPortal : RedirectCommand

{
    
protected override void OnExecute(HttpContext context)

    
{
        
//在输入参数中加入项portalId以便页面处理

        context.Items[
"portalId"= "1";

    }

}

SecondPortal类:

public class SecondPortal : RedirectCommand

{
    
protected override void OnExecute(HttpContext context)

    
{
        context.Items[
"portalId"= "2";
    }

}

效果及实现要点

1Command模式的根本目的在于将“行为请求者”与“行为实现者”解耦,在面向对象语言中,常见的实现手段是“将行为抽象为对象”。

2.实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。

3.通过使用Compmosite模式,可以将多个命令封装为一个“复合命令”MacroCommand

4Command模式与C#中的Delegate有些类似。但两者定义行为接口的规范有所区别:Command以面向对象中的“接口-实现”来定义行为接口规范,更严格,更符合抽象原则;Delegate以函数签名来定义行为接口规范,更灵活,但抽象能力比较弱。

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

适用性

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

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

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

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

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

总结

Command模式是非常简单而又优雅的一种设计模式,它的根本目的在于将“行为请求者”与“行为实现者”解耦。

更多的设计模式文章可以访问《.NET设计模式系列文章

 

参考资料

Erich Gamma等,《设计模式:可复用面向对象软件的基础》,机械工业出版社

Robert C.Martin,《敏捷软件开发:原则、模式与实践》,清华大学出版社

阎宏,《Java与模式》,电子工业出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社

MSDN WebCast C#面向对象设计模式纵横谈(14)Command命令模式(结构型模式)

袁剑,《领悟Web设计模式》

作者:TerryLee
出处:http://terrylee.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2006-07-17 18:11 TerryLee 阅读(13796) 评论(36)  编辑 收藏 网摘 所属分类: [05]  架构与设计

  回复  引用  查看    
#1楼2006-07-18 08:22 | stonezhu      
:-)SEEING,希望能加我(zrc000@hotmail.com),和你学更多东西,最近在看你的Enterprise Library 系列...
  回复  引用  查看    
#2楼[楼主]2006-07-18 08:25 | TerryLee      
@stonezhu

已经加了你:)

  回复  引用    
#3楼2006-07-19 09:10 | ADD[未注册用户]
DocumentInvoker invoker = new DocumentInvoker(discmd,undcmd,redcmd);
//全是构造参数,这有点不好吧

  回复  引用    
#4楼2006-07-19 20:56 | 星星远好[未注册用户]
看过例子后,还是没有发现为什么要用命令模式。我在别的参考书是如此论述命令模式的。
客户程序在很多时候只关注程序代码段A,但由于要执行A,必须要进行一些操作B,C。为了解决这个问题,引入命令模式,把B,C封装成类D,以及D提供一个接口可以执行代码A。
在常见程序代码,我们执行的GUI就有例子,用户按下按钮,按钮类处理相关事情,然后调用程序员的按钮处理代码。我认为这才是命令模式。
楼主还能再跟我论述一下命令模式吗?我看你的解释,又感到不理解了。

  回复  引用    
#5楼2006-07-28 12:11 | Mok[未注册用户]
I create a simple Command Pattern class, which using reflection to call the command. By using this method, i can have a lot diference command. Here the code

namespace Testing.CommandPattern
{
public interface ICommand
{
Hashtable execute(Hashtable request);
}

//can have many command as u like...
// The command Class name consist of 3 part
// Method + Object+Command , eg View Document Command become ViewDocumentCommand

public class ViewDocumentCommand : ICommand
{
public Hashtable execute(Hashtable request)
{
Console.WriteLine("Implement view documnet task here....");
Hashtable result = new Hashtable();
//proces.....return result...if need to...
return result;
}
}

public class CreateDocumentCommand : ICommand
{
public Hashtable execute(Hashtable request)
{
Console.WriteLine("Implement create documnet task here....");
Hashtable result = new Hashtable();
//proces.....return result...if need to...
return result;
}

}

//Class to dispatch the command,using reflection...
public class CommandDispatch
{
private Hashtable _commands = new Hashtable();

public Hashtable dispatch(Hashtable requestParam)
{
ICommand cmd = null;

string methodid = (string)requestParam["methodid"];
string objectid = (string)requestParam["objectid"];

string classname = "Testing.CommandPattern"+ "." + methodid + objectid + "Command";
try
{
Type cType = Type.GetType(classname, false, true);
cmd = (ICommand)Activator.CreateInstance(cType);
return cmd.execute(requestParam);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}


return null;
}
}
}

//here...how u request a command
namespace CommandPattern
{
class Program
{
static void Main(string[] args)
{
CommandDispatch cmd = new CommandDispatch();
Hashtable param = new Hashtable();
param.Add("methodid", "View");
param.Add("objectid", "Document");

//request View Document command
cmd.dispatch(param);

}
}

}

  回复  引用  查看    
#6楼2006-08-15 20:47 | main      
@ADD
我想这样能够通过DocumentInvoker 控制对document的方法使用:)

  回复  引用    
#7楼2006-08-28 20:04 | joyli[未注册用户]
command模式的核心:实现调用操作的对象和操作的具体实现者之间的解耦。
这样写代码不是更简便吗?为什么要解耦。请教!
class cmd
{
public: virtual void action() = 0;
}
class cmd1 : public cmd
{
public:void action(){this->openfile();}
private: openfile(){};
}
class cmd2 : public cmd
{
public:void action(){this->closefile();}
private closefile(){};
}
int main()
{
cmd* pcmd = new cmd1();//把这里改成factory,可以创建cmd1,cmd2
pcmd->action();
delete pcmd;
return 0;
}


  回复  引用  查看    
#8楼[楼主]2006-08-29 09:19 | TerryLee      
@joyli
1.为什么要解藕参看文章中的内容

2.你写的代码不就是工厂模式了吗?工厂模式与Command模式解决的是不同的问题。请参考
http://terrylee.cnblogs.com/archive/2006/01/04/310716.html">http://terrylee.cnblogs.com/archive/2006/01/04/310716.html

  回复  引用  查看    
#9楼2006-09-07 15:56 | 3echo      
写得很好,很容易让人理解。学习

请教一个问题:用简单明了、通俗易懂的语言把问题说清楚,有什么好的办法??需要在哪些方面努力?

  回复  引用  查看    
#10楼[楼主]2006-09-07 17:20 | TerryLee      
@3echo
这个问题好难回答,呵呵
总之一句话,把你所理解的,用一种说话的方式表达出来,我想就通俗易懂了:-)

  回复  引用    
#11楼2006-10-04 09:03 | swafany[未注册用户]
文中提到:这样的使用本来是没有任何问题的,但是我们看到在这个特定的应用中,出现了Undo/Redo的操作,这时如果行为的请求者和行为的实现者之间还是呈现这样一种紧耦合,就不太合适了。
请教为什么不合适?

  回复  引用    
#12楼2006-10-07 10:59 | franckcn[未注册用户]
不知道我这个是不是属于这种模式,类代码
Option Explicit
Dim CalcFlag As Boolean
Dim CalcDic As Dictionary

Private Sub Class_Initialize()
Set CalcDic = New Dictionary
CalcFlag = False
End Sub

Private Sub Class_Terminate()
Set CalcDic = Nothing
End Sub
Public Sub StopCalc()
CalcFlag = False
End Sub
Public Sub StartCalc()
CalcFlag = True
CalcDic.RemoveAll
End Sub
Public Sub Add(ByVal pBarCode As String)
If CalcFlag Then
With CalcDic
If Not .Exists(pBarCode) Then
.Add pBarCode, ""
End If
End With
End If
End Sub
Public Property Get Count() As Long
Count = CalcDic.Count
End Property

客户端代码:
If CmdCalc.Caption = "開始計數" Then
lblCount.Caption = "數量:0"
mCalcCount.StartCalc
CmdCalc.Caption = "停止計數"
Else
CmdCalc.Caption = "開始計數"
If Voice Is Nothing Then
Voice = New SpVoice
End If
Voice.Speak CStr(mCalcCount.Count), SVSFlagsAsync
End If

  回复  引用  查看    
#13楼[楼主]2006-10-07 11:19 | TerryLee      
@franckcn
呵呵,有没有C#的啊,VB.NET的看着好累,主要是我没用过:-)

  回复  引用    
#14楼2006-10-19 17:04 | Frank Huang[未注册用户]
推荐大家去关心下Enterprise Library的Data Access Application Block,里面有很多command pattern例子可做参考。
  回复  引用  查看    
#15楼[楼主]2006-10-19 19:53 | TerryLee      
@Frank Huang
整个EL可以说是学习设计模式的绝佳教材!

  回复  引用    
#16楼2006-12-12 09:11 | balala[未注册用户]
理解不了Command模式,在coding层上感觉和Adapter模式也没什么区别。
  回复  引用    
#17楼2007-01-09 00:40 | 青岛开和[未注册用户]
也许兄弟我才疏学浅,看你自己写的列子,让人觉得云里雾里。
原来
static void Main(string[] args)

{
Document doc = new Document();

doc.Display();

doc.Undo();

doc.Redo();
}
经过你一番命令模式之后变成

static void Main(string[] args)

{

Document doc = new Document();


DocumentCommand discmd = new DisplayCommand(doc);

DocumentCommand undcmd = new UndoCommand(doc);

DocumentCommand redcmd = new RedoCommand(doc);


DocumentInvoker invoker = new DocumentInvoker(discmd,undcmd,redcmd);

invoker.Display();

invoker.Undo();

invoker.Redo();

}
后面的invoker.Display();invoker.Undo();invoker.Redo();
其实还是调用的
doc.Display();

doc.Undo();

doc.Redo();
这不是南辕北辙吗?
好比你洗澡,本来就是光着身子,现在是先穿上棉袄,再脱下来。

也许我理解有误,请牛人指点。

相反第二个例子也就是.NET中的Command模式更让人信服。

  回复  引用    
#18楼2007-01-15 00:13 | sh_city[未注册用户]
晕,我看了李建忠老师也没弄懂这个Command 模式,现在看了您的这个,也还是没弄懂Command ,可能是我太笨了,无法领会这种精深的技术.
  回复  引用  查看    
#19楼[楼主]2007-01-15 08:31 | TerryLee      
@sh_city
刚开始学确实会有一头雾水的感觉,慢慢来,不要着急:)

  回复  引用    
#20楼2007-03-05 17:23 | Michael[未注册用户]
command模式其实很好理解的,比如窗体设计中:窗体中的button的click事件,button是命令者,而事件执行的代码就是命令.命令模式就是把事件的执行又封装一层,使其与命令者(button)不在同一类中.实现命令者(button)与执行命令(button_click事件)的隔离
  回复  引用  查看    
#22楼2007-07-10 00:49 | 黄志强      
好文章,最近也在用Front Controller
  回复  引用    
#23楼2007-08-31 12:00 | 华为菜鸟[未注册用户]
好东西,值得学习
  回复  引用  查看    
#24楼2007-09-17 17:50 | 王晓成      
纯粹为了方便,引用了terrylee的文章
  回复  引用    
#25楼2007-09-27 10:44 | longer[未注册用户]
public abstract class DocumentCommand

{
Document _document;///这里应该是Protected

public DocumentCommand(Document doc)

{
this._document = doc;
}

/**//// <summary>

/// 执行

/// </summary>

public abstract void Execute();

}

  回复  引用  查看    
#26楼2007-12-13 09:59 | 书生多命贱      
楼主的 组合模式的单词拼错了,在文章 效果及实现要点 的第3点,“通过使用Compmosite模式”,应该是“Composite”
  回复  引用  查看    
#27楼[楼主]2007-12-13 12:42 | TerryLee      
@书生多命贱
谢谢

  回复  引用  查看    
#28楼2008-12-26 13:56 | Fengdesudu      
不错,学习中。
  回复  引用  查看    
#29楼[楼主]2009-01-04 11:32 | TerryLee      
@Fengdesudu
谢谢:)

  回复  引用    
#30楼2009-06-01 00:25 | nick_temp[未注册用户]
--引用--------------------------------------------------
swafany: 文中提到:这样的使用本来是没有任何问题的,但是我们看到在这个特定的应用中,出现了Undo/Redo的操作,这时如果行为的请求者和行为的实现者之间还是呈现这样一种紧耦合,就不太合适了。
<br>请教为什么不合适?
--------------------------------------------------------
我也很想知道,为什么不适合,直接说不合适未免太过于牵强

  回复  引用  查看    
#31楼2009-06-01 08:58 | 书生多命贱      
因为紧耦合了,所以就不适合!这个已经说的很清楚了!



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 453143




相关文章:

相关链接: