寫在前面
在一名一線開發(fā)對于App架構(gòu)和組件化的思考 文章中安拟,我們主要站在了軟件工程的角度上,分析了做App架構(gòu)和組件化時該如何下手凳厢,其中也介紹了路由和服務(wù)模塊在組件化中扮演的重要角色。本文料身,我們將進(jìn)行實(shí)操,一步步實(shí)現(xiàn)一個模塊間通信的服務(wù)組件衩茸。
這里剖出一個微服務(wù)的概念芹血,在Java Spring框架中,微服務(wù)是個很火的東西楞慈。鑒于筆者對于Java一概不知幔烛,所以僅僅站在作為一個App開發(fā)的角度去認(rèn)知它。微服務(wù)確切的說是某個功能模塊的子集囊蓝,它把單體架構(gòu)中的某些功能拆離出來饿悬,然后開啟獨(dú)立進(jìn)程來給其他模塊提供服務(wù),通信方式一般是標(biāo)準(zhǔn)REST API來進(jìn)行聚霜。這樣的做的話有幾個好處狡恬。
**1.獨(dú)立進(jìn)程,獨(dú)立部署蝎宇。不會因?yàn)閱误w架構(gòu)機(jī)器掛掉后弟劲,導(dǎo)致所有服務(wù)不可用。 **
**2.避免項(xiàng)目過度臃腫姥芥。 **
3.擴(kuò)展性強(qiáng)兔乞,可以多個微服務(wù)組成集群。
對于Java Spring框架撇眯,這里就不做過多贅述了报嵌。推薦一個比較形象的描述微服務(wù)的漫畫,感興趣的可以看一下熊榛,這樣可以對整個系統(tǒng)上下游架構(gòu)會有更深的理解。
服務(wù)組件在App里應(yīng)用場景
舉個栗子玄坦。
還是拿登錄模塊舉例子。绘沉。煎楣。
在之前的分享中我們知道,登錄模塊一般位于App分層架構(gòu)中的通用模塊層车伞。假如說A模塊要調(diào)用登錄模塊中的獲取登錄態(tài)的方法择懂,在沒有服務(wù)組件的情況下,我們一般會直接把登錄整個模塊import進(jìn)來另玖,這樣做難免有點(diǎn)小尷尬(僅僅是獲取個登錄態(tài)困曙,我就要把整個登錄模塊import進(jìn)來表伦,這樣就耦合在一起)。
再打個很形象的比喻慷丽。蹦哼。。
雖說結(jié)婚不是兩個人的事情要糊,而是兩個家庭的事情纲熏,但是結(jié)婚后你老丈人和丈母娘一起打包過來跟你過了,你是什么感受锄俄?那肯定是臉上笑嘻嘻局劲,心里mmp啊。我是要跟你女兒過日子的啊奶赠,咋都打包給我了鱼填??车柠?剔氏。
所以通過以上的生動的示例,我們總結(jié)出了服務(wù)組件在App里的應(yīng)用場景竹祷。
模塊間更小粒度組件間的通信場景谈跛。
開放一個模塊中某些特定功能API場景,使模塊中的子組件“微服務(wù)化”塑陵。
組件化之間進(jìn)行解耦的應(yīng)用場景感憾。
從0到1編寫一個服務(wù)組件
方案一:通知中心(NSNotificationCenter)
Excuse me?通知不是單向數(shù)據(jù)傳輸么,A給B發(fā)通知令花,B收到通知后處理阻桅,貌似不符合我們這種有返回值的需求啊兼都?
在OC中有個神奇的東西那就是Block嫂沉,說白了是匿名函數(shù),那我們直接把函數(shù)指針傳輸過去不就可以了嘛扮碧?而且我們知道在OC中Block本質(zhì)上是一個對象趟章,恰好發(fā)送通知可以攜帶一個對象,豈不美哉慎王。
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
說寫咱就寫蚓土!Perfect!你為何如此優(yōu)秀@涤佟J衿帷!
登錄模塊:
/*登錄模塊注冊通知*/[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getCookie) name:@"getCookie" object:nil]; - (void)getCookie:(NSNotification *)noti { void(^callBack)(NSString *) = noti.object; /*獲取cookie邏輯*/ ---this is a long story--- /*獲取完畢之后咱旱,調(diào)用block*/ if (callBack) { callBack(@"cookie"); }}
調(diào)用模塊:
/*創(chuàng)建一個Block*/void(^callBack)(NSString *) = ^(NSString *cookie) { NSLog(@"cookie->%@",cookie); };/*調(diào)用方通過發(fā)送通知*/[[NSNotificationCenter defaultCenter] postNotificationName:@"getCookie" object:callBack];
Command+R确丢,完美绷耍!滿足需求,我們成功地在模塊中獲取到登錄模塊中的登錄態(tài)蠕嫁。
這時候我們停下來仔細(xì)想一下通知中心的方案锨天,假如說登錄模塊除了提供獲取登錄態(tài)的服務(wù),可能還有獲取用戶信息服務(wù)等等剃毒。如果服務(wù)越來越多病袄,注冊通知就會分散在不同的文件中、不同的代碼邏輯中赘阀,服務(wù)太分散難以維護(hù)R娌!基公!
我們總結(jié)了一下幅慌,很容易發(fā)現(xiàn)通知的方案所存在的問題。
注冊通知太分散轰豆,難以維護(hù)胰伍。
沒有統(tǒng)一的地方來維護(hù)通知名稱,調(diào)用方需要預(yù)先知道通知名才能調(diào)用該服務(wù)酸休。
傳參數(shù)不太方便骂租,雖然系統(tǒng)發(fā)送通知函數(shù)提供了一個object,但在復(fù)雜業(yè)務(wù)中遠(yuǎn)遠(yuǎn)不夠斑司。
通知中心存在一定的問題渗饮,比如說不支持異步通知(在A線程注冊通知,B線程發(fā)送通知宿刮,接收到通知后回到A線程進(jìn)行處理)互站。
關(guān)于通知中心的弊端,這里也不做贅述僵缺,推薦一個自己之前寫的一個通知中心解決方案胡桃,目前還不太完善。其使用姿勢相當(dāng)優(yōu)雅磕潮,而且實(shí)現(xiàn)了異步通知标捺,感興趣的筒子們可以了解一下。
方案二:反射機(jī)制(NSClassFromString)
名詞解釋:Java反射說的是在運(yùn)行狀態(tài)中茂缚,對于任何一個類戏罢,我們都能夠知道這個類有哪些方法和屬性屋谭。對于任何一個對象,我們都能夠?qū)λ姆椒ê蛯傩赃M(jìn)行調(diào)用龟糕。我們把這種動態(tài)獲取對象信息和調(diào)用對象方法的功能稱之為反射機(jī)制桐磁。以上內(nèi)容來自于網(wǎng)絡(luò)。
在OC中讲岁,runtime也提供了類似的機(jī)制我擂,我們可以通過runtime提供的函數(shù),在運(yùn)行時動態(tài)地獲取到某個類缓艳、方法校摩、屬性等。
NSClassFromString(<#NSString * _Nonnull aClassName#>)
NSSelectorFromString(<#NSString * _Nonnull aSelectorName#>)
既然方案一注冊通知太分散阶淘,那我們可不可以對于每個服務(wù)創(chuàng)建一個類衙吩,然后暴露方法,通過runtime反射機(jī)制去調(diào)用溪窒?
第一步:針對獲取登錄態(tài)的服務(wù)單獨(dú)創(chuàng)建類文件坤塞。
第二步:在類文件中開放一個方法供調(diào)用方調(diào)用。
第三步:調(diào)用方通過NSClassFromString獲取到登錄態(tài)的Class澈蚌。
第四步:調(diào)用方通過NSSelectorFromString獲取到登錄態(tài)提供的selector摹芙。
第五步:調(diào)用該方法- (id)performSelector:(SEL)aSelector withObject:(id)object;完成該服務(wù)的調(diào)用。
登錄模塊:
/*我們在登錄模塊創(chuàng)建一個GetLoginCookie類*//*.h和.m如下*/
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface GetLoginCookie : NSObject- (id)getLoginCookieWithObjc:(id)obj;@endNS_ASSUME_NONNULL_END
調(diào)用模塊:
/*在模塊中獲取到GetLoginCookie的Class*/Class cookieCls = NSClassFromString(@"GetLoginCookie");/*通過Class惜浅,生成一個GetLoginCookie實(shí)例*/id cookieInstance = [[cookieCls alloc]init];/*通過方法名生成一個SEL*/SEL selector = NSSelectorFromString(@"getLoginCookieWithObjc:");/*調(diào)用performSelector并獲取返回值*/NSString *cookie = [cookieInstance performSelector:selector withObject:@"it's me!"];NSLog(@"cookie->%@",cookie);
Command + R瘫辩,完美運(yùn)行,我們也得到了我們想要的結(jié)果坛悉。
對比方案一和方案二伐厌,方案二的確解決了服務(wù)分散不好管理的問題,但是依然存在幾個問題裸影。
依然沒有一個配置的地方讓調(diào)用者一下就能看到類名或者sel名挣轨,方便進(jìn)行調(diào)用。
還有個問題轩猩,我們很容易發(fā)現(xiàn)這兩個方案都是“去中心化的”卷扮。也就是說,消息的發(fā)送和消息的接收處理都是直接點(diǎn)對點(diǎn)的均践。去中心化帶來了很多問題晤锹,如果登錄態(tài)的服務(wù)出現(xiàn)問題,而我們又沒有一個統(tǒng)一收口的地方統(tǒng)一處理彤委,不可控鞭铆。
這就好比區(qū)塊鏈技術(shù)去中心化雖然帶來了很多技術(shù)變革,但同樣也帶來了一些隱患焦影。如果沒有上面的的監(jiān)管车遂,那很多black money可以通過區(qū)塊鏈?zhǔn)侄蜗吹絿夥舛稀O胂牒芏嘭澒倌弥覀冃列量嗫嗬U納的稅,把貪來的錢都洗到了國外舶担,然后老婆孩子在國外逍遙自在坡疼,自己在國內(nèi)做luo官。而我們依然活在水深火熱之中衣陶,百姓民不聊生柄瑰,苦不堪言,我們內(nèi)心該是何等氣氛W娲辍S狻!拯欧。
扯多了详囤,我們回到正題。
方案三:引入中間件(IQService)
通過對比前兩個方案镐作,我們大概對于服務(wù)組件應(yīng)該滿足哪些要素有了更加清晰的認(rèn)識藏姐。
服務(wù)組件要易于管理,統(tǒng)一分布在模塊中的某個地方该贾。
服務(wù)組件最好通過配置文件去管理羔杨,方便業(yè)務(wù)方查閱調(diào)用等。
服務(wù)組件去Model化杨蛋,徹底解除兜材、還有支持同步異步調(diào)用等。
服務(wù)組件最好用中間件方式逞力,有統(tǒng)一收口的地方曙寡,發(fā)生問題可控。
服務(wù)組件最好支持靜態(tài)注冊寇荧、動態(tài)注冊等举庶,擴(kuò)展性高。
我們來簡單畫一下揩抡,服務(wù)組件架構(gòu)圖户侥。
- 首先為了解決服務(wù)易于管理問題,我們這里使用plist來維護(hù)業(yè)務(wù)服務(wù)列表和具體服務(wù)名與服務(wù)的對應(yīng)關(guān)系峦嗤。
如圖所示蕊唐,IQService.plist維護(hù)了業(yè)務(wù)list,一般IQService主工程維護(hù)一份即可烁设。
LoginModule.plist中維護(hù)了該組件為外部提供的所有服務(wù)列表(服務(wù)名和實(shí)現(xiàn)類的對應(yīng)關(guān)系)
- 去model化刃泌,我們這里用多參數(shù)來解決(也可以通過NSDictionry解決)。
/** 同步、異步調(diào)用 @param sevice 微服務(wù)名 */+ (void)invokeMicroService:(NSString *)sevice,...;/** 同步調(diào)用 @param service 微服務(wù)名 @return 同步調(diào)用返回值 */+ (id)invokeMicroServiceSync:(NSString *)service,...;
我們再來看下具體的使用姿勢耙替。
登錄模塊:
首先創(chuàng)建LoginModuleCookieService類,并將該類注冊到LoginModule中曹体。
.h聲明#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface LoginModuleCookieService : NSObject- (NSString *)getCookieWithSignature:(NSString *)signature;@endNS_ASSUME_NONNULL_END
.m實(shí)現(xiàn)#import "LoginModuleCookieService.h"@implementation LoginModuleCookieService- (NSString *)getCookieWithSignature:(NSString *)signature { return [NSString stringWithFormat:@"%@->cookie",signature];}@end
調(diào)用模塊:
同步調(diào)用NSString *cookie = [IQService invokeMicroServiceSync:@"GetCookieSyncService",@"我是同步調(diào)用",nil];NSLog(@"%@",cookie);
異步調(diào)用void (^callBack)(NSString *) = ^(NSString *cookie){ NSLog(@"%@",cookie); };[IQService invokeMicroService:@"GetCookieAsyncService",@"我是異步調(diào)用",callBack,nil];
分析到現(xiàn)在俗扇,方案三基本能滿足大部分業(yè)務(wù)需求。具體實(shí)現(xiàn)代碼已經(jīng)開源到GitHub -----> IQService箕别,一個iOS端模塊間通信的解決方案铜幽。喜歡的筒子可以來波Star??,也歡迎大家提交PR和ISSUE串稀。
騙大家刷完Star除抛,現(xiàn)在再潑盆冷水。母截。到忽。
我們再仔細(xì)思考一下方案三,貌似有幾個問題依然沒有解決
1.編譯時依然無法進(jìn)行參數(shù)正確性校驗(yàn)清寇,attribute喘漏?宏定義?
2.目前只有靜態(tài)注冊华烟,不支持動態(tài)注冊翩迈。
上面兩個問題,歡迎大家進(jìn)行頭腦風(fēng)暴盔夜。有好的解決方案可以留言分享负饲,也可以提交PRs or Issues。https://github.com/Lobster-King/IQService喂链。
在這里提示一點(diǎn)返十,沒有一個方案是100%OK的,只有適合自己的才是最好的衩藤。
架構(gòu)和組件化系列文章預(yù)告:說說MVVM吧慢,會一步步跟大家一起寫一個輕量的view和viewModel進(jìn)行數(shù)據(jù)綁定的框架。
**文章首發(fā)GitHub **https://github.com/Lobster-King/AppArticles