书籍推荐

对于学习设计模式,笔者除了通过网络的方式,还有通过看书的方式。设计模式的书籍,笔者有过两本。

  • 一本是《漫谈设计模式-从面向对象开始》,清华大学出版社,作者是刘济华,使用的是Java语言。可能是由于写作风格的问题,我这本书我没怎么看的进去,只粗看了一些。
  • 另一本是《Head First设计模式》,中国电力出版社,**这本书我超级、超级、超级推荐。**这本书是我在前公司的书架上面找到的,我一边写代码,一边看完了(感谢前司)。这本书图文结合,图片加入了很多手绘风格的文字注释和小图解析,并且使用交谈式的风格,写的通俗易懂。Head First系列丛书,我还看过另外两本,我非常喜欢这个系列,在此也推荐给大家。如果大家要买书的话,可以考虑一下二手的(笔者后面自己买了一本,就是二手的)。
  • MVC模式在《Head First设计模式》也有专门的解析,本文所谈的设计模式也是基于这本书的描述。

设计模式

MVC是多种设计模式的结合,会实现这几种设计模式:观察者模式、组合模式、策略模式和中介者模式。

  1. 观察者模式。MVC必须要实现一个观察者模式(或叫做,发布-订阅模式),即事件机制。观察者模式一般不按那些教程或书籍描述那样实现,而是将事件统一注册在唯一的一个地方,正如上一篇某些评论所讨论的那样。下面的代码,可能是其中的一种写法(如果初学者不熟悉
登录后复制
class EventMgr{ //事件机制,全局
    //事件列表
    private eventMap: Map = new Map();
    //监听事件
    public on(eventId: string, callBack: Function, thiz: any): void { //省略代码}
    //取消监听事件
    public off(eventId: string, callBack: Function, thiz: any): void { //省略代码}
    //派发事件
    dispatch(eventId: string, ...args: any) { //省略代码}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 观察者模式的好处是解耦。通信的两个类实例无需知道彼此,也不会有任何依赖关系,其中一个通过key监听事件/取消监听事件,另一个通过相同的key派发事件即可。
  1. 组合模式。对象之间有“整体/部分”的关系,并且要用一致的方式对待这些对象。组合模式可以将对象组合成树形结构来表现“整体/部分”层次结构。树的组合节点和叶节点能够以统一方式来处理。
  • 游戏的界面由游戏控件组成,对于大部分游戏引擎来说,游戏控件的组合方式就是组合模式。运行中的游戏控件节点,组成了一颗节点树。
  • 对于使用组件模式的cocos来说,transform组件既可以让一个node成为另一个node的子节点,也可以让同一个node成为第三个node的父节点。这可以用统一的方法进行处理
  • 如果你也用过白鹭(egret)引擎,就会知道label和image,是不能添加子节点的。但egret制作的游戏,也会组成一颗节点树,只是它的组合节点和叶节点没有用统一的方式处理
  • 在不少项目,并没有对单页界面与多级菜单(一级菜单、二级菜单、三级菜单)界面进行统一的处理,切换页签逻辑的复用,一般是通过复制代码的方式来进行的(某些项目也会通过公共组件的方式)。但这种界面结构,也可以利用组合模式来进行设计。如此,菜单的深度理论上是可以无限多级的,而无需区分一级菜单,二级菜单还是三级菜单。笔者开发了一个适合初学者的框架,里面就用组合模式实现了单页界面与多级菜单界面的统一处理,感兴趣的,可以添加笔者的qq获取。在下一篇-实践篇,笔者也会对这个框架进行一些解析。

MVC在游戏前端的应用,第二篇之设计模式篇_第1张图片

  • 图1 组合模式
  1. 策略模式。在《Head First设计模式》一书中,作者举了一个例子,如下。
  • view是一个显示节拍的界面,controller可以操作model,使其改变状态或数据,也可以调用view的方法,使其改变显示。
  • 这个view实现了音乐节拍的显示和变化
  • 当我们有新的controller和新的model,实现心脏的跳动逻辑,这个view可以不用改动任何代码便可复用,用做心脏跳动的显示和变化。新的controller和新的model分别实现了view调用的原有controller和model的接口。
  • 这里有两组controller和model,就是两个策略。策略模式使得view的代码能够得以复用。

MVC在游戏前端的应用,第二篇之设计模式篇_第2张图片

  • 图2 策略模式
  • 对于mvc中策略模式在游戏中的应用,可以是成长系统、某些运营活动的复刻、某些公共的界面(如道具购买界面,卡牌中的布阵界面)等。
  1. 中介者模式。
  • view可以监听用户的输入,然后将其交给controller处理。controller负责与外部进行交互。从这个角度来说,controller是中介者。
  • 但controller也不是完全的中介者,因为view也可以直接从model取得它的数据和状态。但是如果设计为view只能通过controller获取model的数据和状态,那么controller就是完完全全的中介者了(当然,不一定非要如此设计)。

最后的讨论--mvc是按模块划分,还是按界面划分?

  • 从策略模式来说,mvc按界面划分,比较容易更换策略。
  • 《Head First设计模式》一书中给出的例子是一个视图搭配一个控制器,但它最后也提到,可以让一个控制器类管理多个视图。
  • 但如果mvc按界面划分,必然会引起类的增多和文件的增多,略有繁琐。那折中呢,对于复杂的系统模块,controller不再只有一个,可以分为2个或3个?
  • 如果mvc按界面划分,那么controller和model合并,是一种合理的做法。
  • 笔者认为比较好的实践是:
  • mvc按界面划分,然后model与controller合并成一个xxxController文件。当模块内有公共逻辑方法时,可以把它放到另一个对外的公共文件内,例如xxxModule(module的翻译是模块),而每个需要该方法的xxxController再进行调用(中介者模式)。

最后的最后

  • 这只是笔者的一些看法,如何设计并没有优劣之分。
  • 事实上,笔者见过一些比较赚钱的项目,对于mvc的运用,也是只有最基本的职责区分而已。也有不少项目是model与controller合并的。

上一篇的回顾

  1. 评论其实大多数还是从代码分离或责任分离的角度出发,从这个角度来说,各个评论都是有道理的,只要自己项目定好规则,都没有错,mc合并也没有问题。 但我想重点指出的是上篇提到mvc的几个问题中的第3点--不注重mvc之间的依赖,将依赖的单例实例写的到处都是,这里重新贴一下上一篇的相关代码。
登录后复制
class controller_a { //控制器a
    /**单例模式,省略了实现 */
    static get_instance() { }
    ca_func_a() { }
}

class controller_b { //控制器b
    /**单例模式,省略了实现 */
    static get_instance() { }
    cb_func_b() { }
}

class view_a {  //界面a
    v_func_a() {
        // ...省略其他代码。**注意!!**这里直接调用了controller_a的单例实例
        controller_a.get_instance().ca_func_a();
    }
    v_func_b() {
        //...省略其他代码。**注意!!**这里直接调用了controller_b的单例实例
        controller_b.get_instance().cb_func_b();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

view的代码直接调用了controller_a的单例实例和controller_b的单例实例,并且这种调用可能分布在view的多个地方,这种写法在不少项目中是很普遍的,这个也确实使得代码难以复用和扩展,因为view的实例和controller的实例之间紧紧的耦合在一起了。对于这点,评论却鲜有提及。

  1. 上一篇提到model和controller合并的原因是理解上的混乱。这里再讨论一种可能的原因。
  1. 有些项目,觉得model的作用只是保存服务器的数据和处理少量的数据逻辑。大部分逻辑应该全放到controller,这样久而久之,就造成了controller非常笨重,而model的代码则相对来说少的多。这种情况是重controller,轻model
  2. 有些项目,觉得controller的责任只是对逻辑的调用和转发,具体的逻辑代码应该写在model里面。这种情况是重model,轻controller
  3. 这两种情况,无论哪一种,都很有可能最终会演变成model和controller合并的结果。
  1. 其中有网友评论提到,controller应该是对外的,model是内聚的。对这个观点,笔者是认同的。
  • 对外,笔者的理解是controller可以依赖任何数据、逻辑和实例;内聚,笔者的理解是model只依赖于本模块内的数据、逻辑和实例。如果model也依赖公共模块数据和逻辑(例如依赖网络层),我觉得model也算是内聚的。
  • 该网友又另外评论到,凡是逻辑与外部相关的,都应该写在controller里面。
  1. 关于某些分歧。
  • 上一篇笔者提到,在某些项目,model也可以向服务器发送协议和监听协议,这样做的原因是,那些项目的model只保存服务器的数据和处理简单的逻辑,代码相对较少。model在这里也只是依赖于公共的网络模块和事件模块,也是内聚的。model在接收服务器数据的时候,只保存自己的数据和处理本模块的数据逻辑,然后通过发送事件的方式让其他模块处理自己的逻辑。网络模块与model的数据息息相关,把协议发送和接收放到model也未尝不可。
  • 对此,有网友也提出,协议的响应或协议回调的处理,应该放在controller里面,我猜想,该网友的项目并不是通过发送事件的方式,而是在controller对应的回调方法里面直接调用相关模块的model方法进行处理的,只有在必要时才发送事件。不知我的猜想是否正确?
  • 这两种做法的目的都是为了model的内聚,一种是通过在model里面发送事件的方式,让其他模块通过监听协议自己处理,另一种则是把其他相关模块的逻辑调用放到了controller里面。
  1. 逻辑代码可以分为数据处理逻辑和业务逻辑,这也鲜有人进行过区分。