设计模式笔记(七)– 命令模式

​命令模式非常有用,它用于很多框架中,您可能在不知情的情况下已使用了这种模式。利用命令模式可以实现命令的发送者与接收者的解耦,并可实现复合命令和可撤销命令。


问题提出

假设我们要设计一个用户界面的框架。

我们创建像Button,CheckBox,TextBox等用户界面组件,其他开发者可以使用我们的组件构建出复杂的界面。

以Button为例。

创建类Button,它有私有属性label,还有一个方法click()。

click方法依赖于用户在哪里使用Button组件,有可能用于保存数据到数据库,也有可能用于删除数据库的某条记录,我们设计Button类时是无法确定点击(click)操作需要做些什么。

命令模式(command pattern)就是用于解决这个问题。


解决方案

假设应用CustomerService需要使用我们框架的Button,他希望点击(click)时就增加一条顾客信息到数据库。Button是完全不清楚用户APP要做什么的,事实上它是不必了解的,因为用户的需求是各种各样的,Button不能依赖于这些实际多变的需求。

我们让Button只依赖于一个Command对象,该对象只有execute方法,我们通过构造函数来传递这个command对象。当Button点击(click)时,就委托command对象调用execute方法执行。如下。

用户的应用则实现Command接口的execute方法,在实现的execute方法里,委托给实际具体的操作,比如下图的addCustomer。

这就是命令模式(command pattern)。

四人帮的命名稍有不同。如下。

命令模式将调用者或发送者与接收者解耦,调用者在完全不清楚接收者的情况下可以与接收者进行通话,这就是命令模式的重点。

命令模式的好处不仅于此。

因为Command对象代表各种请求,我们就可以做更多的情况,我们可以在代码中传递它们,我们可以将它们作为参数传递给我们的方法,我们可以将它们保存到列表中。比如,我们可以跟踪我们执行过的命令,这样可以重新执行或撤销某条命令,我们可以将命令保存到数据库里用于在未来某个时候执行。这样都是命令模式的好处。


解决方案

命令模式的代码非常简单。

创建接口Command,定义execute()方法。

Button类添加command对象,并在构造函数里进行初始化。在click方法里,委托command对象执行execute。

我们已用命令模式完成了Button的实现,Button是根本不知道调用它的用户要做什么的。用户可以是一个字符处理器,也可以是顾客管理程序或其他。

我们创建CustomerService来调用Button。

添加类CustomerService,它有实际的业务处理方法addCustomer,我们希望Button点击(click)时调用该方法。

添加类AddCustomerCommand,它有CustomerService对象service,实现了Command接口的execute方法,在该方法里,直接委托service执行addCustomer方法。

最后到Main类,将他们组合起来查看结果。我们看到,点击Button时(运行click方法),执行了CustomService的addCustome方法,达到了预期的结果。


复合命令

像photoshop这样的应用程序,允许我们记录操作命令,并可以在后面重放这些操作。利用命令模式实现这些场景非常容易。

举例说明。

创建命令ResizeCommand,表示调整大小的操作。

创建命令BlackAndWhiteCommand,表示应用黑白滤镜的操作。

我们希望能记录这些操作。

创建一个稍微不同的Command,名为CompositeCommand,它是多个Command的容器,当我们执行该CompositeCommand时,它将执行它所包含的所有Command。

CompositeCommand有一个类型为Command列表的对象commands,一个add()方法用于添加Command。在execute方法里,循环执行列表里的Command。

在Main类里测试。我们用CompositeCommand记录命令,再重放两次,结果如预期。


可撤销命令

命令模式另一个好处是让我们能够实现撤销机制。

我们创建接口UndoableCommand继承Command,并定义一个方法unexecute。

增加一个UndoableCommand的原因是并不是每一个Command都是可撤销的, 比如我们创建一个文字处理器,对于文字加粗的操作是可撤销的,但保存文档的操作说是不能撤销的。

我们之前介绍过备忘录模式也可用于撤销操作,那命令模式与备忘录模式在实现撤销操作上有什么区别?

备忘录模式在对象改变时存储对象的状态,所以会有很多时间点的快照,有时候成本会很高。比如一个视频编辑器,视频一般比较大,存储各视频状态的成本会很高,浪费内存,而使用命令模式则不会。命令模式知道如何撤销操作,我们就不必存储对象的各个快照点。比如调整视频大小,我们需要保存的是视频的原始尺寸而不是整个视频对象内容的快照。

我们来看看如何使用BoldCommand。它有一个prevContent,代表执行execute前的数据。

BoldCommand 需要与Document通话,Documen有实际的加粗(Bold)逻辑makeBold。BoldCommand也要与History通话,History有push和pop两个简单的方法。

BoldCommand的execute方法里,获取Document的内容(getContent)保存到prevContent,再加粗(makeBold)Document的文本,再调用History保存当前的BoldCommand对象。

BoldCommand的unexecute方法,简单地将保存的内容恢复到Document里即可。

我们添加一个类UndoCommand,调用History获取保存的Command,再调用获取的Command的unexecute。这就很类似我们在很多应用程序里看到过的“撤销”菜单。


代码实现撤销机制

假设我们要创建的文档编辑器是HTML编辑器。类HtmlDocument有content对象和makeBold方法。

接口 interface Command定义execute方法,接口interface UndoableCommand 继承Command并新定义了一个unexecute方法。

创建History类,它有双向队列 commands,以及方法push和pop。

创建BoldCommand用于连接类HtmlDocument的业务逻辑。BoldCommand实现了UndoableCommand,另有prevContent、doc、history,分别表示execute命令执行前的数据,要操作的文档,以及历史记录。在execute方法中,首先获取未加粗前的doc的内容保存,然后执行加粗操作,再将当前的命令状态保存到历史记录。在unexecute中,则简单地设置恢复为doc加粗前的数据。

最后在Main类中测试。

结果如预期一样是正确的。

不过直接使用BoldCommand的unexecute来实现撤回操作与实际情况不太符合,实际应用程序一般有一个通用的撤销菜单或按钮,而无需具体使用某个操作命令。

创建UndoCommand类实现Command接口,有一个history对象。在execute方法里,调用history的pop方法获取最近一个命令,然后执行unexecute,就能达到通用撤销的效果。

再回到main,撤销操作就利用通用UndoCommand操作即可。


小结

命令模式可以实现命令的发送者与接收者的解耦,并可实现复合命令和可撤销命令。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注