簡(jiǎn)單App的開(kāi)發(fā)通常不需要考慮數(shù)據(jù)層的問(wèn)題,或者說(shuō)考慮得可以比較粗糙。比如一個(gè)展示產(chǎn)品信息的頁(yè)面,通常我們認(rèn)為只需要在網(wǎng)絡(luò)正常的情況使用,可以認(rèn)為是一個(gè)非常接近于B-S模式的C-S模式。
而隨著App功能的逐漸迭代進(jìn)化,以及產(chǎn)品方對(duì)于產(chǎn)品的思考逐漸深入,數(shù)據(jù)維護(hù)需求遲早會(huì)提上議事日程。與其一開(kāi)始草草地寫(xiě)一個(gè)App交差,還不如一開(kāi)始就考慮清楚如何設(shè)計(jì)好一個(gè)擴(kuò)展能力強(qiáng),又不至于功能過(guò)剩的架構(gòu)。
A. 網(wǎng)絡(luò)數(shù)據(jù)
網(wǎng)絡(luò)數(shù)據(jù)是App最重要的數(shù)據(jù)來(lái)源?,F(xiàn)在App開(kāi)發(fā)者似乎已經(jīng)可以不動(dòng)腦筋地一上來(lái)就拖進(jìn)AFNetworking之類的功能庫(kù),因?yàn)檫@類功能庫(kù)儼然成為業(yè)界的新標(biāo)準(zhǔn)。其實(shí)使用這種庫(kù)我是百分百得贊成,因?yàn)榍叭艘呀?jīng)造好了輪子,除非你真有更多閑余時(shí)間,或者你老板覺(jué)得研究功能模塊也會(huì)給你發(fā)工資,那你可以嘗試全部采用自寫(xiě)模塊。
首先思考使用AFNetworking給你帶來(lái)了什么益處。首先你不需要再關(guān)注HTTP的更多細(xì)節(jié),包括數(shù)據(jù)緩存,https認(rèn)證,摘要認(rèn)證,業(yè)務(wù)上你不用考慮請(qǐng)求的調(diào)度問(wèn)題,額外的你可能還能獲得數(shù)據(jù)序列化的便利性。這些可能是你當(dāng)下沒(méi)有想到,但未來(lái)App中一定會(huì)需要用到的功能。
然后我們來(lái)思考我們需要注意什么。
- 你的App和服務(wù)器的通訊數(shù)據(jù)。
典型的用JSON。如果你的App用了XML等目前非主流的傳輸協(xié)議,而且短期內(nèi)沒(méi)有精力改,那也沒(méi)關(guān)系。但是勸你一定要遵守RESTful的API的設(shè)計(jì)規(guī)范。
很多程序員以為數(shù)據(jù)傳輸這種事不就是從服務(wù)器拉一個(gè)東西來(lái),怎么傳都行,并沒(méi)什么太大區(qū)別。假如你的App只準(zhǔn)備維護(hù)一年,那是沒(méi)太大區(qū)別。這里舉個(gè)簡(jiǎn)單例子來(lái)說(shuō)明亂定義API的危害:
例子1: 我們App要獲得一個(gè)首頁(yè)數(shù)據(jù),最初程序員隨便取了個(gè)接口名叫
http://api.myapp.com/getData
,然后第二個(gè)頁(yè)面出現(xiàn)了,是個(gè)人信息頁(yè),后臺(tái)程序員說(shuō),那還取這個(gè)名字吧,傳遞的參數(shù)換下就行。更妙的是,參數(shù)還是用POST方式傳輸?shù)?/li>
http://api.myapp.com/getData
POST data: apiName=myProfile
如果產(chǎn)品哪一天說(shuō)你的接口數(shù)據(jù)太大,要做接口緩存,而好死不死的你的參數(shù)在POST里面,那很遺憾,HTTP的緩存就不要想了。你需要自己實(shí)現(xiàn)一套類似HTTP的ETag或者M(jìn)odified-Since邏輯,還不能保證比AFNetworking寫(xiě)的好,這不是給自己找事嗎?
例子2: 很多開(kāi)發(fā)人員寫(xiě)JSON極其隨意,常見(jiàn)如:
{"data": {
"imgurl": "http://api.myapp.com/link",
"title": "My List",
}}
請(qǐng)問(wèn)這個(gè)data是什么,對(duì)于服務(wù)器和客戶端來(lái)說(shuō),對(duì)應(yīng)任何一個(gè)實(shí)際的數(shù)據(jù)結(jié)構(gòu)嗎?哪怕想把幾個(gè)不相干的數(shù)據(jù)結(jié)構(gòu)打包成一個(gè)新節(jié)點(diǎn),也請(qǐng)取一個(gè)確定的名字,data這種名字到最后就是客戶端根本不能預(yù)知到內(nèi)部的數(shù)據(jù)類型,也許還得加一個(gè)type來(lái)區(qū)別,那寫(xiě)個(gè)data只是為了給節(jié)點(diǎn)湊個(gè)名字嗎?
更常見(jiàn)的是一物多名,比如一個(gè)用戶信息,一會(huì)叫userProfile。一會(huì)叫userInfo。這種設(shè)計(jì)最后的結(jié)果必然導(dǎo)致開(kāi)發(fā)人員不看文檔根本不能了解接口的含義,而遺憾的是沒(méi)有文檔又是很多開(kāi)發(fā)組的日常狀態(tài)。
B 數(shù)據(jù)模型
客戶端網(wǎng)絡(luò)數(shù)據(jù)通常會(huì)被轉(zhuǎn)換成數(shù)據(jù)結(jié)構(gòu),供各業(yè)務(wù)層使用。設(shè)計(jì)數(shù)據(jù)模型很多人覺(jué)得是個(gè)簡(jiǎn)單體力活,其實(shí)也不盡然。我覺(jué)得最理想的數(shù)據(jù)模型,應(yīng)當(dāng)由客戶端和服務(wù)器開(kāi)發(fā)人員協(xié)商后確定,切勿由一端主導(dǎo)。
還是以用戶信息為例:產(chǎn)品初期我們可能設(shè)想的用戶信息也就沒(méi)幾樣:
@interface UserModel : NSObject
@property (nonatomic, strong) NSString *userId;
@property (nonatomic, strong) NSString *userName;
@property (nonatomic, strong) NSString *desc;
@property (nonatomic, strong) NSString *iconUrl;
@end
先還是想想userId的數(shù)據(jù)類型吧。不要服務(wù)器定了個(gè)長(zhǎng)整型結(jié)果客戶端是個(gè)字符,后患無(wú)窮!
隨著業(yè)務(wù)的復(fù)雜化,UserModel東西越來(lái)越多了!也許該包含子對(duì)象了。
@interface UserModel : NSObject
@property (nonatomic, strong) NSString *userId;
@property (nonatomic, strong) NSString *userName;
@property (nonatomic, strong) NSString *desc;
@property (nonatomic, strong) NSString *iconUrl;
@property (nonatomic, strong) UserLevel *userLevel;
@property (nonatomic, strong) UserProfile *userProfile;
@property (nonatomic, strong) Relation *userRelation;
@end
什么時(shí)候可以增加子對(duì)象了?其實(shí)并不難確定:
- 服務(wù)端數(shù)據(jù)表發(fā)現(xiàn)用一張表存儲(chǔ)用戶信息已經(jīng)不合適,需要分表時(shí);或者數(shù)據(jù)來(lái)源并不相同時(shí)
- 子對(duì)象有獨(dú)立使用的應(yīng)用場(chǎng)景時(shí)
第一點(diǎn)其實(shí)很好判斷。Relation這個(gè)結(jié)構(gòu)假如包含的是這個(gè)用戶和接口中“我”的關(guān)系信息,對(duì)客戶端來(lái)說(shuō)絕不可能是和其他這個(gè)用戶固有屬性是在一個(gè)表內(nèi),或者說(shuō)產(chǎn)生數(shù)據(jù)邏輯差異很大。這樣還是建議客戶端將這塊數(shù)據(jù)獨(dú)立命名。當(dāng)然,在網(wǎng)絡(luò)傳輸時(shí)也應(yīng)當(dāng)獨(dú)立開(kāi)來(lái)。
tips:給每一個(gè)節(jié)點(diǎn)增加一種“未獲得”狀態(tài)。
這個(gè)技巧的好處是,哪怕來(lái)自服務(wù)器的JSON數(shù)據(jù)是不完整的,你也能輕易的知道究竟是數(shù)據(jù)缺失,還是數(shù)據(jù)本身是個(gè)空。對(duì)于普通的頁(yè)面也許并無(wú)明顯價(jià)值,可如果信息是來(lái)自多個(gè)接口呢?比如從接口1獲得了UserModel的一部分?jǐn)?shù)據(jù),從接口2又補(bǔ)充了一部分,最終形成了完整的UserModel,對(duì)于有緩存需求的App這是一種非常理想的節(jié)約流量的模式。
數(shù)據(jù)模型的建立,比較理想的是采用JSON->Model的工具,如Mantle,JsonModel等,這類工具其實(shí)并不難寫(xiě),但還是建議用一些較成熟的工具。
B. 表現(xiàn)層模型
這個(gè)非常好理解,MVVM開(kāi)發(fā)模型中View Model這一層。我還是強(qiáng)烈建議表現(xiàn)層還是要盡量接近數(shù)據(jù)層模型,最好是它的超集。切記不要另外定義一個(gè)類似UserModel2,最糟糕的一種是里面還都是Dictionary,看似一個(gè)萬(wàn)能結(jié)構(gòu),后人維護(hù)的時(shí)候會(huì)有多恐怖!
C. 客戶端持久化
除了上面說(shuō)到的網(wǎng)絡(luò)層緩存,還有一種App常用的是和業(yè)務(wù)相關(guān)的持久化。比如,啟動(dòng)的時(shí)候想快速展示之前關(guān)閉的頁(yè)面;把音樂(lè),書(shū)籍,圖片等保存到客戶端等。這些業(yè)務(wù)相關(guān)的持久化數(shù)據(jù)需要非常謹(jǐn)慎的設(shè)計(jì)。
我們通常對(duì)App的持久化問(wèn)幾個(gè)問(wèn)題,來(lái)推斷其類型:
- 保存的數(shù)據(jù)量多大?k級(jí)別還是m級(jí)別
- 是否需要本地索引
- 數(shù)據(jù)類型,二進(jìn)制還是文本?
- 讀寫(xiě)的頻率多大
第一個(gè)問(wèn)題涉及到存儲(chǔ)的方式,顯然m級(jí)別的數(shù)據(jù)不適合直接在數(shù)據(jù)庫(kù)存儲(chǔ)。第二個(gè)問(wèn)題是是否使用數(shù)據(jù)庫(kù)存儲(chǔ)的關(guān)鍵,而且提出這個(gè)問(wèn)題不能基于當(dāng)下,要為未來(lái)預(yù)留空間。第三個(gè)問(wèn)題在已經(jīng)確定使用數(shù)據(jù)庫(kù)存儲(chǔ)時(shí),需要考慮是直接存入數(shù)據(jù)庫(kù),還是采用文件+數(shù)據(jù)庫(kù)索引的模式。第四個(gè)問(wèn)題,如果高頻率的讀寫(xiě)信息,信息量又不小,數(shù)據(jù)庫(kù)不不堪重負(fù),必須事先考慮增加內(nèi)存緩沖層。
看過(guò)不少App,什么數(shù)據(jù)都往NSUserDefaults里面塞。也只能說(shuō)我們走大運(yùn),蘋果為我們做的好,起碼NSUserDefaults的效率真是理想,如果同樣的粗野作風(fēng)去操作sqlite,一定會(huì)讓你想死。
再談?wù)剶?shù)據(jù)模型與持久化模型的統(tǒng)一。用過(guò)CoreData的童鞋會(huì)明白,要存儲(chǔ)數(shù)據(jù),需要建立NSManagedObject模型。已經(jīng)有的數(shù)據(jù)Model并不能直接轉(zhuǎn)換成NSManagedObject的子類,要存儲(chǔ)一種類型要建立兩次模型,活活累死人的節(jié)奏。
Realm提供了RLMObject對(duì)象,同時(shí)支持JSON的直接轉(zhuǎn)換,似乎比JSONModel或者M(jìn)TLModel更富有天然的優(yōu)勢(shì)。但RLMObject仍然有很多不完善的功能。而且Realm的不開(kāi)源也讓人不太放心。
最理想的方式是,表現(xiàn)層可以不關(guān)注數(shù)據(jù)的來(lái)源(網(wǎng)絡(luò)來(lái)的?客戶端數(shù)據(jù)庫(kù)?),可以直接使用的Model結(jié)構(gòu)。數(shù)據(jù)邏輯有Model層自己維護(hù),包括并不限于支持自動(dòng)更新,差量更新,懶加載技術(shù)等等。