廉颇老矣,尚能饭否? --辛弃疾
引子
了解设计模式的人应该都多少听说过MVC模式。
严格意义上来说,“MVC模式”是一个伪概念,因为MVC并不属于设计模式,至少不属于GoF的23种设计模式之一,而更像是一个设计模式的结合体:V和C之间会实现观察者模式,M内部会实现单例模式,C在派发任务时会实现Command模式。
不得不说,MVC模式对软件的高可扩展性和高可维护性做出了巨大的贡献,这也使得MVC模式成为很多中等规模甚至大规模软件的常用框架,且经历了20余年仍旧在软件开发领域流行并通用,足可见MVC模式的经典。
但是传统MVC模式真的那么完美吗?
传统MVC的痛点
让我们一个个来说。
Controller:控制器,包含了项目的业务逻辑。但是也是被大家吐槽最多的一个,原因就是很多人,或者说大多数人,习惯于什么都往Controller里写,最后一个Controller超过1000行代码是司空见惯的事。所以关于传统MVC的第一个痛点就是,Controller过于臃肿。
Model:模型,包含了项目的数据模型。MVC定义之初,Model是核心,旨在使得同一个Model可以被复用到多个项目或者被复用到同一个项目的不同模块之中。但是在实际项目中,Model还承载着纯Model层内部的运算的工作,但是运算部分会项目的不同而有所区别,因此与项目的适配反而成为了Model可复用的枷锁。所以关于传统MVC的第二个痛点就是,Model变得不可复用。
View:视图,包含了项目所有的UI组件。视图本身没有什么好被大家诟病的,但是由于MVC中对于View和Controller界限的模糊界定造成了使用者在写代码的时候会觉得这部分代码放在View或者Controller里都可以的情况。例如事件的处理,组件的组合等。所以关于传统MVC的第三个痛点就是,View概念的模糊。
PureMVC
既然上文说的是传统MVC,那么可以判定PureMVC是一个新型MVC。
其实PureMVC只是相对于传统MVC(20年陈酿)来说“新”一些而已,因为PureMVC今年也已经有10年的历史了。
PureMVC是一款基于MVC的开源框架,最初是为基于ActionScript3的Flash,RIA程序开发的,后来被移植到16种语言平台上。
PureMVC分为标准版本和多核版本,后者为程序的模块化开发提供了支持。本文以标准版为例分析PureMVC。
PureMVC的MVC
PureMVC架构图
在PureMVC实现的MVC模式中,MVC分别由三个单例模式来管理,三者成为PureMVC的核心层。
Model与Proxy
Proxy(模式),提供了一个一个包装器或一个中介被客户端调用,从而达到去访问在场景背后的真实对象。Proxy模式可以方便的将操作转给真实对象,或者提供额外的逻辑。
在PureMVC中,Model保存了对Proxy对象的引用,Proxy去操作具体的数据模型(Data Object)。也就是说,Proxy管理Data Object以及对Data Object的访问。
View与Mediator
Mediator(模式),定义了一种封装对象之间交互的中介。这种设计模式被认为是行为模式因为它可以改变模式的运行行为。
正如定义里所说,PureMVC中,View只关心UI,具体的对对象的操作由Mediator来管理,包括添加事件监听,发送或接受Notification,改变组件状态等。这也解决了视图与视图控制逻辑的分离。
Controller与Command
Command(模式),是一种行为设计模式,这种模式下所有动作或者行为所需信息被封装到一个对象之内。Command模式解耦了发送者与接收者之间的联系。
在PureMVC中,Controller保存了所有Command的映射。Command是无状态且惰性的,只有在需要的时候才被创建。
Facade
与传统MVC模式不用的是,PureMVC中对于Model,View,Controller的调用是基于Facade模式的。
Facade模式,对应了GoF中的Facade模式,是一种将复杂且庞大的内部实现暴露为一个简单接口的设计模式,例如对大型类库的封装。
在PureMVC中,Facade是与核心层(Model,View,Controller)进行通信的唯一接口,目的是简化开发复杂度。实际编码过程中,不需要手动实现这三类文件,Facade类在构造方法中已经包含了对这三类单例的构造。
PureMVC各层之间的交互
View层的Mediator可以和Model层的Proxy进行互相访问,但是PureMVC设计之初是希望只有View依赖于Model,反之不成立。也就是View可以知道Model层有什么,但是Model层不需要知道View的任何内容。Mediator访问数据可以直接通过Proxy来完成,但是如果要对Proxy具体的内容进行加工,必须要通过Controller的Command来完成,这有助于实现View和Model之间的松散耦合。
如上文所说,Proxy最好不要直接调用Mediator来通知它请求完成,而是在异步取到数据之后,通过Notification来进行通知。Proxy只发送通知,不应该监听通知,因为Proxy属于Model层,不应该知道View层的状态变化。当然,Proxy应当对外提供数据变更的接口。
Command的实例化与执行只能由Controller来做。作为控制逻辑的执行体,Command有权拿到Proxy和Mediator的对象,并进行值加工,最后会将结果通过Notification发送给其它Command或者Mediator。
业务逻辑 VS 域逻辑
你可能会遇到这个问题:某段逻辑到底是应当放在Proxy(Model)里,还是应该放在Command(Controller)里?
其实这个问题可以引申为业务逻辑与域逻辑的区别。
- 业务逻辑 指的是那些需要协调Model与View的逻辑。
- 域逻辑 指的是仅仅是针对数据模型的操作,不论是对于客户端还是对于服务端,不论是同步的操作还是异步的操作。
因此,业务逻辑理所当然应该放在Command里来完成,而域逻辑应当放在Proxy里完成。
案例分析
这里以笔者实现的一个简单的计算程序为例来分解PureMVC。
PureMVC Demo
创建Facade
这里的关键点是实现startup方法和initializeController,示例如下:
代码语言:javascript复制ApplicationFacade.m
- (void)startup:(id)app
{
[self sendNotification:StartUp body:app];
}
具体的初始化方法放到了StartUpCommand中,包括创建视图,注册Proxy以及注册Mediator:
代码语言:javascript复制StartUpCommand.m
- (void)execute:(id<INotification>)notification
{
UIWindow *appWindow = [notification body];
ViewController *viewController = [[ViewController alloc] init];
appWindow.rootViewController = viewController;
appWindow.backgroundColor = [UIColor whiteColor];
[appWindow makeKeyAndVisible];
// register mediators
[facade registerMediator:[ViewMediator withViewComponent:viewController]];
// register proxys
[facade registerProxy:[ElementProxy proxy]];
}
创建ViewComponent和对应Mediator
本例中只有一个View,负责UI显示。当用户点击“=”时出发操作,此时内部将此事件抛到对应代理中,对应代码如下:
代码语言:javascript复制ViewController.h
@protocol ViewControllerDelegate <NSObject>
- (void)addNumberA:(CGFloat)numberA andNumberB:(CGFloat)numberB;
@end
ViewController.m
- (void)addTwoNumbers
{
if (self.delegate && [self.delegate respondsToSelector:@selector(addNumberA:andNumberB:)]) {
[self.delegate addNumberA:[self.inputA.text floatValue] andNumberB:[self.inputB.text floatValue]];
}
}
在对应Mediator中要关注四个方法:
- onRegister,负责给对应的ViewComponent添加事件或代理:
- (void)onRegister
{
[self.viewComponent setDelegate:self];
}
- listNotificationInterests,像Facade注册Mediator关心的Notification列表。当向Facade发送Notification时会遍历每一个Mediator的InterestList,会根据这个列表进行事件响应。
- handleNotification,一旦向Facade发送的事件命中listNotificationInterests列表则会回调到这个函数,此处应放接收事件后的逻辑。
- 实现对应ViewComponent的事件或者代理方法。本例中为
- (void)addNumberA:(CGFloat)numberA andNumberB:(CGFloat)numberB
方法。
创建DataObject和对应Proxy
本例中,DataObject只保存业务相关的变量,numberA,numberB,result。
本例中业务逻辑由于很简单,因此Proxy只封装了对DataObject中变量的存取以及变量是否可以操作的判断。
代码语言:javascript复制ElementProxy.h
@interface ElementProxy : Proxy
- (void)setNumberA:(NSNumber *)numberA andNumberB:(NSNumber *)numberB;
- (NSNumber *)getNumberA;
- (NSNumber *)getNumberB;
- (void)setResult:(NSNumber *)result;
- (NSNumber *)getResult;
- (BOOL)canOperate;
@end
创建Controller和Command
在PureMVC中,Controller已经在Facade的实例化中被隐式创建好,因此只需要创建对应的Command并且在Facade中进行注册即可。
代码语言:javascript复制- (void)initializeController
{
[super initializeController];
[self registerCommand:StartUp commandClassRef:[StartUpCommand class]];
[self registerCommand:AddTwoNumbers commandClassRef:[AddTwoNumbersCommand class]];
}
对应Command的逻辑:
代码语言:javascript复制- (void)execute:(id<INotification>)notification
{
ElementProxy *elmentProxy = (ElementProxy *)[facade retrieveProxy:[ElementProxy NAME]];
NSNumber *numberA = elmentProxy.getNumberA;
NSNumber *numberB = elmentProxy.getNumberB;
NSNumber *result = [NSNumber numberWithFloat:([numberA floatValue] [numberB floatValue])];
[elmentProxy setResult:result];
[facade sendNotification:ShowResult];
}
模块间交互顺序图
Sequence Diagram
如图所示,在接收到外部事件后,viewCompoent第一时间将事件抛到ViewMediator中,后者将事件相关变量存到Proxy进而存到了VO,也就是DataObject里。之后ViewMediator发送需要操作的命令通知addNumberNotification,Facade将此通知分配给实现注册好的addNumberCommand。Command从Proxy拿到相关变量后,运算,并将结果写到Proxy中,最后向Facade发送可以显示结果的通知showResultNotification。Facade将此通知转发给之前加过此通知到interest list的ViewMediator,Mediator从Proxy处取结果后把结果通过ViewComponent暴露出来的接口设置好,至此一次完整PureMVC交互流程完成。
猛回头
回到文章的开头,PureMVC到底如何解决了传统MVC的三个痛点?
Controller将操作逻辑细化为Command
根据PureMVC的最佳实践,Controller实体不需要单独实现,且Controller内部将每一个操作分割为一个个Command,这从根本上解决了Controller越来越臃肿的问题,强制用户将Controller里每一个操作细粒度化,使得代码可读性更强,维护性更高。
Proxy负责域逻辑,DataObject负责数据模型
PureMVC中,与域相关的逻辑和接口由Proxy来负责,后续的添加和修改接口只在Proxy中完成。而DataObject是完全对业务进行数据建模而产生的数据模型,与业务没有丝毫的关系,因此也保证了高可移植性。
ViewComponent只关注UI,其余的交给Mediator
PureMVC规定了ViewComponent只负责UI的绘制,而其他事情,包括事件的绑定统统交给Mediator来做。这也就避免了ViewComponent内部代码定义模糊,更不会和Controller的代码进行混淆。
后记
记得第一次接触PureMVC是在2009年左右,当时刚接触编程没多久的我读着师兄的解读一遍一遍的用actionScript进行实现,虽然没完全懂为什么有那些模块,模块之间为什么要那样通信,但是开始体会到框架的魅力和使用的乐趣。
随着工作年限的增加和编程经验的增长,越来越觉得这款框架固化了我很多正确的观念,这些观念渐渐的让我对之后的编程有了正确的感觉,所以PureMVC可以称得上是我框架方面的启蒙老师。
但是很遗憾的是,随着Adobe Flash平台的没落,这款在ActionScript上广为流行的框架也变的风光不再,即便它已经被翻译成16种程序语言。
所以我决定在时隔这么久重新学习这个框架,将框架运用到简单的例子中,解决在GitHub上没有可运行的iOS版本PureMVC Demo的尴尬情景。(官方Demo还停留在iOS3.0上)
希望教师节这天,我能帮我这位老师弹弹尘土,让更多的人重新关注到它。毕竟,好的框架值得任何一门语言来借鉴。
本文涉及代码地址
nimoment/PureMVC_practise
本文关联文章
你真的了解MVC吗
Reference
PureMVC Best Practise
Facade Pattern: WikiPedia
Mediator Pattern:WikiPedia
Proxy Pattern:Wikipedia
Command Pattern:WikiPedia