目錄
- 1. 元件化是什麼
- 2. 元件化的作用
- 3. 元件化實現
- 4. 中介軟體通用工具
- 5. BeeHive和CTMediator
1. 元件化是什麼
這裡的元件化一般是指業務模組化,簡單來說就是將一個複雜的系統根據業務劃分成不同的模組,這個沒什麼好說的,一般在做專案時,就已經做好了業務模組的劃分。在討論元件化時,其實只是在討論如何在隔離各個業務模組情況下,實現模組間通訊。(下文中的元件指的就是業務模組)
2. 元件化的作用
元件化的作用是可以實現元件隔離。
元件隔離,指的是各個元件之間不會有任何直接依賴,也就是說元件不會#import
另一個元件,各個元件在編譯時是完全是解耦的。(元件間業務上的依賴是無法避免的)
這樣,各個元件就可以單獨開發和測試,而不需要依賴主工程,可以顯著的提高團隊的工作效率;
由於各個元件之間沒有任何依賴,後期專案的維護也會相對容易一點。
3. 元件化實現
元件化的目的就是隔離元件,那麼應該如何隔離,一般的解決方法是增加一個用於訊息轉發的中間層,通過這個中間層實現元件間通訊,解耦各個元件。
下面使用Limboy文章中的例子來說明這個中間層的作用
上述兩圖分別表示,在不使用中間層和使用中間層的情況下,元件間通訊時,元件和中間層的依賴關係
不使用中間層的情況下,各個元件之間都是直接依賴,就是元件直接#import
被呼叫的元件,這些依賴關係凌亂而且複雜,在這種依賴關係下,如果想要多個元件並行開發,必須跟其他元件開發者做好介面約定,這裡可能會有一份元件的介面文件,當元件介面出現變動時,需要通知所有此元件的使用者修改呼叫方法,這種情況下,後期維護會非常困難;
在使用中間層之後,所有的依賴關係都轉接到中間層上了,所有的元件間通訊都在中間層上集中處理,這樣當元件出現變化時,只需要修改中間層就可以了。
下列是中間層Mediator
的程式碼實現:
//Mediator.m
#import "BookDetailComponent.h"
#import "ReviewComponent.h"
@implementation Mediator
+ (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
return [BookDetailComponent detailViewController:bookId];
}
+ (UIViewController *)ReviewComponent_viewController:(NSString *)bookId reviewType:(NSInteger)type {
return [ReviewComponent reviewViewController:bookId type:type];
}
@end
複製程式碼
到目前為止,已經初步實現元件化了,各個元件相互隔離,並且可以相互通訊。
存在的問題
-
最顯著的就是中間層的程式碼的維護問題,當專案中的元件越來越多,中間層的程式碼會越發膨脹,到那個時候,維護中間層可能會花費大量的時間。
-
另外就是中間層對元件存在依賴,這樣的話,就很難將中間層抽取出來單獨使用了,比如在新工程裡面開發新元件的時候,想使用中間層,卻發現需要引用其他所有的元件。
對於第一個問題,其解決方案一般是將中間層中的介面進行分類,讓各個元件的建立者維護自己的中間層介面。 對於第二個問題,需要打破中間層對元件的依賴,然後再做一些異常判斷。
對於上述兩個問題,BeeHive和CTMediator這兩個元件化工具都有一套完整的解決方案,下文中將通過分析這兩個工具,來說明它們的具體步驟以及其內在聯絡。
4. 中介軟體通用工具
中間層的作用是幫助不同元件進行通訊,它不可避免的會對元件形成依賴。雖然可以通過一些手段使得中間層與元件在編譯層面上解耦了,但是中間層和元件仍然會存在業務上的關聯。 換句話說,當使用中間層隔離各個元件時,中間層必然會與業務存在關聯。
如果想要複用中間層,則必須將具體的業務邏輯剝離出中間層。在本文中,將剝離了具體業務的中間層稱作中介軟體通用工具,BeeHive和CTMediator都是這種工具,下一節會講到它們。
想要建立一箇中介軟體通用工具,就需要搞清楚,中間層中哪些操作是業務相關的,哪些是非業務相關的。
元件間通訊的流程可能有如下幾個步驟:
-
呼叫者發起呼叫 元件呼叫者至少需要傳遞一個識別符號給中間層,告訴中間層它想要呼叫哪個元件
-
中間層返回目標元件的控制程式碼 根據呼叫者傳入的識別符號,中間層返回一個目標元件的控制程式碼,使用這個元件控制程式碼就可以和元件進行互動。 這個控制程式碼可能是一個響應類,可能是一個可執行程式碼塊,或者是其他可用來和目標元件互動的東西。
-
呼叫目標元件 通過使用這個控制程式碼,可以和目標元件進行互動
從上述流程可知,中間層必定存在某種對映關係來指定識別符號和元件控制程式碼的對應關係,這種對映關係指定了元件的呼叫邏輯。
在這個流程中,與中間層相關的步驟如下:
-
生成對映關係 對映關係指定了元件的呼叫邏輯,生成這種對映關係的部分,必定與業務相關聯。
-
儲存對映關係
-
獲取元件控制程式碼 中間層一般是使用一個字典來儲存這種對映關係,在儲存和使用這種對映關係時,僅僅將它當做普通的物件來操作,所以通常[步驟2]和[步驟3]是與業務無關的。
-
使用元件控制程式碼 如果元件控制程式碼是要特定的上下文才能使用,比如是一個響應類,在使用控制程式碼時,需要依賴於業務邏輯; 如果元件控制程式碼不需要特定的上下文就能使用,比如是一個block,在使用控制程式碼時,不需要依賴於業務邏輯。
上述四個步驟,[步驟1]與業務相關的,[步驟2]和[步驟3]兩個步驟與業務無關,[步驟4]則需要看情況而定。
如果想要建立一箇中介軟體通用工具,則必須將業務邏輯從中間層中剝離出來,然後中間層中剩餘的邏輯就是中介軟體通用工具需要負責的部分了。 很明顯,中介軟體通用工具可以包含[步驟2]和[步驟3],其功能如下:
- 將生成的對映關係儲存起來
- 根據呼叫者傳入的識別符號,返回元件控制程式碼
根據具體情況,中介軟體通用工具也可以包含[步驟4],負責直接使用元件控制程式碼。
5. BeeHive和CTMediator
BeeHive和CTMediator是兩個常用的中介軟體通用工具,它們的解決方案都比較成熟,下面簡單解析一下這兩個工具,看看他們是如何實現的。
5.1. BeeHive
BeeHive
使用protocol-impClass
方式來表示上文所說的對映關係,protocol
表示目標元件對外暴露的方法,impClass
表示目標元件的控制程式碼。
BeeHive
內部使用一個可變字典來儲存protocol-impClass
,其中protocol
作為key,impClass
作為value;
在呼叫元件時,呼叫者將目標元件的協議protocol
作為引數傳給BeeHive
,然後BeeHive
返回對應的元件控制程式碼impClass
。
5.1.1. 構建中間層
(構建中間層等同於上節中的前兩個步驟:生成對映關係和儲存對映關係)
在BeeHive
中,中間層由協議protocol
、協議對應的響應類impClass
以及BeeHive
組成。
在使用BeeHive
呼叫元件之前,需要使用BeeHive
構建中間層,一般分為以下兩步:(下列程式碼來自BeeHive
專案中的demo)
- 宣告元件協議
定義一個協議
protocol
,在這個協議中宣告元件對外暴露的方法,每一個元件對應一個協議protocol
//建立協議
//TradeServiceProtocol.h
#import "BHServiceProtocol.h"
@protocol TradeServiceProtocol <NSObject, BHServiceProtocol>
@property(nonatomic, strong) NSString *itemId;
@end
複製程式碼
- 註冊對映關係
在元件中指定一個類作為其實現類
impClass
,這個實現類需要遵守這個協議protocol
,然後使用BeeHive
提供的方法將protocol-impClass
這種對映關係註冊到BeeHive
中。 可以在BeeHive
之外的任何地方註冊,只需要在呼叫元件之前註冊就行了。
//註冊protocol-impClass對映關係
#import "BHService.h"
[[BeeHive shareInstance] registerService:@protocol(TradeServiceProtocol) service:[BHTradeViewController class]];
複製程式碼
BeeHive
本身並不會生成對映關係,它只是提供註冊方法給呼叫者使用,真正生成對映關係的是BeeHive
的呼叫者,BeeHive
本身沒有依賴具體的元件。
BeeHive
內部使用一個可變字典來儲存protocol-impClass
對映關係,它並不關心protocol
和impClass
是否和元件有關,它唯一的要求是protocol
和impClass
必須有值,且impClass
必須遵循協議protocol
。
也就是說,在生成對映關係和儲存對映關係這兩個步驟中,BeeHive
只負責後者,然後BeeHive
提供生成前者的介面,具體生成前者的操作不是由BeeHive
執行。
5.1.2. 呼叫元件
(呼叫元件等同於上一節中的後兩個步驟:獲取元件控制程式碼和使用元件控制程式碼)
在呼叫元件時,呼叫者將目標元件的協議protocol
作為引數傳給BeeHive
,根據上述註冊的對映關係protocol-impClass
,獲取協議protocol
對應的實現類impClass
,也就是說呼叫者需要依賴這個協議protocol
,然後呼叫者就可以使用這個實現類來訪問目標元件了。
//BHViewController.m
#import "BHService.h"
...
id<TradeServiceProtocol> v2 = [[BeeHive shareInstance]createService:@protocol(TradeServiceProtocol)];
if ([v2 isKindOfClass:[UIViewController class]]) {
v2.itemId = @"sdfsdfsfasf";
}
...
複製程式碼
當呼叫者使用BeeHive
呼叫元件時,BeeHive
根據協議protocol
獲取對應的實現類impClass
,BeeHive
只是將這個實現類impClass
當做一個普通的Class
型別,然後返回這個實現類給呼叫者。這裡的實現類impClass
就是元件控制程式碼,所以在獲取元件控制程式碼的時候,BeeHive
和業務是沒有依賴的。
至於如何使用實現類impClass
,那是呼叫者負責的,BeeHive
並不關心。
也就是說,在獲取元件控制程式碼和使用元件控制程式碼這兩個步驟中,BeeHive
只負責前者,而後者是由元件的呼叫者執行的。
根據以上分析,BeeHive
完全負責中介軟體通用工具的標準。
5.2. CTMediator
CTMediator
內部是使用下列runtime
方法實現的
- (id)performSelector:(SEL)aSelector withObject:(id)object;
複製程式碼
在呼叫目標元件時,呼叫者將元件響應類的類名和方法名作為引數傳給CTMediator
,CTMediator
通過上述方法呼叫目標元件。
在CTMediator
中,由於只需要響應類的類名和方法名就能呼叫元件,所以這裡將響應類的類名和方法名當做元件控制程式碼。
在CTMediator
中,元件的響應類被稱作target-action
。
5.2.1. 建立中間層
在CTMediator
中,中間層是由target-action
、CTMediator
和其分類共同組成的。
target-action
代表元件對外的介面,CTMediator
的分類是面向呼叫者的介面,CTMediator
則負責將這二者關聯起來。
- 建立
target-action
針對每個元件建立一個target類,其內部定義了元件對外暴露的action(方法)。和元件通訊時,其實質是呼叫一個特定的target-action
的方法。target
類的類名必須以Target_
開頭,比如Target_A
,action
的方法名必須以Action_
開頭,比如Action_nativeFetchDetailViewController
。
建立一個target-action
(下列程式碼來自CTMediator
專案中的demo)
//Target_A.h
@interface Target_A : NSObject
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;
@end
複製程式碼
//Target_A.m
#import "Target_A.h"
#import "DemoModuleADetailViewController.h"
@implementation Target_A
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因為action是從屬於ModuleA的,所以action直接可以使用ModuleA裡的所有宣告
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
複製程式碼
- 建立
CTMediator
的分類CTMediator
分類是面向元件呼叫者的,每一個元件都有一個對應的CTMediator
分類,呼叫者使用這個分類的介面來和元件通訊。CTMediator
分類中每一個方法內部都會呼叫一個或多個target-action
的方法,呼叫者使用分類方法來呼叫元件時,其最終目的是呼叫特定的target-action
的方法。
建立一個CTMediator
的分類(下列程式碼來自CTMediator
專案中的demo)
//CTMediator+CTMediatorModuleAActions.h
#import "CTMediator.h"
@interface CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail;
@end
複製程式碼
//CTMediator+CTMediatorModuleAActions.m
#import "CTMediator+CTMediatorModuleAActions.h"
NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之後,可以由外界選擇是push還是present
return viewController;
} else {
// 這裡處理異常場景,具體如何處理取決於產品
return [[UIViewController alloc] init];
}
}
@end
複製程式碼
CTMediator的分類
中的每一個方法都會呼叫特定的target-action
的方法,這種呼叫關係被寫死在程式碼中,屬於硬編碼,它表示中間層識別符號-元件控制程式碼
的對映關係。
定義CTMediator的分類
的方法的過程可以看做是生成這種對映關係的過程。
CTMediator的分類
是由元件作者建立的,CTMediator
不會對它產生依賴。
也就是說,生成對映關係和儲存對映關係這兩個步驟都是由CTMediator分類
負責的。
5.2.2. 呼叫元件
呼叫元件時,需要引用之前定義的分類,然後去這個分類的標頭檔案中找到想要執行的方法,最後執行這個方法。
呼叫者只需要依賴CTMediator
的分類,就可以完成元件間通訊了。
//ViewController.m
#import "CTMediator+CTMediatorModuleAActions.h"
...
UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
[self presentViewController:viewController animated:YES completion:nil];
...
複製程式碼
在呼叫元件時,呼叫者只需呼叫對應CTMediator的分類
的方法,然後CTMediator的分類
根據對映關係獲取元件控制程式碼,也就是target和action的字串名稱,再將元件控制程式碼傳給CTMediator
;
CTMediator
接受到元件控制程式碼後,執行對應的target-action
的方法。
從這個角度來說,CTMediator
實現了獲取元件控制程式碼和使用元件控制程式碼這兩個步驟。
5.3. 其他
5.3.1. CTMediator分類的作用
下列程式碼沒有使用分類,其效果和上面使用分類的程式碼等同
UIViewController *viewController = [[CTMediator sharedInstance]performTarget:@"A" action:@"nativeFetchDetailViewController" params:@{@"key":@"value"} shouldCacheTarget:NO];
if (![viewController isKindOfClass:[UIViewController class]]) {
viewController = [[UIViewController alloc] init];
}
[self presentViewController:viewController animated:YES completion:nil];
複製程式碼
可以看出,上述呼叫程式碼比較繁瑣,呼叫者需要記住target和action的字串名稱,然後手動輸入,這對於呼叫者來說是不太友好的;傳入的引數是一個字典,呼叫者無法直觀的知道方法所需的具體引數,而且呼叫元件的邏輯會分散在專案各處,可讀性很差。使用CTMediator的分類
可以統一呼叫入口,並提供可讀性強的介面。
5.3.2. BeeHive的protocol
和CTMediator的category
的異同
-
相同
BeeHive
中的protocol
和CTMediator
中的category
有一些相似之處,它們都包含了中間層對外的介面,而且它們和元件的關係也是一對一的,從這一點上來看,它們在功能上是一致的。 -
不同 如果沒有
protocol
,則中間層無法生成識別符號-元件控制程式碼
的對映關係,呼叫者在不(編譯層)依賴元件控制程式碼的情況下,不可以拿到元件控制程式碼。對於BeeHive
來說,protocol
是不可或缺的; 如果沒有category
,呼叫者在不(編譯層)依賴元件控制程式碼的情況下,可以拿到元件控制程式碼,因為元件控制程式碼只是兩個字串。對於CTMediator
來說,category
不是必須的。