`

第16章 命令模式(Command Pattern)

 
阅读更多

命令模式(Command Pattern)

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

TerryLee,2006年7月

概述

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

意图

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

结构图

Command模式结构图如下:

图1 Command模式结构图

生活中的例子

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

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

Command模式解说

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

图3

示意性代码:

/**////<summary>

///文档类

///</summary>

publicclassDocument

{
/**////<summary>

///显示操作

///</summary>

publicvoidDisplay()

{
Console.WriteLine("Display");
}

/**////<summary>

///撤销操作

///</summary>

publicvoidUndo()

{
Console.WriteLine("Undo");
}

/**////<summary>

///恢复操作

///</summary>

publicvoidRedo()

{
Console.WriteLine("Redo");
}
}

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

classProgram

{
staticvoidMain(string[]args)

{
Documentdoc=newDocument();

doc.Display();

doc.Undo();

doc.Redo();
}
}

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

图4

示意性代码:

/**////<summary>

///抽象命令

///</summary>

publicabstractclassDocumentCommand

{
Document_document;

publicDocumentCommand(Documentdoc)

{
this._document=doc;
}

/**////<summary>

///执行

///</summary>

publicabstractvoidExecute();

}

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


图5

示意性代码:

/**////<summary>

///显示命令

///</summary>

publicclassDisplayCommand:DocumentCommand

{
publicDisplayCommand(Documentdoc)

:base(doc)
{

}

publicoverridevoidExecute()

{
_document.Display();
}
}


/**////<summary>

///撤销命令

///</summary>

publicclassUndoCommand:DocumentCommand

{
publicUndoCommand(Documentdoc)

:base(doc)
{

}

publicoverridevoidExecute()

{
_document.Undo();
}
}


/**////<summary>

///重做命令

///</summary>

publicclassRedoCommand:DocumentCommand

{
publicRedoCommand(Documentdoc)

:base(doc)
{

}

publicoverridevoidExecute()

{
_document.Redo();
}
}

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

图6

示意性代码:

/**////<summary>

///Invoker角色

///</summary>

publicclassDocumentInvoker

{
DocumentCommand_discmd;

DocumentCommand_undcmd;

DocumentCommand_redcmd;

publicDocumentInvoker(DocumentCommanddiscmd,DocumentCommandundcmd,DocumentCommandredcmd)
{

this._discmd=discmd;

this._undcmd=undcmd;

this._redcmd=redcmd;

}

publicvoidDisplay()

{
_discmd.Execute();
}

publicvoidUndo()

{
_undcmd.Execute();
}

publicvoidRedo()

{
_redcmd.Execute();
}
}

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

classProgram

{
staticvoidMain(string[]args)

{

Documentdoc=newDocument();


DocumentCommanddiscmd=newDisplayCommand(doc);

DocumentCommandundcmd=newUndoCommand(doc);

DocumentCommandredcmd=newRedoCommand(doc);


DocumentInvokerinvoker=newDocumentInvoker(discmd,undcmd,redcmd);

invoker.Display();

invoker.Undo();

invoker.Redo();

}
}

可以看到:

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

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

.NET中的Command模式

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

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

图8 Front Controller的命令部分结构图

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

Handler类:

/**////<summary>

///Handler类

///</summary>

publicclassHandler:IHttpHandler

{
publicvoidProcessRequest(HttpContextcontext)

{

Commandcommand=CommandFactory.Make(context.Request.Params);

command.Execute(context);

}

publicboolIsReusable

{
get

{
returntrue;
}
}
}

Command接口:

/**////<summary>

///Command

///</summary>

publicinterfaceCommand

{
voidExecute(HttpContextcontext);
}

CommandFactory类:

/**////<summary>

///CommandFactory

///</summary>

publicclassCommandFactory

{
publicstaticCommandMake(NameValueCollectionparms)

{

stringrequestParm=parms["requestParm"];

Commandcommand=null;

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

switch(requestParm)

{
case"1":

command=newFirstPortal();

break;

case"2":

command=newSecondPortal();

break;

default:

command=newFirstPortal();

break;
}

returncommand;

}
}

RedirectCommand类:

publicabstractclassRedirectCommand:Command

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

privateUrlMapmap=UrlMap.SoleInstance;

protectedabstractvoidOnExecute(HttpContextcontext);

publicvoidExecute(HttpContextcontext)

{
OnExecute(context);

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

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

context.Server.Transfer(url);

}
}

FirstPortal类:

publicclassFirstPortal:RedirectCommand

{
protectedoverridevoidOnExecute(HttpContextcontext)

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

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

}
}

SecondPortal类:

publicclassSecondPortal:RedirectCommand

{
protectedoverridevoidOnExecute(HttpContextcontext)

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

效果及实现要点

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

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

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

4.Command模式与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
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
分享到:
评论

相关推荐

    24个设计模式与6大设计原则

    第 12 章 命令模式【COMMAND PATTERN】 112 第 13 章 装饰模式【DECORATOR PATTERN】 126 第 14 章 迭代器模式【ITERATOR PATTERN】 137 第 15 章 组合模式【COMPOSITE PATTERN】 147 第 16 章...

    设计模式 design pattern

    第3章 创建型模式 54 3.1 Abstract Factory(抽象工厂)— 对象创建型模式 57 3.2 Builder(生成器)—对象创建型 模式 63 3.3 Factory Method(工厂方法)— 对象创建型模式 70 3.4 Prototype(原型)—对象创建型 ...

    [源代码] 《易学 设计模式》 随书源代码

    第21章 独具匠心:命令模式 (Command) 第22章 步调一致:访问者模式 (Visitor) 第23章 左右逢源:调停者模式 (Mediator) 第24章 白纸黑字:备忘录模式 (Memento) 第25章 周而复始:迭代器模式 (Iterator) 第26章 ...

    Java设计模式

    第 12 章 命令模式【COMMAND PATTERN】 ............................................................................................. 112 第 13 章 装饰模式【DECORATOR PATTERN】 .............................

    《设计模式》中文版(23个设计模式的介绍与运用)

    第3章 创建型模式 54 3.1 Abstract Factory(抽象工厂)— 对象创建型模式 57 3.2 Builder(生成器)—对象创建型 模式 63 3.3 Factory Method(工厂方法)— 对象创建型模式 70 3.4 Prototype(原型)—对象创建型 ...

    设计模式 GOF 23

    第3章 创建型模式 54 3.1 Abstract Factory(抽象工厂)— 对象创建型模式 57 3.2 Builder(生成器)—对象创建型 模式 63 3.3 Factory Method(工厂方法)— 对象创建型模式 70 3.4 Prototype(原型)—对象创建型 ...

    设计模式:可复用面向对象软件的基础--详细书签版

    第3章 创建型模式 54 3.1 abstract factory(抽象工厂)— 对象创建型模式 57 3.2 builder(生成器)—对象创建型 模式 63 3.3 factory method(工厂方法)— 对象创建型模式 70 3.4 prototype(原型)—对象...

    入门学习Linux常用必会60个命令实例详解doc/txt

    如果选择用命令行模式登录Linux的话,那么看到的第一个Linux命令就是login:。 一般界面是这样的: Manddrake Linux release 9.1(Bamboo) for i586 renrel 2.4.21-0.13mdk on i686 / tty1 localhost login:root ...

    asp.net知识库

    深入剖析ASP.NET组件设计]一书第三章关于ASP.NET运行原理讲述的补白 asp.net 运行机制初探(httpModule加载) 利用反射来查看对象中的私有变量 关于反射中创建类型实例的两种方法 ASP.Net应用程序的多进程模型 NET委托...

    2009 达内Unix学习笔记

    集合了 所有的 Unix命令大全 登陆服务器时输入 公帐号 openlab-open123 telnet 192.168.0.23 自己帐号 sd08077-you0 ftp工具 192.168.0.202 tools-toolss 老师测评网址 http://172.16.0.198:8080/poll/ 各个 ...

    java 面试题 总结

    16、同步和异步有何异同,在什么情况下分别使用他们?举例说明。 如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,...

    超级有影响力霸气的Java面试题大全文档

    当客户机第一次调用一个Stateful Session Bean 时,容器必须立即在服务器中创建一个新的Bean实例,并关联到客户机上,以后此客户机调用Stateful Session Bean 的方法时容器会把调用分派到与此客户机相关联的Bean实例...

    代码语法错误分析工具pclint8.0

    mands”命令项,在“Command”栏中选择“PC-lint unit check”命令运行即可。 注意到我的Run一栏的参数和上面的提示不一样,其实我的其他古怪参数都放到c:\lint\s td.lnt中了。请注意,不论你怎样配置参数一定...

    Linux操作系统基础教程

    关於 Process 处理的指令...............................................................................................16 六. 关於字串处理的指令...........................................................

    C++MFC教程

    +-- 第六章 网络通信开发 |------ 6.1 WinSock介绍 |------ 6.2 利用WinSock进行无连接的通信 +------ 6.3 利用WinSock建立有连接的通信   第一章 VC入门 1.1 如何学好VC 这个问题很多朋友都问过我,当然流汗是...

Global site tag (gtag.js) - Google Analytics