遊戲服務器的思考之三:談談MVC

遊戲服務器也是基於MVC架構的嗎?是的,全部的應用系統都是基於MVC架構的,這是應用系統的天性。無論是客戶端仍是後臺,都包含模型、流程、界面這3個基本要素;不一樣類型的應用,3要素的「重量」可能各有誤差。目前那些聲稱比MVC更好的架構,在我看來,不過是MVC的在某種場合下的細化。可是,MVC這個概念是比較抽象的,項目中每一個人都有本身的理解,在細節之處你們的實現每每截然不同。像Spring這樣的基於MVC的具體框架工具,可以緩解一些混亂局面,可是做爲一個很是通用、有彈性的框架,它容許你作任何違反MVC的設計。要獲得一份結構清晰、可擴展、質量穩定的項目代碼,必須遵循良好的MVC設計理念,這個「理念」既來自軟件開發行業的現有知識,也來自項目團隊的共識。
 
首先來看看MVC的經典定義:
Model(模型)是應用程序中用於處理應用程序數據邏輯的部分。
  一般模型對象負責在數據庫中存取數據。
View(視圖)是應用程序中處理數據顯示的部分。
  一般視圖是依據模型數據建立的。
Controller(控制器)是應用程序中處理用戶交互的部分。
一般控制器負責從視圖讀取數據,控制用戶輸入,並向模型發送數據。
 
這是來自百度百科的定義,我沒有找到更權威的定義。以目前軟件系統的複雜度,這個定義顯得太簡陋了,幾乎無法指導實際的工做。
 
這篇文章,我想談談對MVC的理解,爲了簡單起見,拿」救濟金「這個遊戲裏面的極小的模塊作示例。這是一個極爲簡單的例子,可是足以說明個人設計理念。只要設計理念是完備且清晰的,那麼其餘更復雜的模塊徹底能夠套用相似的思路。
 
咱們遊戲裏面」救濟金「模塊的業務邏輯是這樣的:用戶在破產時(擁有金幣數小於某個值),能夠領取必定的的救濟金幣,天天最多能夠領取N次,N取決於用戶的VIP等級。
 
一、「模型」是什麼
上面的定義說模型是「應用程序數據邏輯的部分」,該怎麼理解?首先沒有任何疑問的是,軟件的核心數據結構是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,推薦看看《領域驅動設計.軟件核心複雜性應對之道》。
 
二、控制層
控制層解析來自客戶端的輸入,調用一個或多個模塊的模型層完成業務功能,而後將結果輸出給客戶端。
控制層提供的接口基本對應客戶端須要調用的接口,在救濟金這個業務裏,咱們須要兩個接口:一個查詢救濟金額度和剩餘的領取次數;一個領取接口;因而對應的類設計可能以下:
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」反而更好。
 
三、這裏有視圖嗎
和其餘APP同樣,遊戲的視圖是經過遊戲客戶端來呈現,後臺僅僅提供數據給客戶端而已,看起來這裏沒有視圖這個元素。MVC軟件架構最初來源於單機軟件,這個類型的軟件裏面,數據處理、控制邏輯、視圖輸出所有在一個進程裏面。如今流行的互聯網應用,前端重視圖,後臺重數據,彷佛都不足以構成完整的MVC結構。假設咱們把視圖換個說法叫作「輸出」,那麼就造成了兩個MVC結構。後端的View是輸出的接口數據,這些數據在前端反序列化之後,又承擔了M的角色。
 
所以遊戲服務器的視圖層比較簡單,本質上就是通信協議的定義,以及消息接受和發送的邏輯。比起web系統,遊戲系統在通信協議的設計上追求更加簡潔和高效,後臺每每直接未來自模型層的數據結構直接傳遞給客戶端,客戶端和服務端在數據模型上保持了很是高的一致性。爲何能夠這麼作?這是由遊戲系統的封閉性決定了的(參考第一篇)。
 
四、業務邏輯在哪裏?
有人說:在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獲取用戶註冊天數的方法調用。不得不感嘆:無論提及來有多簡單,改需求歷來不是一件簡單的事,這就是技術和產品經常撕逼的緣由了。 在上面這個設計原則之下,新的需求該怎麼知足至少可以一目瞭然。修改完了以後,每一個部分仍然各司其職,保持了不錯的內聚性。無論咱們的設計算不算行業先進水平,在快速的迭代過程當中,保持可持續和一致性纔是最重要的。
 
五、膨脹的控制層
若是有一個設計良好的模型層,那麼在產品的迭代過程當中最有可能出問題的就是控制層。在移動應用的開發裏面有經典的Massive Controller問題,由此誕生了不少MVC的衍生架構,好比MVP,MVVP等。後臺代碼也同樣,控制層既要處理來自前端的輸入,又要粘合模型層的接口來實現功能;而後諸如」流程分支「、」特殊處理「這些放哪都不合適的代碼,最終也落在了這一層。
 
首先控制層的代碼膨脹每每有其餘的緣由:
1)重複代碼太多,好比對輸入輸出處理沒有良好的封裝
2)內聚性差,在Spring這種框架的幫助下,要添一個接口處理方法是實在太簡單了,致使開發者決策的隨意性
3)  在需求更改的時候,沒有辨別出歸屬於模型層的變化,直接在控制層完成全部的事。
 
第三點是比較容易重複犯的一個錯誤,即便是經驗豐富的程序員。譬如上面第一部分救濟金系統的的例子,有很多人會直接在controller層加一個分支邏輯就完事了。若是要快速上線,這是多是最簡單的辦法,但久而久之,代碼變得混亂不堪。
 
若是有設計良好的模型層,再加上一點點開發規範,後臺控制層的通常代碼不會膨脹很厲害(這一點和移動應用不同,後者很大程度是受系統的UI framework拖累)。有些開發者爲了拆分控制層,容易作出兩個錯誤的決策:1)增長一個Service類來輔助Controller,這個Service不三不四,不知道屬於Controller層,仍是模型層;2)部分邏輯入侵到模型層,破壞了模型層的內聚性。
 
總結:
應用軟件應當使用MVC的架構模式,這是毋庸置疑的(至少目前沒有更好的選擇)。MVC三層之中,首先要設計模型層,也就是對業務創建模型,模型層的核心使命是表達業務規則(經過數據結構和服務接口);控制層是一組實現對應需求用例的方法集合,它接受輸入,調用模型層完成功能,並返回結果;視圖對App或遊戲後臺,可對應爲輸入輸出處理層。
 
MVC這種架構模式並無具體的規範,在細節之處要靠本身去把握。本文對MVC的理解能夠算做一種mvc的架構風格,它能指導開發者如何把功能模塊劃分到各個層次,以及在產品迭代過程當中維護好各個層次(尤爲是模型層)的內聚性。這種架構風格是否正確,是否足夠優秀,並非特別重要的事,」有風格「自己纔是最重要的。
相關文章
相關標籤/搜索