前言
本文是研發(fā)一個在線超市的電商類APP過程中苍糠,對架構(gòu)的整理叁丧。
功能:
- 1、瀏覽商品、購買商品拥娄、切換商店坷衍;
- 2、查看訂單条舔、訂單投訴枫耳、意見反饋;
- 3孟抗、登陸迁杨、退出、收貨地址管理凄硼;
- 4铅协、支付、取消訂單摊沉;
- ...
箭頭的指關(guān)系 代表著 直接調(diào)用狐史,也代表著持有引用的意思。
所有的實例都可以通過監(jiān)聽event说墨,達到交流的效果骏全。
其中Model是單例,Controller 是storyBoard 上實例尼斧,view是Controller運行時加載并初始化姜贡;
Message是工廠模式,每一個協(xié)議都實例化一個對象來解決棺棵;
Event通過封裝NSNotifycation實現(xiàn)楼咳。
網(wǎng)絡通訊是通過Message調(diào)用AFNetworking。
設計中遇到的問題
1烛恤,網(wǎng)絡層請求的封裝
一開始的做法是定義一個server類來處理請求母怜,頭文件中定義請求的類型,所有的網(wǎng)絡請求都走server類缚柏,server類直接調(diào)用AFNetworking.
大致的形式如下:
Server:NSObject
{
-(void) requestLogin;
-(void) requestLogout;
....
}
這樣的好處是直接調(diào)用苹熏,開發(fā)方便,邏輯也比較簡單船惨。
壞處是柜裸,所有的代碼都寫在一起,不方便維護粱锐,同時不好做統(tǒng)一的邏輯處理(比如說session過期等)疙挺。
現(xiàn)在采用的做法:
定義一個BaseMessage,封裝AFNetworking的調(diào)用怜浅,還有統(tǒng)一的邏輯處理铐然。
同時定義OrderMessage,CartMessage,GoodsMessage,ShopMessage等繼承BaseMessage蔬崩,來具體實現(xiàn)特定的邏輯。
這樣做的好處搀暑,把邏輯分類沥阳,代碼按照模塊分散到每個SubMessage,方便維護自点。
還可以優(yōu)化的地方:
現(xiàn)在的請求是一個Message就是一個網(wǎng)絡請求(http)桐罕,處理完之后要在Message拋出事件,來通知其他模塊的信息桂敛。并且功炮,每個Message都是相互獨立的,并沒有統(tǒng)一調(diào)度的過程术唬。
可以新建一個MessageQueue類薪伏,來存放所有的Message請求,通過MessageQueue來調(diào)度http請求粗仓。
這樣的涉及到的問題是:結(jié)果回來后嫁怀,如何通知其他對象?
做法1:
當請求Message的時候借浊,self實現(xiàn)一個接口塘淑,并且傳入self;回調(diào)的時候巴碗,直接通過接口調(diào)用朴爬;
做法2:
請求的時候,帶一個閉包參數(shù)橡淆,回調(diào)的時候直接調(diào)用閉包;
做法3:
請求之后監(jiān)聽事件母赵;回調(diào)時通過事件響應逸爵;
比較理想的做法:
有controller 、 message 凹嘲、msgCenter三個實例师倔。
controller監(jiān)聽事件,發(fā)送message周蹭;
message實現(xiàn)接口趋艘,自己把自己添加進msgCenter隊列;
msgcenter對BaseMessage進行處理凶朗,通過接口來查詢message 里ID等詳細信息瓷胧;回調(diào)后,通過調(diào)用message的接口棚愤。
controller 和 message 用的是監(jiān)聽者模式搓萧;
msgCenter 和 message 之間用的是代理模式杂数;
msgCenter 可以實現(xiàn)異步的與服務器交互,和對message 的統(tǒng)一處理瘸洛。
2揍移,MVC框架的實現(xiàn)
iOS的設計,本身就含有很多MVC的思想反肋,比如說要實現(xiàn)一個自定義UITableView那伐,就要繼承UITableView,自定義delegate石蔗,與Controller的交流 是通過delegate實現(xiàn)罕邀。同時,一個頁面就是一個controller抓督,也要繼承UIViewController燃少。
自然而然地,在寫代碼的時候就會M(model)铃在、V(View)阵具、C(controller)的區(qū)分。
但在實際的需求中定铜,遇到一些正常的需求的時候阳液,如果沒有設計好,也會很棘手揣炕。
比如說:首頁下方的購物車模塊帘皿、商品展示的模塊,這些都是需要重復出現(xiàn)的模塊畸陡,也就是需要重用的模塊鹰溜。如何設計購物車模塊,使得購物車模塊 和 持有購物車的模塊(首頁丁恭、子類目等)之間沒有耦合曹动,也是一個麻煩的事情。
具體的需求有幾個:
1牲览、購物車點開的時候墓陈,頁面除購物車的背景要灰掉,同時購車要有上滑的動畫第献;
2贡必、點擊購物車或者點擊背景的時候,購物車彈下庸毫,同時灰色背景去除仔拟;
3、購物車中點擊商品的增減岔绸,要實時反饋到頁面上(首頁理逊、子類目等)橡伞;
4、購物車點開之后的大小晋被,由購物車內(nèi)的物品決定兑徘,有最大高度;
5羡洛、購物車的物品減到0的時候挂脑,不消除;
....
controller與view之間的交互欲侮,controller持有view崭闲,可以 直接調(diào)用view;view要調(diào)用controller或者其他view威蕉,可以通過事件刁俭、委托等方式;
不管是事件還是委托韧涨,為的是解耦牍戚,讓view與controller之間不耦合,所以切記不可在view定義一個controller的屬性虑粥,然后傳遞controller進來如孝。
解決方案:
view 與 controller 之間用委托(記得@property(weak),否則循環(huán)引用娩贷,內(nèi)存無法釋放)第晰;
view 與 view 之間用事件機制;
在interface builder可以用outlet 來做委托機制彬祖,非常方便茁瘦,具體的流程和UITableView類似;
3储笑,事件機制
沒有用第三方的事件機制腹躁,用的是NSNotifycation來實現(xiàn);
一開始的做法是:
#define NOTIFY_SERVER_USER_LOGIN @"NOTIFY_SERVER_USER_LOGIN"
#define NOTIFY_UI_REQUEST_PHONE_CALL @"NOTIFY_UI_REQUEST_PHONE_CALL"
直接define具體的協(xié)議南蓬,如果帶有數(shù)據(jù),就存到notify的userInfo哑了;
這樣在代碼寫了比較多時候赘方,當改動一個notify的userInfo的時候,經(jīng)常會忘記改其他的某處弱左,而且往往記不得userInfo里面的數(shù)據(jù)格式窄陡,不便于維護;
于是在NSNotify的基礎(chǔ)上做了一層封裝:
定義一個BaseEvent拆火,負責把event的類型轉(zhuǎn)成nsnotify(類型名字@""跳夭,屬性轉(zhuǎn)成dict)涂圆,同時把nsnotify轉(zhuǎn)成event;
同時Event分成幾類ErrorEvent ServerEvent UIEvent DataEvent等。
比如:
@interface UIMessageConfirmEvent : BaseEvent
@property (nonatomic , copy) NSString* message;
@end
發(fā)送事件的時候币叹,可以直接在event類型里面給message賦值润歉;
開發(fā)中的原則
- 1,不要有很大的文件颈抚,除非很少改動踩衩;(大文件,不方便維護和開發(fā))
- 2贩汉,一個函數(shù)盡量只做一個功能驱富,如果有多個地方調(diào)用,要保證調(diào)用的意義是相同的匹舞;(盡量不要在調(diào)用參數(shù)中帶默認參數(shù)褐鸥,或者在復雜調(diào)用中帶flag來標示這次調(diào)用的含義)
- 3,調(diào)用時的參數(shù)檢查赐稽,一般由被調(diào)用的函數(shù)檢查參數(shù)叫榕;如果涉及到參數(shù)不對時,需要有相應的邏輯操作又憨,比如彈出提示框等的翠霍,盡量由調(diào)用者來檢查參數(shù);
開發(fā)中的問題
問題1:model要不要監(jiān)聽事件蠢莺?
目前寒匙,Model需要被改變的時候是:
1、viewController請求數(shù)據(jù)時候躏将;
2锄弱、message發(fā)生變化的時候掌动;(比如說登陸邻遏、注銷泵殴、商店切換)
壞處:message處有各個model的代碼切厘。
比如說切換商店后外邓,就有這些變換止吁;
[self lyPostNotification:NOTIFY_INDEX_DATA];
[[CartModel instance] onShopChange]; [[CategoryDetailModel instance] onShopChange];
[[ServiceModel instance] onShopChange];
登出后把篓,需要把這些Model清空单旁;
[self lyPostNotification:NOTIFY_USER_LOGOUT];
[[CartModel instance] clearCache]; [[AddressModel instance] clearCache]; [[UserModel instance] clearCache];
[[OrderModel instance] clearCache];
如果新增model比如說新添加的serviceModel拦赠,需要在message處添加相應的邏輯代碼巍沙;
還有的問題是,Model的clearCache等函數(shù)被message調(diào)用荷鼠,耦合性特別強句携;相當于做了事情A(登出),接著馬上做事情B(清空數(shù)據(jù))允乐,而且是直接調(diào)用矮嫉,以后修改起來很容易犯錯誤削咆。
設想:
如果model監(jiān)聽事件的話;
那么model只需監(jiān)聽各個事件蠢笋,然后再寫處理函數(shù)拨齐;
model的邏輯都聚合在自身;
帶來的后果是挺尿,model稍微變大奏黑。多個事件,和多個處理函數(shù)编矾。
不過這個如果是message處來調(diào)用熟史,處理函數(shù)還是一樣要寫,只是多了事件處理監(jiān)聽的代碼窄俏。
model的邏輯是聚合了蹂匹,但是注銷的邏輯分散了,看不出來注銷完干了什么事情凹蜈。這個問題好像也不是問題限寞,因為可以查看事件的監(jiān)聽者,看到監(jiān)聽了注銷事件的model仰坦。
有一處代碼履植,集合了所有的model instance。所有的model都必須先初始化悄晃。否則無法監(jiān)聽事件玫霎。
問題2:調(diào)用message的時候,message從各個model獲取了數(shù)據(jù)妈橄,那么庶近,誰來做參數(shù)檢查?
1眷蚓,誰調(diào)用(不同層次之間)鼻种,誰負責檢查參數(shù);比如orderMessage需要用戶登錄沙热,那么調(diào)用orderMessage的時候叉钥,就必須保證已經(jīng)登錄。(因為message和model不在同一層次)
2篙贸,如果是檢查的內(nèi)容是自身內(nèi)容沼侣,那么由自己負責。
問題3:從頁面A (ControllerA) 跳到 頁面B(ControllerB)歉秫,用戶的操作數(shù)據(jù)(比如說留言),怎么合適地從A傳到B养铸?
這個是1.5版本架構(gòu)具體的實現(xiàn)雁芙,參考兩個概念MVP MVVM轧膘。
讓顯示邏輯和業(yè)務邏輯有所分開,是比較重要的兔甘。
待完成微信端的開發(fā)谎碍,再來寫一版詳細的改動日志。 (然而不可能)
附錄
蘑菇街的IM 網(wǎng)絡層:
API center 負責管理洞焙、注冊所有的API蟆淀;接受服務器數(shù)據(jù)(可以分離連接代買,僅提供接受數(shù)據(jù)的接口)澡匪,調(diào)用解析接口熔任,回調(diào)API;超時處理唁情;有自己的線程疑苔。
有一個SuperAPI 還有一個 APIProtocal
superAPI 是所有API的基類,負責注冊request和respone甸鸟、timeout惦费、保存返回閉包、打包數(shù)據(jù)(調(diào)用protocal)抢韭、發(fā)送數(shù)據(jù)薪贫。
APIProtocal封裝了協(xié)議的解析、打包刻恭。
controller調(diào)用實現(xiàn)了Protocal的superAPI子類(childAPI)瞧省,并且把回調(diào)函數(shù)的閉包傳遞進去,放在superAPI吠各。
childAPI實現(xiàn)了APIProtocal 從而對服務器返回的數(shù)據(jù)解析臀突,并且返回cmd、msgID贾漏。
APIcenter 有四個map(request response notify timeout)
request是請求 response是回復 timeout是超時處理 notify是服務器通知候学。
每一次通訊,都需要注冊request纵散、response梳码、timeout。
消息處理完后伍掀,remove from map掰茶。(notify不需要remove)
流程如下:
1.controller 調(diào)用 childAPI的request,傳入數(shù)據(jù)(object)蜜笤,和completion閉包濒蒋。
2.childAPI->superAPI 的request統(tǒng)一處理流程,在center,注冊request和respone沪伙、timeout瓮顽、保存返回閉包、打包數(shù)據(jù)(調(diào)用protocal)围橡、發(fā)送數(shù)據(jù)暖混。
3.superAPI通過APIProtocol 調(diào)用childAPI的數(shù)據(jù)打包,調(diào)用center 的發(fā)送翁授。
4.center收到tcp返回的bytes拣播,調(diào)用protocal(在responseMap注冊的API)解析。
5.解析完成清除map中的API(request收擦、response贮配、timeout),在主線程回調(diào)completion閉包炬守,并且將數(shù)據(jù)傳遞過去牧嫉。
6.controller 收到數(shù)據(jù),存到model减途,顯示處理酣藻。
7.如果在4過程中,timeout到了鳍置,那么直接清除request辽剧、resonse、timeout税产,回調(diào)timeout閉包怕轿。
總結(jié)
- 1,一個函數(shù)盡量只做一個功能辟拷,如果有多個地方調(diào)用撞羽,要保證意義相同。(這樣才能復用衫冻,不要帶flag的參數(shù)诀紊,混亂)
- 2,業(yè)務邏輯隅俘、顯示邏輯的代碼 集合在一起邻奠。(ReactiveCocoa)
- 3,盡量少出現(xiàn)allModel为居、allEvent 的東西碌宴,不方便添加,多人開發(fā)會沖突蒙畴;
- 4贰镣,發(fā)現(xiàn)改一個東西,需要改很多處代碼的時候,要考察下關(guān)系問題;
Callback block 代替delegate和event進行回調(diào)八孝,實現(xiàn)業(yè)務聚合董朝,(注意,這里不能代替某些event干跛,比如說登出、拉取訂單等祟绊,但是最好這些可以反映到model楼入,由model發(fā)出變化事件)最合適用于只有一次event 監(jiān)聽,的event牧抽。
ViewController的瘦身是MVC實現(xiàn)的要點嘉熊,用Category、業(yè)務細分然后把delegate把代碼劃分到對應的類扬舒。
后記
MVC阐肤、MVVM、MVP都是很不錯的思想讲坎,設計模式里面更是前人經(jīng)驗的總結(jié)孕惜。
原本計劃在做完整個APP再進行一次總結(jié),可后來做微信端的開發(fā)晨炕,學習Angular-js花費了一大塊的時間衫画。再后來,就喜歡玩其他東西了瓮栗。
培養(yǎng)一些興趣削罩,這些興趣不是物質(zhì)的享受,而是一種能讓你思考费奸,進步的興趣弥激。