假设以下场景:
Application分为左右两部分:左边是文件夹树,右边是当前选中的文件夹下的所有子文件夹。
新增功能:
在文件树上和右边的列表面板上,添加对文件夹的CUD操作,当操作成功后,需要同时更新左右两部分的视图。(这里忽略CUD的用户界面)
- 如果新增一个文件夹,新增成功后在文件夹树和右边的面板上都要显示这个新的文件夹。
- 如果修改一个文件夹名,成功后两边的视图都需要更新这个文件夹的名字。
- 如果删除一个文件夹,左边的文件夹需要删除不显示此文件夹,而且缺省选中上一级的文件夹,如果没有上一级文件夹,则选中顶层的第一个文件夹;右边的视图也需要相应的显示选中的文件夹下的子文件夹。
假设左边的文件夹为PanelA,右边的列表面板PanelB,CUD操作对应的命令分别为CCommand,UCommand和DCommand(这里的命令代表了请求数据和处理数据的逻辑模块)。
于是问题可以分解为:
- PanelA和PanelB执行三条命令。
- 命令执行完毕后,PanelA和PanelB进行更新。
- PanelA上选中某文件夹,PanelB进行更新。
执行命令
先看分解问题1,由于PanelA和PanelB都需要执行三条操作(CUD),很自然把这三条操作封装成三个命令,也就是上面的CCommand,UCommand和DCommand。
我们先看上面分解的问题1,一个视图需要执行一个命令,一个简单地方法是由视图直接create一个命令实例,然后执行。如下面伪码所示:
var cmd:ICommand = new CCommand();
cmd.execute(some parameters);
|
所以,方案必须是把视图和命令解耦。这需要在视图和命令之间建立一种“协议”,“协议”传输执行命令所需的所有参数。Adobe有一个轻量级的解决方案,
Cairngorm.
Cairngorm
Cairngorm是一个比较简单的微框架,基本思想是把命令的请求者(如视图)和命令的执行者分开。
Cairngorm包括
CairngormEvent,FrontController和Command三部分,其中的
CairngormEvent就是上面说的“协议”,Command就是上面提到的三个命令,而对这两者进行管理的就是
FrontController.
由上图,PanelA和PanelB只需要构造对应的
CairngormEvent(一般做法是扩展
CairngormEvent类,然后
传输执行CUD命令所需的参数),并dispatch
CairngormEvent就可以了。
FrontController将根据事件的类型执行对应的Command。
由于视图和命令分离了,如果创建操作需要使用新的命令(由CCommand改为CCommand2),只需要修改
FrontController中的映射关系即可。只要“协议”不变,无需修改视图代码。
//
FrontController代码
addCommand(CreateEvent.CREATE_FOLDER, CCommand);
// CreateEvent代码
public class CreateEvent extends
CairngormEvent
{
// some parameters
}
// PanelA/PanelB代码
var evt:CreateEvent = new CreateEvent(some parameters);
evt.dispatch();
|
如何更新视图
回到上面分解的问题2,当命令执行完毕后,视图需要作出更新。
一个直观的方法是由命令直接调用视图的更新接口。此做法存在一个致命问题:
命令必须要知道有哪些需要刷新的视图,如本文场景中的PanelA和PanelB,要达到这种效果,“协议”中必须传输PanelA和PanelB的引用,如果要增加一个刷新的视图,必须修改“协议”,
这和我们解耦的目的是南辕北辙的,也会增加代码的维护工作量。
传统的观察者模式
对于此种情况,一般比较好的做法是利用观察者模式。观察者模式的本质是,将更新的主动权归还给更新的主体(如上面的PanelA),而更新的发起者只需要发出一个通知。
对于此种情况,一般比较好的做法是利用观察者模式。观察者模式的本质是,将更新的主动权归还给更新的主体(如上面的PanelA),而更新的发起者只需要发出一个通知。
现在来看一下传统的观察者模式能不能解决那个“致命问题”。首先看一张观察者模式的UML图:
经典的GoF的的观察者设计模式
ConcreteSubject就是上面提到的命令(如CCommand),ConcreteObserver就是PanelA、PanelB,我们注意到,PanelA和PanelB都必须实现同一个接口Observer,而Subject必须要先attach这些Observer对象。也就是说,CComamnd必须要保存PanelA和PanelB的引用,尽管是以Observer接口的形式,并不知道具体类。要做到这一点,要不CCommand要通过协议知道视图的引用,要不视图要知道最终执行的命令。这些都不是我们想看到的。
改良的观察者模式
我们把Subject抽象成一个类似于Mediator的Controller,如下图。PanelA和PanelB都需要在Controller中进行attach,CCommand执行完毕后需要通知更新时,不是直接调用PanelA/PanelB的更新接口,而是调用Controller的更新接口,这样,CCommand和视图就解耦了。
先看Command侧的具体做法。Command需要通知Controller,需要两个信息:一个是更新信息,如创建文件夹成功后的附加信息(时间等);一个是通知的类型,也就是要让Controller通知哪些视图。这里我们可以仿造解决问题1的做法,利用一个“协议”,我把它定义为ResponseEvent。
public class ResponseEvent extends Event
{
public var data:Object;
public var type:String;
}
|
显然,这里的Controller是一个单例类。下面的示例代码忽略的单例类的构成方法,而且使用了一个内置的
IEventDispatcher对象作为存储类。
public final class CommandController
{
public function CommandController
(single:Singleton)
{
_eventDispatcher = new EventDispatcher();
}
private var _eventDispatcher:IEventDispatcher;
public function addEventListener(type:String, func:Function):void
{
_eventDispatcher.addEventListener(type, func);
}
public function removeEventListener(type:String, func:Function):void
{
_eventDispatcher.removeEventListener(type, func);
}
public function dispatch(event:ResponseEvent):void
{
_eventDispatcher.dispatchEvent(event);
}
}
|
- 没有FrontController,Cairngorm需要在FrontController中预先初始化所有的CairngormEvent和Command之间的映射关系。如果需要动态增删映射关系,需要知道FrontController的引用,比较麻烦。所以上面的做法是相应把FrontController底层的CairngormEventDispatcher拿出来用。这样就可以随时增删映射关系。
- 这样做还有一个好处,就是不需要实现Cairngorm的ICommand接口,直接使用回调方法。
// PanelA/PanelB代码
CommandController.getInstance().addEventListener(CreateResponseEvent.CREATION_COMPLETE, update);
public function update(event:ResponseEvent):void
{
// 更新视图
}
// 命令代码,执行完毕后
var evt:
CreateResponseEvent = new
CreateResponseEvent(
CreateResponseEvent.CREATION_COMPLETE);
evt.data = data;
CommandController.getInstance().dispatch(evt);
|
当然,直接使用
Cairngorm的
CairngormEventDispatcher也可以达到相同的目的。
视图间的通讯
最后分解问题3,是一个视图之间通讯的问题。GoF的做法是用中间者模式,但如果只是PanelA控制PanelB,则还是可以使用观察者模式,直接使用问题2的解决方法。
(全文完)