寫在前面
在一名一線開發對於App架構和元件化的思考 文章中,我們主要站在了軟體工程的角度上,分析了做App架構和元件化時該如何下手,其中也介紹了路由和服務模組在元件化中扮演的重要角色。本文,我們將進行實操,一步步實現一個模組間通訊的服務元件。
這裡剖出一個微服務的概念,在Java Spring框架中,微服務是個很火的東西。鑑於筆者對於Java一概不知,所以僅僅站在作為一個App開發的角度去認知它。微服務確切的說是某個功能模組的子集,它把單體架構中的某些功能拆離出來,然後開啟獨立程式來給其他模組提供服務,通訊方式一般是標準REST API來進行。這樣的做的話有幾個好處。
1.獨立程式,獨立部署。不會因為單體架構機器掛掉後,導致所有服務不可用。
2.避免專案過度臃腫。
3.擴充套件性強,可以多個微服務組成叢集。
對於Java Spring框架,這裡就不做過多贅述了。推薦一個比較形象的描述微服務的漫畫,感興趣的可以看一下,這樣可以對整個系統上下游架構會有更深的理解。
服務元件在App裡應用場景
舉個例子?。
還是拿登入模組舉例子。。。
在之前的分享中我們知道,登入模組一般位於App分層架構中的通用模組層。假如說A模組要呼叫登入模組中的獲取登入態的方法,在沒有服務元件的情況下,我們一般會直接把登入整個模組import進來,這樣做難免有點小尷尬(僅僅是獲取個登入態,我就要把整個登入模組import進來,這樣就耦合在一起)。
再打個很形象的比喻。。。
雖說結婚不是兩個人的事情,而是兩個家庭的事情,但是結婚後你老丈人和丈母孃一起打包過來跟你過了,你是什麼感受?那肯定是臉上笑嘻嘻,心裡mmp啊。我是要跟你女兒過日子的啊,咋都打包給我了????。
所以通過以上的生動的示例,我們總結出了服務元件在App裡的應用場景。
- 模組間更小粒度元件間的通訊場景。
- 開放一個模組中某些特定功能API場景,使模組中的子元件“微服務化”。
- 元件化之間進行解耦的應用場景。
從0到1編寫一個服務元件
方案一:通知中心(NSNotificationCenter)
Excuse me?通知不是單向資料傳輸麼,A給B發通知,B收到通知後處理,貌似不符合我們這種有返回值的需求啊?
在OC中有個神奇的東西那就是Block,說白了是匿名函式,那我們直接把函式指標傳輸過去不就可以了嘛?而且我們知道在OC中Block本質上是一個物件,恰好傳送通知可以攜帶一個物件,豈不美哉。
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
說寫我們就寫!Perfect!你為何如此優秀!!!
登入模組:
/*登入模組註冊通知*/
[[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---
/*獲取完畢之後,呼叫block*/
if (callBack) {
callBack(@"cookie");
}
}
複製程式碼
呼叫模組:
/*建立一個Block*/
void(^callBack)(NSString *) = ^(NSString *cookie) {
NSLog(@"cookie->%@",cookie);
};
/*呼叫方通過傳送通知*/
[[NSNotificationCenter defaultCenter] postNotificationName:@"getCookie" object:callBack];
複製程式碼
Command+B,完美!滿足需求,我們成功地在模組中獲取到登入模組中的登入態。
這時候我們停下來仔細想一下通知中心的方案,假如說登入模組除了提供獲取登入態的服務,可能還有獲取使用者資訊服務等等。如果服務越來越多,註冊通知就會分散在不同的檔案中、不同的程式碼邏輯中,服務太分散難以維護!!!
我們總結了一下,很容易發現通知的方案所存在的問題。
- 註冊通知太分散,難以維護。
- 沒有統一的地方來維護通知名稱,呼叫方需要預先知道通知名才能呼叫該服務。
- 傳引數不太方便,雖然系統傳送通知函式提供了一個object,但在複雜業務中遠遠不夠。
- 通知中心存在一定的問題,比如說不支援非同步通知(在A執行緒註冊通知,B執行緒傳送通知,接收到通知後回到A執行緒進行處理)。
關於通知中心的弊端,這裡也不做贅述,推薦一個自己之前寫的一個通知中心解決方案,目前還不太完善。其使用姿勢相當優雅,而且實現了非同步通知,感興趣的筒子們可以瞭解一下。
SmartBlock(一個用Block實現的通知替代方案,並且已實現在不同執行緒進行傳送訊息和執行Block,支援多引數傳送,解決回撥地獄問題,適用於元件化資料傳輸等。)
方案二:反射機制(NSClassFromString)
名詞解釋:Java反射說的是在執行狀態中,對於任何一個類,我們都能夠知道這個類有哪些方法和屬性。對於任何一個物件,我們都能夠對它的方法和屬性進行呼叫。我們把這種動態獲取物件資訊和呼叫物件方法的功能稱之為反射機制。以上內容來自於網上。
在OC中,runtime也提供了類似的機制,我們可以通過runtime提供的函式,在執行時動態地獲取到某個類、方法、屬性等。
NSClassFromString(<#NSString * _Nonnull aClassName#>)
NSSelectorFromString(<#NSString * _Nonnull aSelectorName#>)
既然方案一註冊通知太分散,那我們可不可以對於每個服務建立一個類,然後暴露方法,通過runtime反射機制去呼叫?
- 第一步:針對獲取登入態的服務單獨建立類檔案。
- 第二步:在類檔案中開放一個方法供呼叫方呼叫。
- 第三步:呼叫方通過NSClassFromString獲取到登入態的Class。
- 第四步:呼叫方通過NSSelectorFromString獲取到登入態提供的selector。
- 第五步:呼叫該方法- (id)performSelector:(SEL)aSelector withObject:(id)object;完成該服務的呼叫。
登入模組:
/*我們在登入模組建立一個GetLoginCookie類*/
/*.h和.m如下*/
複製程式碼
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface GetLoginCookie : NSObject
- (id)getLoginCookieWithObjc:(id)obj;
@end
NS_ASSUME_NONNULL_END
複製程式碼
#import "GetLoginCookie.h"
@implementation GetLoginCookie
- (id)getLoginCookieWithObjc:(id)obj {
return @"cookie";
}
@end
複製程式碼
呼叫模組:
/*在模組中獲取到GetLoginCookie的Class*/
Class cookieCls = NSClassFromString(@"GetLoginCookie");
/*通過Class,生成一個GetLoginCookie例項*/
id cookieInstance = [[cookieCls alloc]init];
/*通過方法名生成一個SEL*/
SEL selector = NSSelectorFromString(@"getLoginCookieWithObjc:");
/*呼叫performSelector並獲取返回值*/
NSString *cookie = [cookieInstance performSelector:selector withObject:@"it's me!"];
NSLog(@"cookie->%@",cookie);
複製程式碼
Command + B,完美執行,我們也得到了我們想要的結果。
對比方案一和方案二,方案二的確解決了服務分散不好管理的問題,但是依然存在幾個問題。
- 依然沒有一個配置的地方讓呼叫者一下就能看到類名或者sel名,方便進行呼叫。
- 還有個問題,我們很容易發現這兩個方案都是**“去中心化的”**。也就是說,訊息的傳送和訊息的接收處理都是直接點對點的。去中心化帶來了很多問題,如果登入態的服務出現問題,而我們又沒有一個統一收口的地方統一處理,不可控。
這就好比區塊鏈技術去中心化雖然帶來了很多技術變革,但同樣也帶來了一些隱患。如果沒有上面的?的監管,那很多black money?可以通過區塊鏈手段洗到國外。想想很多貪官拿著我們辛辛苦苦繳納的稅,把貪來的錢都洗到了國外,然後老婆孩子在國外逍遙自在,自己在國內做luo官。而我們依然活在水深火熱之中,百姓民不聊生,苦不堪言,我們內心該是何等氣氛!!!?。
扯多了,我們回到正題。?
方案三:引入中介軟體(IQService)
通過對比前兩個方案,我們大概對於服務元件應該滿足哪些要素有了更加清晰的認識。
- 服務元件要易於管理,統一分佈在模組中的某個地方。
- 服務元件最好通過配置檔案去管理,方便業務方查閱呼叫等。
- 服務元件去Model化,徹底解除、還有支援同步非同步呼叫等。
- 服務元件最好用中介軟體方式,有統一收口的地方,發生問題可控。
- 服務元件最好支援靜態註冊、動態註冊等,擴充套件性高。
我們來簡單畫一下,服務元件架構圖。
- 首先為了解決服務易於管理問題,我們這裡使用plist來維護業務服務列表和具體服務名與服務的對應關係。
如圖所示,IQService.plist維護了業務list,一般IQService主工程維護一份即可。
LoginModule.plist中維護了該元件為外部提供的所有服務列表(服務名和實現類的對應關係)
- 去model化,我們這裡用多引數來解決(也可以通過NSDictionry解決)。
/**
同步、非同步呼叫
@param sevice 微服務名
*/
+ (void)invokeMicroService:(NSString *)sevice,...;
/**
同步呼叫
@param service 微服務名
@return 同步呼叫返回值
*/
+ (id)invokeMicroServiceSync:(NSString *)service,...;
複製程式碼
我們再來看下具體的使用姿勢。
登入模組:
首先建立LoginModuleCookieService類,並將該類註冊到LoginModule中。
複製程式碼
.h宣告
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LoginModuleCookieService : NSObject
- (NSString *)getCookieWithSignature:(NSString *)signature;
@end
NS_ASSUME_NONNULL_END
複製程式碼
.m實現
#import "LoginModuleCookieService.h"
@implementation LoginModuleCookieService
- (NSString *)getCookieWithSignature:(NSString *)signature {
return [NSString stringWithFormat:@"%@->cookie",signature];
}
@end
複製程式碼
呼叫模組:
同步呼叫
NSString *cookie = [IQService invokeMicroServiceSync:@"GetCookieSyncService",@"我是同步呼叫",nil];
NSLog(@"%@",cookie);
複製程式碼
非同步呼叫
void (^callBack)(NSString *) = ^(NSString *cookie){
NSLog(@"%@",cookie);
};
[IQService invokeMicroService:@"GetCookieAsyncService",@"我是非同步呼叫",callBack,nil];
複製程式碼
分析到現在,方案三基本能滿足大部分業務需求。具體實現程式碼已經開源到GitHub -----> IQService,一個iOS端模組間通訊的解決方案。喜歡的筒子可以來波Star❤️,也歡迎大家提交PR和ISSUE。
騙大家刷完Star,現在再潑盆冷水。。。?
我們再仔細思考一下方案三,貌似有幾個問題依然沒有解決
1.編譯時依然無法進行引數正確性校驗,attribute?巨集定義?
2.目前只有靜態註冊,不支援動態註冊。
上面兩個問題,歡迎大家進行頭腦風暴。有好的解決方案可以留言分享,也可以提交PRs or Issues。github.com/Lobster-Kin…。
在這裡提示一點,沒有一個方案是100%OK的,只有適合自己的才是最好的。?
架構和元件化系列文章預告:說說MVVM,會一步步跟大家寫一個輕量的view和viewModel進行資料繫結的框架。
文章首發GitHub github.com/Lobster-Kin…