游戏服务器也是基于MVC架构的吗?是的,所有的应用系统都是基于MVC架构的,这是应用系统的天性。不管是客户端还是后台,都包含模型、流程、界面这3个基本要素;不同类型的应用,3要素的“重量”可能各有偏差。目前那些声称比MVC更好的架构,在我看来,不过是MVC的在某种场合下的细化。但是,MVC这个概念是比较抽象的,项目中每个人都有自己的理解,在细节之处大家的实现往往大相径庭。像Spring这样的基于MVC的具体框架工具,能够缓解一些混乱局面,但是作为一个非常通用、有弹性的框架,它允许你做任何违反MVC的设计。要得到一份结构清晰、可扩展、质量稳定的项目代码,必须遵循良好的MVC设计理念,这个“理念”既来自软件开发行业的现有知识,也来自项目团队的共识。
 
首先来看看MVC的经典定义:
Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。
  通常模型对象负责在数据库中存取数据。
View(视图)是应用程序中处理数据显示的部分。
  通常视图是依据模型数据创建的。
Controller(控制器)是应用程序中处理用户交互的部分。
通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。
 
这是来自百度百科的定义,我没有找到更权威的定义。以目前软件系统的复杂度,这个定义显得太简陋了,几乎没法指导实际的工作。
 
这篇文章,我想谈谈对MVC的理解,为了简单起见,拿”救济金“这个游戏里面的极小的模块做示例。这是一个极为简单的例子,但是足以说明我的设计理念。只要设计理念是完备且清晰的,那么其他更复杂的模块完全可以套用类似的思路。
 
我们游戏里面”救济金“模块的业务逻辑是这样的:用户在破产时(拥有金币数小于某个值),可以领取一定的的救济金币,每天最多可以领取N次,N取决于用户的VIP等级。
 
1、“模型”是什么
上面的定义说模型是“应用程序数据逻辑的部分”,该怎么理解?首先没有任何疑问的是,软件的核心数据结构是Model的一部分,这个小功能里,有几个数据需要被建模:用户领取救济的最小金币限制、救济金额度、用户可以领取的次数、用户目前领取的次数。
 
1)救济金额度和用户金币限制
这两个数值是一个与具体用户无关的业务配置值,可以实现为常量,也可以写在配置文件里面。参考第二篇的设计思想,我们应该建立一个json格式的救济金配置文件,内容可能是:
{
userCoinLimit:10000;//用户金币要低于1万
reliefCoin:5000; //救济金币5000
}
载入到一个叫做ReliefConfig的数据结构里面:
public class ReliefConfig {
     long userCoinLimit;
     long reliefCoin;
}
 
2) 用户可以领取的次数
这个数值与用户的VIP等级有关,而用户的VIP等级的控制是另一个模块的事。在这里,模型所要表达的是一个约束关系。这个”约束关系”需要用一条数据记录来表达吗?还是写死在代码里面就行了。这取决于业务的复杂程度和对灵活性的期望,假如我们期望在线调整这个约束关系,那么“写死在代码里面“就不行。
 
我们的游戏里,这个规则相对固定,可以用写死在代码里:
int getUserReliefTimeLimit(int vipLevel)
{
     return reliefConfig.getTime()+vipLevel; //领取次数是一个固定值加上vip等级
}
领取次数的固定部分放在上面的配置类里,增加一个字段如下:
public class ReliefConfig {
     long userCoinLimit;
     long reliefCoin;
     int reliefTime;
}
 
3)用户今天领取的次数
这个数据是用户相关的数据,不断地发生变化,需要使用数据库来记录,而且和日期相关,可以考虑设计成下面这个数据结构:
public class UserReliefTime {
    String date;
    int reliefTime;
}
 
好了,我们已经为这个小系统建立了数据模型:两个类加一个函数,顺便制定了数据的存储方案:配置文件+数据库表。在一个业务系统实现之前,哪怕逻辑再简单,也要对业务建立模型,以”郑重而清晰”地表达业务规则。
 
模型是否只包含这些?当然不止,这只是静态的数据模型,按照上面的定义:“应用程序数据逻辑的部分”,模型至少还要对外提供数据操作的接口。
在提供什么样的接口之前,我们要做一个决策:“用户领取救济“的逻辑是否属于模型的一部分,换句话说,在模型层是否要提供一个”领取救济金“的接口。
 
要决策这个问题,要考虑一个背景:修改用户的金币在游戏里面是一个特别重要的行为,有一个单独的模块(暂且叫做用户属性管理模块)来负责,如果救济金模块的模型部分要实现这个逻辑,那么就会依赖于用户属性管理模块。就整个救济金模块来说,对户属性管理模块形成依赖是必然的,但在模型层形成这种依赖,我觉得不恰当,因为破坏了模型层的内聚性。
 
最终救济金模型的操作接口被设计成这样,实现部分就略过了:
public interface ReliefService
{
int getReliefTime(String userId); //获取用户今天的领取次数
int addReliefTime(String userId);//增加用户今天领取次数
int getReliefTimeLeft(String userId,int vipLevel); //获取用户今天剩余的领取次数
long gerReliefCoin(); //获取救济金额度
}
 
4)模型层的代码清单
一个配置读取类ReliefConfig,一个数据库ORM对象类UserReliefTime,一个Service类ReliefService。
我认为Service类是属于模型层的,这块可能有一些争议,原因在于Service从名字上含义就模糊,
 
模型层用来做什么?简单来说,就是表达规则以及业务相关核心数据的”增删改查“,这里的”增删改查“是高于底层数据存储层的,是饱含业务语义的,在修改数据的过程中维护着数据的业务一致性。对于救济金这个模块来说,核心数据是:用户领取救济的最小金币限制、救济金额度、用户可以领取的次数、用户目前领取的次数;模型层的使命是:
1)屏蔽这些数据的底层存储细节;
我们有两种存储方式:文件和数据库,模型层封装了这些细节;
2)维护这些数据之间的一致性;
所谓一致性,就是数据在变化过程中符合业务规则约束,用户领取了一次救济金,那么剩余的次数必然减少,除非这个过程中vip等级提升;
3)提供这些数据增删改查接口。
模型层提供的接口一般不会是getter&setter这样的简单接口,而是饱含业务语义的接口。一个新来的团队成员,一看这一组接口,基本就能明白这个模块的基础功能。
 
现在再考虑”给用户发放救济金”这个动作,它并不是救济金这个模块的模型层的使命,后者只关注用户领取的次数,并不关注金币是怎么发放到用户手上的。划清这个界限是很有必要的,随着业务变得越来越复杂,救济金模块将来还可以依赖其他模块,有时候开发者会有一种冲动,在模型层直接访问其他子模块的接口,以减少模型接口的参数,让模型层看起来功能更强大。但是实际上,这样做是在破坏模型的内聚性,让它变得不稳定。
 
模型层特别强调内聚性,尽量不要对其他模块形成较强的依赖(我的习惯是,可以引用来自其他模块的数据结构,但避免使用其他模块的Service),模型层的功能要恰到好处,既不能退化成数据层(只包含数据库访问的逻辑以及相关的PO对象),也不能混入非核心的逻辑;我比较推崇DDD(模型驱动设计)的模型设计方法,如果不知道DDD,推荐看看《领域驱动设计.软件核心复杂性应对之道》。
 
2、控制层
控制层解析来自客户端的输入,调用一个或多个模块的模型层完成业务功能,然后将结果输出给客户端。
控制层提供的接口基本对应客户端需要调用的接口,在救济金这个业务里,我们需要两个接口:一个查询救济金额度和剩余的领取次数;一个领取接口;于是对应的类设计可能如下:
public class ReliefController()
{
//查询
public output handleReliefCheck(input)
{
     String userId = input.userId
     long reliefCoin = reliefService.getReliefCoin(userId,);
     int vipLevel = userService.getVipLevel(userId);
     int  leftTime = reliefService.getReliefTimeLeft(userId,vipLevel);
     return {“reliefCoin":reliefCoin,”leftTime”:leftTime}
}
//领取
public output handleDrawRelief(input)
{
     String userId = input.userId
     long reliefCoin = reliefService.getReliefCoin(userId,);
     int vipLevel = userService.getVipLevel(userId);
     int  leftTime = reliefService.getReliefTimeLeft(userId,vipLevel);
     if (leftTime<=0) {
            return {“error”:...}
     }
     userService.addCoin(userId,reliefCoin);
     leftTime = reliefService.addReliefTime(userId);
     return {“reliefCoin":reliefCoin,”leftTime”:leftTime}
}
}
 
可以看到,一旦模型层确立,控制层该怎么编写变成一件很自然的事。在上面这个简单的控制器里面,我们做了以下几件事:
1)解析来自客户端数据
2)构建返回给客户端数据
3)调用救济金模块的模型层接口以及用户属性管理模块的模型层接口完成了救济金领取的业务功能;
上面这几件事是控制层的职责,千万不要浸染到模型层。
 
一个模块的控制层往往就是一个类,一个public方法对应一个客户端接口。控制层充当了两个角色:
1)直接实现需求用例,所以它的public方法列表和相关需求用例呈现一对一的映射关系;
2)像粘合剂一样,调用相关模块的模型层接口以最终实现需求用例;
 
相比模型层,控制层的代码是比较廉价的,经常需要修改。说实话,我不建议在这一层去发挥面向对象设计技巧,保持“Easy To Delete”反而更好。
 
3、这里有视图吗
和其他APP一样,游戏的视图是通过游戏客户端来呈现,后台仅仅提供数据给客户端而已,看起来这里没有视图这个元素。MVC软件架构最初来源于单机软件,这个类型的软件里面,数据处理、控制逻辑、视图输出全部在一个进程里面。现在流行的互联网应用,前端重视图,后台重数据,似乎都不足以构成完整的MVC结构。假设我们把视图换个说法叫做“输出”,那么就形成了两个MVC结构。后端的View是输出的接口数据,这些数据在前端反序列化以后,又承担了M的角色。
 
因此游戏服务器的视图层比较简单,本质上就是通讯协议的定义,以及消息接受和发送的逻辑。比起web系统,游戏系统在通讯协议的设计上追求更加简洁和高效,后台往往直接将来自模型层的数据结构直接传递给客户端,客户端和服务端在数据模型上保持了非常高的一致性。为什么可以这么做?这是由游戏系统的封闭性决定了的(参考第一篇)。
 
4、业务逻辑在哪里?
有人说:在MVC架构下,业务逻辑在模型层,控制层只是映射输入到模型层而已。上面的设计明显已经和这个说法背道而驰:业务逻辑一部分在控制器,一部分在模型。说实话,我从未在实际项目中,见过符合前面说法的设计,反而这种说法导致了混乱,导致非核心逻辑入侵了业务模型。
 
“业务逻辑“是一种很模糊的说法,有一本书(忘了是哪本书了)说了一句很正确的话:在一个软件产品里面,”业务逻辑“是最没逻辑的部分。因为它受到太多因素的影响:平台、用户、设计潮流、以及产品经理的个人喜好。业务逻辑中仍然包含”很有逻辑“的一部分,这部分就是”业务模型“,成功抽取出这个部分加以精心实现是得到一个优秀设计的关键;“没逻辑”的部分我们就放到控制层。
 
以上面救济金的子系统为例,假设有一天策划提出这样一个需求:我们希望注册时间超过5天的用户得到更多的救济金。在上面这个设计里面, 相关修改可能是这样的:
public class ReliefConfig {
     long reliefCoin;
     long reliefCoinDay5; //增加一个配置数据
     int reliefTime;
}
 
public interface ReliefService
{
     long getReliefCoin(int days); //获取救济金额度方法要加一个参数:注册天数
}
 
public class ReliefController()
{
//领取
public output handleDrawRelief(input)
{
     String userId = input.userId
     int vipLevel = userService.getVipLevel(userId);
     int  leftTime = reliefService.getReliefTimeLeft(userId,vipLevel);
     if (leftTime<=0) {
            return {“error”:...}
     }
     int days = accountService.getRegisterDays(userId); //控制层要从账号服务里面拉取注册天数
     long reliefCoin = reliefService.getReliefCoin(days);
     userService.addCoin(userId,reliefCoin);
     leftTime = reliefService.addReliefTime(userId);
     return {“reliefCoin":reliefCoin,”leftTime”:leftTime}
}
}
 
上面有3处修改:模型层的配置文件加了一个字段,ReliefService的getReliefCoin接口加了一个参数,控制层的领取方法,增加了从accountService获取用户注册天数的方法调用。不得不感叹:不管说起来有多简单,改需求从来不是一件简单的事,这就是技术和产品常常撕逼的原因了。 在上面这个设计原则之下,新的需求该怎么满足至少能够一目了然。修改完了之后,每个部分仍然各司其职,保持了不错的内聚性。不管我们的设计算不算行业先进水平,在快速的迭代过程中,保持可持续和一致性才是最重要的。
 
5、膨胀的控制层
如果有一个设计良好的模型层,那么在产品的迭代过程中最有可能出问题的就是控制层。在移动应用的开发里面有经典的Massive Controller问题,由此诞生了很多MVC的衍生架构,比如MVP,MVVP等。后台代码也一样,控制层既要处理来自前端的输入,又要粘合模型层的接口来实现功能;然后诸如”流程分支“、”特殊处理“这些放哪都不合适的代码,最终也落在了这一层。
 
首先控制层的代码膨胀往往有其他的原因:
1)重复代码太多,比如对输入输出处理没有良好的封装
2)内聚性差,在Spring这种框架的帮助下,要添一个接口处理方法是实在太简单了,导致开发者决策的随意性
3)  在需求更改的时候,没有辨别出归属于模型层的变化,直接在控制层完成所有的事。
 
第三点是比较容易重复犯的一个错误,即使是经验丰富的程序员。譬如上面第一部分救济金系统的的例子,有不少人会直接在controller层加一个分支逻辑就完事了。如果要快速上线,这是可能是最简单的办法,但长此以往,代码变得混乱不堪。
 
如果有设计良好的模型层,再加上一点点开发规范,后台控制层的一般代码不会膨胀很厉害(这一点和移动应用不一样,后者很大程度是受系统的UI framework拖累)。有些开发者为了拆分控制层,容易做出两个错误的决策:1)增加一个Service类来辅助Controller,这个Service不伦不类,不知道属于Controller层,还是模型层;2)部分逻辑入侵到模型层,破坏了模型层的内聚性。
 
总结:
应用软件应当使用MVC的架构模式,这是毋庸置疑的(至少目前没有更好的选择)。MVC三层之中,首先要设计模型层,也就是对业务建立模型,模型层的核心使命是表达业务规则(通过数据结构和服务接口);控制层是一组实现对应需求用例的方法集合,它接受输入,调用模型层完成功能,并返回结果;视图对App或游戏后台,可对应为输入输出处理层。
 
MVC这种架构模式并没有具体的规范,在细节之处要靠自己去把握。本文对MVC的理解可以算作一种mvc的架构风格,它能指导开发者如何把功能模块划分到各个层次,以及在产品迭代过程中维护好各个层次(尤其是模型层)的内聚性。这种架构风格是否正确,是否足够优秀,并不是特别重要的事,”有风格“本身才是最重要的。
 
 
 

游戏服务器的思考之三:谈谈MVC的更多相关文章

  1. 谈谈MVC项目中的缓存功能设计的相关问题

    本文收集一些关于项目中为什么需要使用缓存功能,以及怎么使用等,在实际开发中对缓存的设计的考虑 为什么需要讨论缓存呢? 缓存是一个中大型系统所必须考虑的问题.为了避免每次请求都去访问后台的资源(例如数据 ...

  2. 游戏服务器设计之NPC系统

    游戏服务器设计之NPC系统 简介 NPC系统是游戏中非常重要的系统,设计的好坏很大程度上影响游戏的体验.NPC在游戏中有如下作用: 引导玩家体验游戏内容,一般游戏内有很多主线.支线任务,而任务的介绍. ...

  3. FPS游戏服务器设计的问题 【转】

    一.追溯 去gameloft笔试,有一个题目是说: 叫你去设计一个FPS(第一人称射击游戏),你是要用TCP呢还是要用UDP,说明理由 . 二.学习 这是两篇网上找到的文章,写非常不错. 当时笔试的时 ...

  4. 同一世界服务器架构--Erlang游戏服务器

        Erlang最大的优点是方便,很多基础功能都已经集成到Erlang语言中.之前用C++写服务器的时候,管理TCP连接很繁琐,需要写一大堆代码来实现.底层的框架需要写很多代码实现,这样既浪费时间 ...

  5. (转)FPS游戏服务器设计的问题

    FPS游戏服务器设计的问题出处:http://www.byteedu.com/thread-20-1-1.html一.追溯 去gameloft笔试,有一个题目是说: 叫你去设计一个FPS(第一人称射击 ...

  6. 游戏服务器菜鸟之C#初探一游戏服务

    本人80后程序猿一枚,原来搞过C++/Java/C#,因为工作原因最后选择一直从事C#开发,因为读书时候对游戏一直比较感兴趣,机缘巧合公司做一个手游的项目,我就开始游戏服务器的折腾之旅. 游戏的构架是 ...

  7. 游戏服务器菜鸟之C#初探四游戏服务

    经过多次折腾之后,在一次进行了一次重大的重构,去解决问题 主要重构如下 1.将原来的单一协议修改多协议进行,一些查询.认证的功能都采用HTTP进行,避免全部采用TCP链接资源的消耗: 2.原来单一的部 ...

  8. Redis在游戏服务器中的应用

    排行榜游戏服务器中涉及到很多排行信息,比如玩家等级排名.金钱排名.战斗力排名等.一般情况下仅需要取排名的前N名就可以了,这时可以利用数据库的排序功能,或者自己维护一个元素数量有限的top集合.但是有时 ...

  9. [.net 面向对象程序设计深入](4)MVC 6 —— 谈谈MVC的版本变迁及新版本6.0发展方向

    [.net 面向对象程序设计深入](4)MVC 6 ——谈谈MVC的版本变迁及新版本6.0发展方向 1.关于MVC 在本篇中不再详细介绍MVC的基础概念,这些东西百度要比我写的全面多了,MVC从1.0 ...

随机推荐

  1. 170330、Spring中你不知道的注入方式

    前言 在Spring配置文件中使用XML文件进行配置,实际上是让Spring执行了相应的代码,例如: 使用<bean>元素,实际上是让Spring执行无参或有参构造器 使用<prop ...

  2. 160415、sql语句sort排序,sort为空的在后面

    按sort排序,sort为空的在后面 select * from 表名 order by (case when sort is null or sort='' then 1 else 0 end),s ...

  3. tomcat------->简单配置

    主机名:www.snowing.com 域名:snowing.com http://主机+服务器端口号/path(web应用)/xxx.html 例: http://localhost:8080/it ...

  4. TCP requires two packet transfers to set up the connection before it can send data

    wHTTP重用现存连接来减少TCP建立时延. HTTP The Definitive Guide 4.2.3 TCP Connection Handshake Delays When you set ...

  5. 第六周小组作业 软件测试与评估:百词斩VS扇贝单词

    被测产品说明: A:百词斩 B:扇贝单词 一.基本任务 1.测试进度表 | 项目 | 内容说明 | 预估耗时(分钟) | 实际耗时 (分钟) | | -------------- | -------- ...

  6. Java 之NIO

    1. NIO 简介 Java NIO(New IO)是从1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API; NIO 与原来的IO有同样的作用和目的,但是使用的方式完全不同 ...

  7. squee_spoon and his Cube VI---郑大校赛(求最长子串)

    市面上最常见的魔方,是三阶魔方,英文名为Rubik's Cube,以魔方的发明者鲁比克教授的名字命名.另外,二阶魔方叫Pocket Cube,它只有2*2*2个角块,通常也就比较小:四阶魔方叫Reve ...

  8. npm命令,查看当前npm版本,更新nmp到最新版本,安装sails

    打开Node.js command prompt 1 查看npm当前版本 npm -v 2 更新npm至最新版本 npm install npm@latest -g 3 安装sails  npm in ...

  9. PHP面试专用笔记精简版

    [PHP笔记] 1.require 遇到即包含文件,require_once 只包含一次.require 遇到错误会终止,一般放在程序的最前面:include遇到错误会继续执行,一般放在流程控制语句中 ...

  10. 如何使用别人的代码 (特指在MFC里面 或者推广为C++里面)

    别人写了一堆代码,给了你源代码.在C++里面 应该是  头文件(.h)和源文件(.cpp).  那么我们如何使用他们呢?? 第一步:将其包含进来 如下图  ,不论是头文件还是源文件都如此 第二步:告诉 ...