MVC模式是针对有相对复杂的用户交互应用的一种设计模式。由于产品迭代速度快,用户界面往往会发生重大变更,而业务逻辑也经常会因为用户的反馈而修正。
应用采用事件驱动,用户操作视图,视图产生相应事件,事件触发事件处理函数,事件处理函数执行业务逻辑,修改模型数据,模型数据通知视图数据已经修改,视图根据模型数据修正本身的表现。
然而糟糕的设计中,界面部分代码和事件处理函数往往混杂在一起,模型数据到处都可以引用修改,各部分耦合严重,当有某部分有变更时,往往发现牵扯的代码很多,小心翼翼的修改完每一处以后,又带来了未知数量的BUG。
在MVC模式中,严格把领域模型,控制器,和视图解耦,使得每一部分的变更都尽可能小的影响其他部分,在中大型项目中,也使技术水平不一的程序员可以在框架制约下,写出更具一致性的代码(个人认为这一点比前者带来的好处更诱人)。
下图是传统MVC模式中模块之间的通信图:
如果你尝试过在项目中引入MVC模式,会发现很多时候,你会有职责划分的困扰:
-
- 事件触发后,并不单纯只是通过控制器修改模型数据,有时需要修改视图的展现,甚至只是修改视图的展现,那这部分职责也要划分到控制器中去吗?(那对于用户交互复杂的应用来讲,控制器的职责未免过多了吧?)
- 视图通过事件处理函数引发控制器执行逻辑,则必须要保存会控制器的引用,如果把所有的逻辑统统写到事件处理函数中呢?(好吧,假如你要更换界面库呢?)
- 模型数据可能有多种,每种模型数据也有多种可以执行的操作,那控制器到底要做到多大粒度?(一对多?多对多?)
- 模型更新后对视图的通知,由模型保存视图的引用来进行通知?通过事件机制来通知?(不管如何又带来了视图和模型之间的耦合)
想象一下在一个多人协作的项目组中,部分程序员负责构建领域模型,部分程序员 负责构建业务逻辑,部分程序员负责界面展现,全部资源按照特长各尽其职,但是不管是糟糕的设计,还是传统的MVC模式,所有部分的工作都必须在得到其他部 分工作的支持之后才能完成。然后不管是项目进度还是办公室又变成了一片狼藉。
针对传统MVC的问题,pureMVC将各部分进一步解耦,并以最简单的形式把经典设计模式应用到设计中,在代码构建中,为小中型项目提供了构建基础。当然,任何收益都是有代价的,对于pureMVC所引入的问题,本文将会在文章末尾和大家讨论。
为了解决上面提到的问题1和2,pureMVC引入Mediator模式解耦视图和控制器,Mediator负责接收视图事件,执行必要的视图展现逻辑和通过控制器来执行业务逻辑。
针对问题3,pureMVC引入了Command来形成自然而然的对应关系,每个Command对应某一操作,通过简单的修改,我们还可以得到附加的收获,支持撤销和重做。
针对问题4,pureMVC通过通知机制来解决传统事件机制的一些问题,程序员只需要注册通知和命令的对应关系,对每种可接收通知的类型,罗列它们感兴趣的通知类型并编写通知处理就可以了。
pureMVC还在控制器和数据模型之间引入了Proxy来解耦,这样可以自然的解决远程数据的存取,也为模型数据改变通知的发出找到了责任对象(由Proxy验证域逻辑,执行操作,并发出模型数据更新通知)。
下面是pureMVC框架的模块示意图:
这样,用户操作通过Mediator,执行了必要的展现逻辑之后,发出通知;通知触发一个命令来执行相应的业务逻辑修改模型数据;模型数据发出通知,通知所有感兴趣的视图根据修改过的数据来更新视图。
这样,负责不同部分的同事只要事先约定系统中有哪些通知,并约定一些命名规则,就可以并行进行自己的工作,单元测试并按计划交付整合了。
下面是pureMVC新引入模式的通信图:
说了这么多,可能有些同行还是摸不清思路,下面我们用一个实际的构建过程来说明一个基于pureMVC框架的应用是如何运作的。
pureMVC是一个开源项目,该框架在不同平台下的代码都可以在官方网站找到,需要的同行可以找来配合本文阅读。
假设我们在.NET平台下用C#开发一个动画编辑器项目,在对动画每一帧的编辑行为中,自然的划分出了以下职责对象:
- Control:用户视图组件,负责呈现已经编辑完成的帧并接受用户操作
- FrameEditMediator:负责接受事件,执行必要的视图展现逻辑更在必要时形成命令修改帧数据,接受数据模型更改通知并更新视图展现
public Edit() : base("Edit") { size = new Size(240, 320); scale = 1; BufferedPanel panel = new BufferedPanel(); panel.Location = new System.Drawing.Point(0, 0); panel.Size = size; panel.TabIndex = 0; panel.Paint += new System.Windows.Forms.PaintEventHandler(this.frameEditor_Paint); panel.MouseDown += new System.Windows.Forms.MouseEventHandler(this.frameEditor_MouseDown); panel.MouseMove += new System.Windows.Forms.MouseEventHandler(this.frameEditor_MouseMove); panel.MouseUp += new System.Windows.Forms.MouseEventHandler(this.frameEditor_MouseUp); panel.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.frameEditor_MouseWheel); m_viewComponent = panel; } public override IList ListNotificationInterests() { List intereste = new List(); intereste.Add(Shelf.SelectedChanged); return intereste; } public override void HandleNotification(INotification notification) { switch (notification.Name) { case Shelf.SelectedChanged: { Panel panel = (Panel)m_viewComponent; IProxy proxy; proxy = Facade.RetrieveProxy("Group"); group = (Group)proxy.Data; proxy = Facade.RetrieveProxy("Animation"); animation = (Animation)proxy.Data; proxy = Facade.RetrieveProxy("Frame"); frame = (Frame)proxy.Data; panel.Refresh(); } break; default: break; } } private void frameEditor_MouseDown(object sender, MouseEventArgs e) { Panel p = (Panel)(sender); if (e.Button == MouseButtons.Left) { p.Focus(); } } private void frameEditor_MouseMove(object sender, MouseEventArgs e) { } private void frameEditor_MouseUp(object sender, MouseEventArgs e) { } private void frameEditor_Paint(object sender, PaintEventArgs e) { //todo: } private void frameEditor_MouseWheel(object sender, MouseEventArgs e) { //todo: } private Size size; private int scale; private Group group; private Animation animation; private Frame frame; }
AddSegmentCommand:对每一帧在指定位置添加指定图形切片的命令
{ public AddSegmentCommand() { } public override void Execute(INotification notification) { //todo: } }
FrameProxy:验证命令合法性,对真正的模型数据执行修改,当修改成功后,发出数据更新的通知
{ public const String SegmentAdded = "FrameProxy.SegmentAdded"; public const String SegmentRemoved = "FrameProxy.SegmentRemoved"; public const String SegmentOrderChanged = "FrameProxy.SegmentOrderChanged"; public FrameProxy() : base("Frame", null) { } public void AddSegment(int templateIdx) { if (m_data != null) { Frame frame = (Frame)m_data; frame.AddSegment(templateIdx); Facade.SendNotification(SegmentAdded); } } public void RemoveSegment(int idx) { if (m_data != null) { Frame frame = (Frame)m_data; frame.RemoveSegment(idx); Facade.SendNotification(SegmentRemoved); } } public void UpSegment(int idx) { if (m_data != null) { Frame frame = (Frame)m_data; frame.UpSegment(idx); Facade.SendNotification(SegmentOrderChanged); } } public void DownSegment(int idx) { if (m_data != null) { Frame frame = (Frame)m_data; frame.DownSegment(idx); Facade.SendNotification(SegmentOrderChanged); } } public void TopSegment(int idx) { if (m_data != null) { Frame frame = (Frame)m_data; frame.TopSegment(idx); Facade.SendNotification(SegmentOrderChanged); } } public void BottomSegment(int idx) { if (m_data != null) { Frame frame = (Frame)m_data; frame.DownSegment(idx); Facade.SendNotification(SegmentOrderChanged); } } }
Frame:真正的帧数据
class Frame { public Frame() { segments = new ArrayList(); rects = new ArrayList(); Delay = 1; } public Segment AddSegment(int templateIdx) { Segment sec = new Segment(templateIdx); segments.Add(sec); return sec; } public Segment GetSegment(int index) { if (index >= 0 && index < segments.Count) { return (Segment)segments[index]; } else { return null; } } public void RemoveSegment(int segIndex) { segments.RemoveAt(segIndex); } public void UpSegment(int segIndex) { if (segIndex < segments.Count - 1) { Object temp = segments[segIndex + 1]; segments[segIndex + 1] = segments[segIndex]; segments[segIndex] = temp; } } public void DownSegment(int segIndex) { if (segIndex > 0) { Object temp = segments[segIndex - 1]; segments[segIndex - 1] = segments[segIndex]; segments[segIndex] = temp; } } public void TopSegment(int segIndex) { if (segIndex < segments.Count - 1) { Object temp = segments[segIndex]; segments.RemoveAt(segIndex); segments.Add(temp); } } public void BottomSegment(int segIndex) { if (segIndex > 0) { Object temp = segments[segIndex]; segments.RemoveAt(segIndex); segments.Insert(0, temp); } } public int SegmentCount { get { return segments.Count; } } public int RectCount { get { return rects.Count; } } public int Delay { set; get; } private ArrayList segments; private ArrayList rects; }
用户通过拖拽大图的切片到编辑区的视图,FrameEditMediator 负责接受视图事件,并发出值为”AddSegment”的通知,通知触发AddSegmentCommand命令来执行业务逻辑,计算相应坐标之后,找到 FrameProxy执行对Frame的操作,FrameProxy验证了合法性之后,执行操作,发出帧更新通知,因为 FrameEditMediator对该通知感兴趣,它接受到之后,更新视图,一个完整的修改操作就完成了。
下面我们来讲一下pureMVC所带来的问题。首先,它的通知机制基于字符 串,字符串固然灵活,但对命名冲突之类的问题不好控制,这个可以通过严格的命名规则来解决;其次,通知不定向,会带来调试上的困难;再次,性能问题,由于 强制解耦,系统中的对象众多,对象往往通过字符串来索引,所以,在游戏设计这种以帧为逻辑执行单位的应用中,务必要注意性能问题。
不管仍然存在哪些问题,在项目的实际应用中,该框架仍然带来了意料之中的好处,使得应对需求变更及时,后期维护简单。当然,框架只是工具,心中有框架,手中无框架才是真正的高手。希望本文和pureMVC能够给各位同行的工作带来启发。
- 图片来源:http://java.sun.com/blueprints/patterns/MVC-detailed.html
- PureMVC是一种MVC框架,最初使用ActionScript 3实现,现在在多种语言平台上有实现版本。官方站点:http://puremvc.org/