博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

命令模式

Posted on 2009-01-15 21:11  夏虫  阅读(548)  评论(0编辑  收藏  举报
    当我要求我负责的系统应是事务的,要能Undo,结果同事告诉我一堆东西后说可以使用命令模式,于是我开始研究什么是命令模式,我才突然明白什么东西都是要先了解后听取的。

    命令模式的应用我认为可以理解成流水线,领导下发生产命令,流水线上的工人按预先安排好的顺序开始工作,这一顺序的安排由该流水线的负责人负责。这恰好可以反映出命令模式中的几个角色。其中流水线的负责人是命令模式的核心就是为了解耦领导和工人而添加的,在原始的模式中是领导接命令工作,反映到程序中好像UI直接调用一系列具有相同接口定义的业务逻辑类,例如在仓库系统中我们希望在按下按钮后,系统保存入库单,然后修改库存,然后记入日志,

onbuttonclick{
  Save();
  Update():
  Log();

,一般情况下没有问题,但如果你现在想在记入日志后,能在修改对应订单的话,你就必须修改调用者的代码,在本例中就是在后面在加行函数调用,这就导致调用者和执行者的过于耦合,修改执行者就必须修改调用者,所以我们就想加一个中间者来解耦他们(通常对这个中间者的修改的代价远小于调用者),在命令。下面是我在这一过程中从网络上获得的我认为有用的资料的混乱合编体。

    在开始前提个概念,那就是什么是类,什么是可以封装的,以前一直是对具体的自然物进行封装,如入库单,但在命令模式中进行的封装不是物而是业务,比如将入库封装成一个类,虽然我看着别扭但在某种应用中确实有道理,同时,如果你看了工厂模式后,就会发现两者很象,我也这么认为,并且认为他们的不同就是封装的理念不同,一个封装的是物一个是业务。

Motivation
记录,撤销/重做,事务等情况下如何将请求者和实现者进行解耦
Intent

将一个请求封装为一个对象,从而可以使用不同的请求对客户(行为请求者)进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
限制 :对各命令的调用方式必须是相同的,包括参数,差异的屏蔽工作由实现类(拉长)完成。

通常命令模式有一下几个角色
调用者:
命令的执行者:
  1.生成有序的命令队列:相当于流水线;
  2.按顺序执行命令操作:命令相当于工人;
  3.提供撤销命令操作;
  4.记录已经操作的命令
 
 抽象命令:
    抽象的命令接口
  具体命令:
  具体的命令。
  由三个要素组成:执行者,执行者要作的操作和被执行的对象组成。当然还可以有其他,比如将对象执行成什么结果。例如:调用Mypait类(

执行者)将My rectangle(对象)填充(操作)为红色(结果)。这样就可以完全描述一个命令了。
执行者:
真正执行逻辑操作的对象


实现方法:
1.建立基类(接口),定义标准调用方式

abstract class Command
{
abstract public void execute();
abstract public void unexecute();
abstract public void reexecute();
//一般有这样这个方法,根据需要可以增删
}

2.建立基于应用的子类,每个业务对应一个,下例中建立了两个命令
// 1:写作命令,选择一个作者(Author类实例对象),让他写作(调用它的write方法)写作的对象是书(Book的实例对象)形成了一个写作

的命令,写作的对象是Book的实例
public class WriteCommand implement Command
{
Author author; //执行者
Book book; //要执行的对象
public WriteCommand (Author author,Book book) {
this. author = author;
this. book = book;
}
// 在这里执行要执行的操作, 屏蔽具体函数的差异
public override void Execute()
{
author.write (book);
}

}

// 2: 出版命令,选择一个出版社(publisher类实例对象),让他出版书(调用它的publisherBook方法)出版的对象是书(Book的实例对象

)形成了一个出版的命令
public class PublishCommand implement Command
{
Publisher publisher;
Book book;
public PublishCommand (Publisher publisher) {
this. publisher = publisher;
this. book = book;
}
// Methods
public override void Execute()
{
publisher. publisherBook(book);
}
}

3.在调用者处如UI中建立命令队列,其实见不见队列并不是主要的,建立队列的用处在于,可以预遍命令序列,但如果应用队列是固定,唯一

变化的是具体的实现者,我认为就没必要建立队列。
//调用者
public class Invoker{
List commands; //命令集合

public void setCommands(List commands){
this.commands = commands;
}

public void addCommand (Command command,int i){
commands.add(i,command);
}
public void removeCommand (int i){
commands.add(i,command);
}
//得代执行命令
public void action(){
for(Iterator it = list.iterator();it.hasNext();){
  Command command = (Command) it.next();
  Command. execute();
}
}
……………
//还可以有丰富的redo和undo操作;(当然一些都给基于命令类提供的相应方法)
}

这样我们的客户端代码就可以这样写:
//如果我要出一本书
//一本空白的书
Book book = new Book();
//先找一个作者和出版社
Author author = new Author();
Publisher publisher = new Publisher ();
//产生命令集合
Command writeCommand = new WriteCommand (author,book);
Command publishCommand = new PublishCommand(publisher,book);
List commandsList = new List ();
commandsList.add(writeCommand);
//找个调用者,把命令给它,让他来根据命令协调工作
Invoker invoker = new invoker();
Invoker.setCommands(commandsList);
invoker.action();

应用方案:
1》 分布登记统一执行:
在作程序时,经常碰到一些需求,先注册一些操作,并不马上执行,等最终确定后统一执行。如一个具体的例子:用户定制自己的报表,可以订

阅饼,柱,折线,曲线图,客户选择相应的报表组合,这样对应一个命令集合,在没确定之前用户可以增删这些报表(命令),等最终确定统

一交给调用者根据命令执行,生成组合报表。实现了命令分布提出,确定后统一执行的功能。

2》形如流水线操作:如出书的例子

3》系统需要支持命令的撤消(undo)。提供redo()方法
我们可以和容易的加入undo和redo,这个不难理解

4》在Invoker中我们可以实现跟踪,和日志。

5》当系统需要为某项服务增加新的功能的时候,命令模式使新的功能(表现为一种命令)很容易地被加入到服务种里。
命令联系了工具类即执行类和系统逻辑,

简化/变化的命令模式:
命令模式的角色比较多,在实际应用种我们可以根据所需要的功能和不需要的功能加以简化。

1》去掉 调用者
产生命令集合后,我们可以直接在client中迭代执行执行操作
2》 变化 调用者 成为 跟踪者
//调用者
public class Invoker{
List commands; //已经执行完毕的命令集合
public void addCommand (Command command,int i){
commands.add(i,command);
}
public void action(Command command){
//执行操作
command. execute();
//
commands.add(command);
}
}
……………
//还可以有丰富的redo和undo操作;(当然一些都给基于命令类提供的相应方法)
}
这样这个类就记录了所有执行过的操作。

3》去掉 命令 用map替代
我们完全可以用map代替命令,这样无需定义各种命令类
我们改进例子
Author author = new Author();
Publisher publisher = new Publisher ();
Map m = new HashMap;
m.put(author, write);
m.put(author, publisherBook);
在Invoker的action方法:
迭代map
运用java反射来调用方法;

4》去掉执行者:
直接在命令中(execute方法种)加业务逻辑。这样只适合于简单的小的系统.

其他要说的内容
1》 将某些参数传给某个方法的方式很多,除了当作方法的参数外还可以当作类的成员便俩变量传入:
这就为命令的抽象带来了极大的方便
abstract class Command
{
abstract public void execute();
}
当我们已经有了执行者(类Test)方法execute(args1,args2 ….argsn)
我们不必向Command加入execute(args1,args2 ….argsn)抽象方法,在说即使加了,在我们迭代的时候也无法判断或十分不容易判断哪个命

令调用哪个execute方法。
那么我们可以这样
class ConcreteCommand : Command
{
Test test;
args1
args2
…..
argsn
public override void Execute()
{
test. execute (args1,args2 ….argsn);
}
}
2》 在想跟踪操作的时候,一般为每一个操作对象分配一个调用者,操作对象在调用者中设置。(可以抽象出一个总的调用者,来协调调用每

一个具体的调用者)
3》 命令的抽象粒度我觉得是要注意的。
4》 理解思想,不要机械的照搬。消化成自己的,加以灵活的运用和创造在是根本出路。
所谓命令模式的根本思想就是在 先形成命令,在根据命令执行。


命令模式的理解,关键有2点:

一、在面向对象编程中,大量使用if…else…,或者switch…case…这样的条件选择语句是“最差实践”。通常这类代码,意味着有重构的余

地。

命令模式就是干掉条件选择语句的利器。

1,首先提供一个接口:

public interface Command {

public void execute();

}

 

2,然后提供这个接口的实现类。每一个实现类的方法就是if…else…的一个代码块中的代码。

这样,调用方直接把一个具体类的实例传进来即可。如:

Public void test(Command para){

   Para.execute();

 

}

    即可,不需要再判断出现了哪种情况,应该执行哪一段代码。

一切的问题都由调用方处理。

如果不使用命令模式,那么如果情况逐步增多,如,从原来的2种,增加到20种,那么方法中的判断就会从1次增加到19次。

而使用命令模式,仅仅调用方需要从2个实现类增加到20个实现类即可。上面的test方法根本不需要做任何改变。

 

二、主要的用途是,使用参数回调模式。

   最主要使用命令模式的方式是使用参数回调模式。

命令接口作为方法的参数传递进来。然后,在方法体内回调该接口。

上文的例子是如此,Swing的事件机制也是使用这种方法。

如:

public interface Command {

public void execute();

}

 

public void actionPerformed(ActionEvent e) {

Command cmd = (Command)e.getSource();

cmd.execute();

}