從〇開始構架前端(NLDV框架)
框架 設計模式
摘要:一個普通應用,大到微信, 小到豆瓣FM,必不可少的都包括四部分:Network、Logic、Data、View(NLDV)。如何把他們組合起來,結構清晰、又協作便利,是前端主程的基本修養。本文用通(有)俗(點)易(啰)懂(嗦)的語言,界定了這四個模塊的職能范圍,同時提供了一種簡單易用的組織方式。知者可互動,不知者可參考。
- 博客: http://www.cnblogs.com/jhzhu
- 郵箱: jhzhuustc@gmail.com
- 作者: 知明所以
- 時間: 2014-08-21
目錄
先嘮點嗑
說說自己吧:畢業三年,經歷4個項目,前兩個主做功能開發,后兩個全面負責。因為大學對UI交互深感興趣,夢想成為優秀的交互設計師,所以畢業就做了前端,想着至少也離得近點。不料一入代碼深似海,以前看過的十幾本交互書也隨風而去,腦子里剩下的只有:幾行干巴巴的代碼、幾個平台的API、和一點不成熟的總結。借此機會,整理一下。
我把他名為NLDV,也就圖一叫着方便(首字母的組合),並不是想標新立異。你或許會想到MVC,其實本質上跟MVC沒啥區別。只是與時俱進,現在App幾乎都是連着網的,也就把Network提出來了。
從第三個項目開始,這個NLDV的結構在我意識里漸漸清晰。第四個項目(游戲),因為是從零開始,我把NLDV的結構應用到了項目中。經歷了渲染引擎更換和網絡引擎更換,過程中其它模塊兒做得改動極少,切身體會到它的妙處,忍不住前來分享。
從賬號登陸談起
我們從一個最簡單的登陸請求談起。按照NLDV框架的思想,步驟如下:
- Logic告訴Network:以后你收到來自遠方服務器的消息都跟我打聲招呼,不然我告訴程序員整死你丫。(Logic向Network注冊網絡監聽函數。)
- 用戶點擊登陸按鈕,此時View調用Logic的登錄方法。Logic趕緊寫封信,告訴Network把這封信送到服務器。(Logic根據傳入的參數,構造相應的消息體,Network負責把消息發出去)
- 漫長的等待。。。
- Network終於等到服務器發來的消息,就急急忙忙遞給Logic。Logic打開信封一看:“尼瑪,居然能一次成功了!32個贊!”
- 可是信上還有性別,年齡,頭像,郵箱等等等信息,頭都大了,不記下來恐怕是隔夜就忘啊。於是,找來Logic的御用秘書Data(只有Logic能寫入),這些信息就交給你了,臨時存儲還是永久存儲我不管,反正我和View來取的時候,你要能給我。
- 好消息要和大家分享,於是Logic全應用廣播:“我們已經出色的完成了登陸任務,大家再接再厲”
- View收到此廣播消息,用界面告訴用戶“親愛的,你登陸成功了”。到此,完成一次登陸。
(PS:對於大多數界面實例,Logic剛剛發的這條廣播是可以當耳邊風的,因為跟自己業務無關。但,對於登陸界面來說,他必須時刻豎起耳朵監聽這個消息,要不然就是失職。所以一般情況下登陸View會在發送登錄請求之前就向系統注冊監聽這個消息的函數,以確保萬無一失。這是后話,沒看懂也沒關系)
職責分配
首先,看一幅圖:
通過上面的例子和圖示,可以來總結一下NLDV四大家族各自的職能范圍了:
Network
Network的是數據交流的基礎。在這里並不單指socket,並且不暴露任何底層網絡的實現。而是一個更加完整、穩定,有糾錯功能的職能單位。主要功能:
- 響應Logic的調用,將構造好的消息體轉換成服務器能識別的字節串,發送出去。
- 接收服務器發過來的字節串,轉換成前端可識別的結構體,通知Logic。
- 前后端在定義消息的時候,一般會用一個消息號(/或
主消息號-子消息號對)來唯一標識一種消息。 - 而Logic也不止一個,賬號系統,業務系統等,每個系統有對應的Logic,分別處理相關的業務邏輯。
- 一個Logic僅僅會對某一些消息感興趣,所以,它只向Network注冊自己感興趣的消息號。
- 前后端在定義消息的時候,一般會用一個消息號(/或
- 容錯處理。比如有限次的自動重發,需要的時候自動重連,網絡徹底不可用時通知Logic等。
我們用最少的接口來定義它:
// Real msg struct will implement this interface, adding some getter/setters.
interface IMessage
{
uint getMsgId();// unique id for a type of msg
byte[] toBytes();// serialize to bytes.
void parseBytes( byte[] bytes); //deserialize to useful info.
}
//Generally, "Logic" will implement this interface for recieving data
interface INetworkHandler
{
void onMessageRecv( IMessage msg);
}
interface INetwork
{
void send( IMessage msg );
void registHandler( uint msgId, INetworkHandler handler );
void unregistHandler( uint msgId, INetworkHandler handler );
}
Logic
Logic是一個應用中核心實現業務邏輯的部分。主要功能有:
- 響應來自用戶(View)的功能請求。或通過寫入Data來改變狀態,或構造消息體發送到服務器。
- 收到網絡消息后,做相應的邏輯處理,將數據的改變寫入Data,最后廣播本地事件。
- 這里的本地事件通常用一個 字符串或者整數指代,稱為本地事件ID。
- 這個過程通常會用到觀察者模式。有一個本地消息中心
LocalMessageCenter,監聽者(通常是View)用本地事件ID來向LocalMessageCenter注冊,而Logic調用LocalMessageCenter.dispatch( localEventId )即可廣播此本地事件。 - 是的,Logic直接調用View的方法也能達到同樣的目的。但是,為了減小Logic和View之間的耦合性,還是選用
LocalMessageCenter作為中間層。隨着需求的改變,View類可能面目全非,但LocalMessageCenter的接口卻可以長久不變。
Logic的大致結構如下:
class SomeLogic implemets INetworkHandler
{
SomeLogic()
{
Network.getInstance().registHandler(1, this);
Network.getInstance().registHandler(2, this);
}
void onMessageRecv( IMessage msg)
{
switch( msg.getMsgId() )
{
case 1:
logicHandler1(IMessage msg);
break;
case 2:
logicHandler2(IMessage msg);
break;
...
}
}
//請求操作
void logicReq1(...);
void logicReq2(...);
...
//消息處理
void logicHandler1(IMessage msg)
{
//TODO: 收到消息,邏輯處理
//TODO: 向Data寫入數據
//TODO: 廣播本地事件
LocalMessageCenter.getInstance().dispatch( "Logic1Compelete" );
};
void logicHandler2(IMessage msg);
...
}
ILocalMessageHandler
{
void onLocalMessage( String msg );
}
LocalMessageCenter
{
static LocalMessageCenter getInstance(); // Singleton
void dispatch( String msg );
void regist( String msg, ILocalMessageHandler handler );
void unregist( String msg, ILocalMessageHandler handler );
}
Data
這個模塊是全應用的數據中心。提供兩個功能:
- 存儲。前面已經提到,基本只有Logic對Data有寫入權限。由於沒有想到好的辦法,這個規范暫時只能通過編碼習慣來約定,沒有做框架級的約定,如果大家有好的辦法,歡迎補充。Logic 不關心數據的存儲方式:同步OR異步,臨時OR永久;這些都由Data自己決定。
- 讀取。Logic和View都會使用到Data中的數據。但是需要注意異步讀取的問題。一般App要求View對操作的響應速度要在0.1s級別。所以,若涉及大量的數據存儲或讀取,便需要借助異步處理。對於存儲,我們可能不是那么關心異步存儲什么時候結束,只需要知道它成功了既可。但對於讀取,經常遇到的情況是:頁面上加個菊花,數據完全讀取成功之后,移除菊花。這就需要一個通知機制。此時,我們也會用
LocalMessageCenter作為通信的橋梁。
View
NLDV框架對View的限制,相比以上3個模塊,非常少。因為,對於NLDV框架來說,View跟特定的平台無關,即它是對各種不同平台顯示框架的抽象。在IOS里它是UIFramework,在Android里它是xxx,在游戲里它是openGL,甚至,在下面會講到的機器人模擬器里它是一堆測試代碼。
在這個框架里,View只在兩個過程中出現:
- 調用Logic的方法,發送邏輯請求。
- 監聽本地事件,讀取Data,顯示內容。
只補充一點:在View顯示過程中,需要用到很多二級數據(二級數據,就是跟原始數據相對,對原始數據進行整合或者篩選后得到的數據),這些二級數據的處理過程最好在View中處理。因為這些代碼大多跟特定的界面有關,而跟App的主要邏輯關系不大,為了以后更改方便,最好寫在View里。
WHY NLDV?
看到這里,讀者大概能隱約的感受到NLDV的一些優點,但是又不那么清晰,要不要看下去呢?下個里程碑就到了看這個人扯淡有用么?再看下去兩盤Dota的時間可就沒了丫。。。
別急,舉幾個栗子提提神。
引擎更換
最早,因為兼容PC版本的斗地主,我們采用了原始的字節對齊的方式進行網絡傳輸。又因為設計失誤,網絡層字節的pack和unpack以及異步讀取的處理,都各種出問題。(因為需要跨平台,所以我們用了C++語言,采用了最基礎的BSD socket進行socket連接。)
結果是,游戲在網絡不穩定的情況下各種閃退。這下產品經理不高興了。又因為當初開發網絡層的同學接手了其他事情,只剩下我各種修修補補,最終也沒能徹底解決問題。
於是,我決定重寫網絡層。(媽蛋,早看這段代碼不爽了)。於是我花了兩天封裝了一個帶自動重連和容錯功能,支持異步接受和發送的GameSocket,簡單測試可以發送和接受字節。5分鍾替換游戲中的舊網絡層,你猜,怎么着?一次Run就登陸成功了!點了幾下,所有功能完好如初!尼瑪,世界上有比這還幸福的事情么?
其實,別聽我說的挺牛逼的,其實替換過程就改了不到10行代碼。因為實在是跟Logic、Data、View沒啥耦合的地方。
制作機器人
一般在線游戲,都會有一兩個用戶沒問題,大量用戶就有問題的時候。所以,機器人測試總是必要的。
當我把前端代碼交給后端,簡單介紹了一下結構之后,后端的小伙伴兒們都驚呆了。不是因為他看到這框架有多么優秀,而是:“這樣,我只要寫一個while循環,500行代碼就能完成一個機器人了啊。我還申請了一個星期來做這個事情呢!”(這是他的原話)。
說的更具體點,制作一個機器人就這么幾步:
- 把所有的View代碼文件刪掉。
- 寫一個
AndroidLoop類。在這個類里,監聽斗地主主流程里必須處理的LocalMessage,在適當的時候發送主流程中的請求。(對於斗地主來說,主流程包括登陸、選房間、搶地主、出牌、退房間。每個應用有所不同,靈活自便。) - NLD(NLDV去掉V)部分都不變,
幾乎不用改一行代碼。
邏輯清晰
框架的作用,理論層面上規范了整個軟件的結構;而在實現層面,通俗一點,它就規范了什么代碼該寫在哪里,不要隨地亂放。
作為一個針對性很強的框架,在上述過程中,NLDV約束了很多可能不需要約束的規范。其中大部分是項目中的干貨經驗。我知道他並不總是好的,我考略了很久要不要把他們加進來。最終,我還是寫下來了,考慮到剛開始從零開始寫應用的讀者來說,這些可能避免他們走很多彎路;而對有經驗的讀者來說,可能他們有判斷的能力,可以取舍自如。
我想,如果嚴格按照NLDV框架來編寫程序,顯而易見的好處就是:
- 層次清晰,出現問題容易定位。
- 主程再也不用擔心同事們把代碼寫得到處都是了。。。
框架之外(下回分解)
NLDV的適用場景(下回分解)
因為各種原因,這篇博客斷斷續續寫了兩個星期了,再不發布就要胎死腹中了。所以,最后兩節放在這篇日志的續集中寫。如果您感興趣,請私信我,我會盡快補上。
