游戏开发中一块必不可少的模块就是游戏界面(User Interface,俗称UI)的制作,即UI在接收到用户的输入之后,如何进行响应的问题,这个问题我们称之为UI交互逻辑的实现(当然,下面的这些设计模式其实不仅仅局限于游戏UI,一些常见的应用场景甚至包括网页交互、应用UI交互、前后台交互)。
UI交互逻辑在实践中的复杂度远比上面一句话描述的要高,主要需要解决如下的一些问题:
- 如何保证整体的交互逻辑清晰、简洁、易维护
- 如何保证多个关联UI之间的响应逻辑能够实现很好的分离与解耦
- 如何做到职责分离,将展示的相关逻辑与底层的数据逻辑做到很好的隔离
而在长期的实践过程中,UI交互逻辑积累了一些成熟易用的设计模式,下面来对这些设计模式的具体方案以及优缺点进行叙述与对比分析。
1. Model View Controller(MVC)
MVC模式将整个交互逻辑分为三个部分,即Model,View以及Controller:
- Model对应的是整个交互逻辑中的底层数据,负责整个系统的数据表达以及底层系统的交互逻辑(这里的交互逻辑是指应用层面的相关逻辑,而非UI交互界面的相关逻辑),这个部分的数据跟UI是完全解耦的
- View对应的是整个交互逻辑中的上层视角,负责整个系统的视觉表现,比如游戏界面中与单个界面关联的Class就可以看成是一个View,最简单的示例就是与一个按钮关联的Class,需要注意的是,同一个Model可以对应于不同的View(当然也就对应于不同的Controller,在MVC模式中,View跟Controller通常是成双成对的),用通俗的话来解释就是,同样一份数据可以有不同的表达形式,比如可以用表格来表示,也可以用曲线来表示。
- Controller对应的是整个交互逻辑的具体实施细节,负责链接Model跟View,将Model中的数据反应到View上面,接收到View上的相关输入并将之翻译成Model中的数据变动
整个逻辑Working Flow可以用下图表示:
相关的交互逻辑用文字表达如下:
- Model负责管理整个应用的数据,包括业务逻辑的处理与存储,数据更新等。Model只从Controller处接收用户的输入;Model的数据变化不需要通过Controller转发给View,而是通过观察者模式,自动转达到View上。
- View负责将Model用一种用户可以直观感受的方式绘制或者展示出来,可以理解为展示给用户的界面相关的逻辑代码
- Controller负责从View处接收到用户的输入事件,经过有效性验证后根据不同的事件类型转达至Model处,完成对Model的调整与修改。由于Controller对View的实现不需要关心,只需要进行被动转发,因此一个Controller可以对应多个View
MVC中的交互流程有两种模式:
- 第一种模式中,用户的输入直接作用在View上,通过View->Controller->Model的方式完成整个交互链路,如下图所示:
- 第二种模式中,用户的输入通过某种其他方式直接作用在Controller上,比如系统应用检测到其他的数据变动之后直接调用Controller上的相关接口,完成Controller->Model->View的交互链路,如下图所示,需要注意的是,在这种模式下,Model的数据变化会通过调用View的接口完成数据到View的同步(通常是通过observe+listener的方式实现):
observer同步模式实现model到view更新的好处在于,如果有多个view同时指向同一个model,不过分别负责model的不同部分,那么在某个view更新了自己负责的model部分之后,另外的view在observer的模式下不需要进行更新响应;而如果我们使用更为直接的flow同步模式,在model发生变化的时候发起对所有的controller的相应接口的调用,就会变得浪费。
不过observer同步模式的不足之处在于代码跳转逻辑隐藏比较深,在追查bug的时候效率会比较低。
总结来说,MVC有如下的一些优点:
- 很好的实现了View跟Model的分离,业务逻辑全放在Controller中,具有较高的模块化程度,当业务逻辑发生变化时,不需要改动View跟Model,只需要调整Controller即可
- 将用户响应逻辑分解成了Controller跟View两个部分,前者负责根据用户的输入完成相应的交互逻辑,后者负责将底层的应用数据以直观地方式展示给用户,Controller不会直接改动到View,Controller到View的通信需要通过Model来完成
- 支持多个View绑定到同一个Model中(比如通过观察者模式进行监听),且保持多个View之间的响应是相互独立互不干扰的
其缺点为:
- 因为View的同步是根据观察者模式进行监听的,而View只能在有UI的环境下运行,因此单元测试以及调试较为困难
- View是与Model关联的,对于不同的Model而言,View很难做到复用
2. Model View Presenter(MVP)
MVP其实就是在MVC的基础上将Controller替换成Presenter,是MVC的一个变种,其整体的Working Flow可以用下图表示:
在MVP模式中,Presenter发挥的是”中间人“的作用,所有展示(presentation)相关的逻辑(即将Model的数据变动反应在View上的相关逻辑)都会交由Presenter来完成(不论是从Model到View,还是反过来,都是如此),也就是说,Model跟View之间不再发生任何的交互,而这正是MVP跟MVC的关键区别。
MVP模式下,用户的操作以及后续的触发逻辑可以用如下的流程示意图说明:
在MVP模式中,有多种实现方案:
- 一个极端也是最常用的模式就是采用Passive View模式,在这种模式中,View只用作一个转发器,是一个非常轻量的Class,没有任何主动的逻辑,都是在响应用户与Controller的输入,这也是为什么叫Passive View的原因。
- Supervising Controller模式,在这个模式下,Presenter会将一部分简单的逻辑放在View中,一些复杂的高层次的逻辑还是放在Presenter中,因此可以认为是一种监督式的Controller模式,在实际工作中,这种模式使用较少
Passive View模式下,View在接收到用户输入后,通过一个方法调用Presenter的对应方法进行响应,完成对Model数据的更新,并根据更新后的Model数据完成对View的修正;在View的初始化逻辑中,可以同步创建一个对应的Presenter,后续的所有逻辑都交由Presenter进行响应,下面给出一份C#示例代码:
public class DomainView : IDomainView
{
private IDomainPresenter _domainPresenter = null;
/// <summary>Constructor.</summary>
public DomainView()
{
_domainPresenter = new ConcreteDomainPresenter(this);
}
}
前面说过,View到Model的调用是通过Presenter完成的,这种调用转达通常是通过View持有Presenter完成的,而后面Model数据的变化通知给Presenter通常是通过observer模式实现,而Presenter调用View的相关接口进行更新则通常是需要Presenter持有View的成员变量,也就是说,View跟Presenter通常是相互持有的。
MVP的优点有:
- V跟M分离,降低耦合,且由于View不需要关心Model的数据,因此可以实现View的重用
- 模块职责划分更为明确,增强代码可读性
- Presenter可以被多个View所共用,因为View的变化比Model变化频率高,通过这种方式可以有效增强代码复用能力
- 对视图而言,隐藏了数据,简化了View的逻辑
- 由于主要的功能逻辑已经都放在Presenter中,不需要依赖于View了,所以可以更好的进行单元测试
MVP的缺点有:
- View跟Presenter之间的交互过于频繁
- View跟Model的数据同步需要手写逻辑实现,当View较为复杂时,开发成本与维护成本都会上升
- 一些复杂的View中会导致Presenter过于臃肿
对于一些复杂的View来说,为了减轻Presenter的负担,可以在Presenter跟View之间增加Mediator,在Presenter跟Model之间增加Proxy:
MVC与MVP的区别点有:
- View跟Model不再直接交互,而是通过Presenter完成交互逻辑
- View跟Presenter通常是一一对应,但是一些复杂的View可能需要多个Presenter来进行承接(所有的复杂逻辑都放在Presenter中,如果View过于复杂就会导致Presenter的臃肿,为了瘦身增效,可以考虑将之拆分成多个Presenter),而MVC中,Controller可以被多个View所共享,且Controller可以通知哪些View可以被显示出来
3. Model View View-Model(MVVM)
在MVP模式中,View跟Model的变动是通过View与Presenter的接口调用实现的,当需要处理的UI元素不断增加,需要处理的Model数据不断增加,就会导致相应的处理接口数目不断增加,导致开发与维护成本上升。为了解决这个问题,MVVM通过View-Model完成View跟Model之间的双向绑定(这个逻辑通过一个叫做binder的东西实现),从而在两者任何一方发生变化的时候,都能自动完成另一方的数据转达通知与同步,避免了手写大量同步代码的低效。
MVVM的流程示意图给出如下:
这是一种从MVP模式中演变过来的设计模式,从上图看来,MVVM跟MVP没有太大区别,但实际上我们前面说过,MVP中V跟P是相互持有来实现数据同步的,而在MVVM中,两者(View跟ViewModel)的关联与同步则是通过DataBinding实现(这种binding可以通过反射或者其他的方式来自动完成,降低手写代码的复杂度),具体而言,就是View跟View-Model通过双向绑定(即View-Model的变化会反映到View上,而View的变化也会反映到View-Model上,这个绑定通常是通过观察者模式完成)实现了View跟View-Model数据的自动同步,避免了人为的干涉以及开发者对于View/Model之间复杂数据同步之间的关注:
- View-Model负责监测Model的变化,并根据变化对View进行更新,在实现上,这一步可以通过事件监听或者Data Binding完成(如DataChangeNotify)
- View-Model负责监听View的变化,并将View的变动反映到Model上,在实现上,这个通常是通过事件监听完成
MVVM的优点在于:
- 除了Model可以继承MVC/MVP的可复用性之外,在部分情况下,View-Model也可以被复用
- 通过数据绑定实现了Model跟View的自动关联,Model跟View无需编写过于复杂的更新逻辑,所有的更新同步统一在View-Model中完成
- 数据同步逻辑通过双向绑定实现,可以保证View的代码简洁
MVVM的缺点在于:
- 数据绑定使得调试变得困难
- 数据绑定导致代码重用较为困难
- 如果Model较大会使得数据的释放存在困难,从而导致内存消耗增加
另外,值得一提的是,MVC/MVP/MVVM三种模式中,由于都增加了中间用于同步的转发层,因此Model数据不必来自于同一个Class,甚至于同一个中间转发层可以被多个View所共享。
总结
MVC起于草莽,引入了分层概念,不过Model跟View之间还是存在耦合,维护起来存在困难。
MVP在MVC的基础上做了进一步的解耦,隔离了Model跟View之间的交互,不过Model跟View之间的数据同步需要手动添加接口实现,开发与维护成本较高。
MVVM在MVP的基础上引入了双向绑定机制,使得Model跟View的同步可以自动完成,从而解放了开发者的双手,提升了开发的效率。
MVP跟MVVM完全隔绝了Model跟View,所有的同步都是通过中间层经过拷贝进行转达,不过有时候有些数据拷贝的开销太大,可能也会考虑打破View跟Model的界限,完成两者的直接同步,也就是说,在实践中,我们会发现,有时候模式并不是完全清晰的,而是多种方案混杂一炉的。
上面给出的MVC、MVP以及MVVM是狭义下的设计模式,从广义上来说,因为MVC模式就是实现Model跟View的分离,因此三者都可以归纳在MVC模式下。
参考
[1]. 谈谈MVC模式
[2]. MVC,MVP 和 MVVM 的图示
[3]. Model–view–controller
[4]. Model–view–presenter
[5]. Model–view–view model
[6]. GUI Architectures
[7]. MVVM 介绍
[8]. MVVM模式理解
[9]. MVC,MVP 和 MVVM 模式如何选择?
[10]. Android高精战争(MVC、MVP、MVVM)
[11]. MVC、MVP、MVVM的区别及联系













网友评论