[iOS] 模塊化 & 組件化

感覺我去年11月的時候還不知道啥是組件化和模塊化,今年這個時候就可以寫這個topic了也是神奇0.0

首先說下在我看來這兩者的區(qū)別吧:

  • 模塊化:

如果工程很大,公司幾百上千的人開發(fā)同一個app,很容易就會有沖突,那么就需要劃分業(yè)務(wù)線,大家規(guī)定好對外暴露的接口,盡量只改動自己業(yè)務(wù)線的內(nèi)容,例如直播就是一個業(yè)務(wù)線,也可以作為一個模塊。

所以模塊化更多感覺是根據(jù)業(yè)務(wù)線劃分,每個業(yè)務(wù)線會有自己獨立的一個或者多個git,這樣的話其實主工程沒有實際的代碼只有一些config文件,只要引入各個業(yè)務(wù)線的git并做初始化即可。這樣其實QA的壓力也會小一點,畢竟業(yè)務(wù)線之間的耦合會更可控,業(yè)務(wù)線可以有自己的QA測試。

  • 組件化:

組件化一般說的是模塊也是業(yè)務(wù)線自己內(nèi)部的事兒了,也是為了提高復(fù)用、可插拔、減少Q(mào)A測試壓力等。

我經(jīng)歷過的兩家公司的部門都是做拍攝和錄制相關(guān)的,例如直播,從頁面上來看,送禮就是一個小組件。

組件化的目的也很純粹,比如在app X上你做了一個直播,然后你們又搭建了一個app B,也想要直播里面的購物車,但不要別的東西要怎么辦?app B不可能為了要引入一個購物車模塊把你app X的整個直播間代碼引入叭。

當(dāng)如模塊化其實也是有這個考慮,如果新起一個app只想要直播這個模塊,會很容易引用。如果大家代碼都混在一起,直播里面直接引入了其他模塊的.h文件,那么你無法做到只把直播的代碼拷過去,就要引入整個舊app?這肯定是不行的。

其實組件化和模塊化都是為了解耦,讓底層不依賴上層,以及同層之間盡量不依賴,依賴也不要直接引入對方的.h文件。


模塊化

首先強推:https://blog.csdn.net/u013378438/article/details/85702346

模塊化前

模塊化的過程比較痛苦(實際上組件化也是),模塊化其實就是把業(yè)務(wù)A和業(yè)務(wù)B之間的互相依賴,轉(zhuǎn)化為大家都依賴中間層:

模塊化后

當(dāng)改成醬紫以后,如果其他app需要復(fù)用某個模塊,只要把中間層以及想要用的模塊的代碼搞走就好啦,非常的方便。

所以要如何做模塊化呢?流程大概是醬紫的:

  1. 首先需要把每個業(yè)務(wù)拆分出單獨的pod
  2. 然后梳理相互之間的依賴(分下類比如view、vc、AB test、storage)
  3. 讓模塊間的依賴impl替換為依賴接口
  4. 實現(xiàn)中間層,并替換所有依賴為中間層代碼

其實感覺過程不是很復(fù)雜對不對,可能在設(shè)計的part,看起來比較復(fù)雜的是中間層如何設(shè)計才能解決以下問題:

  • Mediator如何調(diào)度不同的模塊?
  • 不同模塊僅和Mediator通信,那不同模塊又如何知道其他模塊能夠提供的接口(服務(wù))?
  • 模塊知道Mediator就可以work。但是對于Mediator來說,豈不是要知道所有的模塊?如何避免讓Mediator成為一個巨無霸?

關(guān)于中間層的設(shè)計,業(yè)界給出了兩種方式:

  1. 運用OC特有的語言機制,就是runtime反射調(diào)用。
  2. 設(shè)計注冊機制,每一個模塊主動向Mediator注冊自己,在Mediator中統(tǒng)一通過抽象的Class類型來管理這些模塊。用戶通過模塊對應(yīng)的key向Mediator索要對應(yīng)的模塊,然后在Mediator外部自行調(diào)用模塊的功能。

現(xiàn)在假設(shè)一個場景,有兩個模塊分別為A和B,A在某個場景下需要使用定義在模塊B內(nèi)的氣泡,我們先看下最簡單的方式

implement 1
實現(xiàn)1

因為比較簡單,B的代碼就不截圖啦,它就是有一個createBubbleView的方法。這個時候moduleA是直接#import "ModuleBImpl.h",這種情況下如果你想把A獨立出去,就要連著B一起給出去。而且這樣A、B模塊間可以隨意訪問調(diào)用,如果你負(fù)責(zé)B模塊你都無法預(yù)知對方會調(diào)用你的哪些方法。

implement 2 - 中間層初實現(xiàn)
實現(xiàn)2

如果我們新建一個Mediator,讓它引入各個模塊,并提供方法,這樣A模塊只要引入這個文件即可,至少不需要直接引入模塊B了,也可以限制A可以調(diào)用的方法。

#import "ModuleAImpl.h"
#import "Mediator.h"
@import UIKit;

@implementation ModuleAImpl

- (void)showBubble {
    UIView *bubble = [Mediator createBModuleBubbleView];
    // add到view上面
}

@end

這么做仍舊有一點問題,Mediator直接依賴了B的實現(xiàn),那么如果想獨立出A,就要把Mediator帶上,但是如果不帶B的實現(xiàn)的話就會編譯不過。

也就是說,Mediator 必須知道每一個模塊的存在,這就在模塊和Mediator間產(chǎn)生了強耦合。這樣做會有很多缺點:

  • Mediator必須知道每一個模塊以及模塊所能夠提供的所有接口,會使得Mediator變得十分臃腫甚至難以維護。(試想一下,如果你負(fù)責(zé)維護Mediator,你需要知道每個業(yè)務(wù)部門的業(yè)務(wù)接口邏輯)
  • 由于Mediator與模塊的強耦合性,導(dǎo)致每當(dāng)模塊添加或修改接口,都需要Mediator跟著變動,而Mediator又是所有模塊都會引用到的一個中介,這么一個三天兩頭就會變化的Mediator很難搞。

所以這里做一點修改,讓Mediator依賴B的接口,但是如果依賴接口,Mediator要如何生成一個B呢?

implement 3 - 注冊實現(xiàn)中間層

我們可以讓各個模塊自己向Mediator里面注冊自己,這樣Mediator就可以拿到他們啦:

#import <Foundation/Foundation.h>
@import UIKit;

NS_ASSUME_NONNULL_BEGIN

@interface Mediator : NSObject

+ (Mediator *)sharedInstance;

- (void)registerService:(Protocol *)proto forService:(Class)serviceClass;

- (Class)fetchService:(Protocol *)proto;


@end

NS_ASSUME_NONNULL_END

===============================

#import "Mediator.h"

@interface Mediator()

@property(nonatomic, strong) NSMutableDictionary *serviceDict;

@end

@implementation Mediator

+ (Mediator *)sharedInstance {
    static Mediator *mediator;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[Mediator alloc] init];
    });
    return mediator;
}

- (void)registerService:(Protocol *)proto forService:(Class)serviceClass {
    [self.serviceDict setObject:serviceClass forKey:NSStringFromProtocol(proto)];
}

- (Class)fetchService:(Protocol *)proto {
    return self.serviceDict[NSStringFromProtocol(proto)];
}

@end

然后模塊B需要在初始化的時候注冊自己,為了方便就在load的時候做啦,但是實際上最好還是init的時候去做:

#import "ModuleBImpl.h"
#import "Mediator.h"

@implementation ModuleBImpl

+ (void)load {
    [[Mediator sharedInstance] registerService:@protocol(ModuleBInterface) forService:[self class]];
}

+ (UIView *)createBubbleView {
    return [[UIView alloc] init];
}

@end

這里的ModuleBInterface就是模塊B對外提供的接口功能:

#ifndef ModuleBInterface_h
#define ModuleBInterface_h

@class UIView;

@protocol ModuleBInterface <NSObject>

+ (UIView *)createBubbleView;

@end


#endif /* ModuleBInterface_h */

然后A就可以通過Mediator拿到B的抽象啦~


A使用B

現(xiàn)在如果某個app需要引用A模塊,就只需要把Mediator和A的目錄下的文件拿走就可以啦,只是注意從Mediator拿東西的時候,要有意識可能是空,當(dāng)然你也可以自己寫一個新的B的impl注冊到中間層里面~

我們之前項目還做了一個放在自己模塊里面的configuration類,類似讓你自己返回protocol也就是key對應(yīng)的class~ 但其實本質(zhì)還是沒有變的,只是可配置性更強一點:

@implementation xxxConfiguration

+ (NSString *)name {
    return @"xxx";
}

+ (nullable NSArray<Class> *)moduleServiceClasses {
    return @[
        [xxx class],
    ];
}

+ (nullable NSArray<Protocol *> *)moduleInterfaceProtocols {
    return @[
        @protocol(xxx),
    ];
}

然后你可以AppDelegate里面在app初始化的時候,調(diào)用這個config文件來進行注冊配置~ 這樣的話,只要你引入了這個模塊,就可以注冊這個模塊內(nèi)的接口,然后其他模塊就可以用了。

implement 4 - runtime實現(xiàn)中間層

首先先看下不通過中間層,僅僅通過反射如何做:

#import "ModuleAImpl.h"
@import UIKit;

@implementation ModuleAImpl

- (void)showBubble {
    Class cls = NSClassFromString(@"ModuleBImpl");
    UIView *bubble = [cls performSelector:@selector(createBubbleView) withObject:nil];
}

@end

講真模塊之間要是這么使用,感覺會被打死。。其他模塊要是改個類名,那我們妥妥跪了,所以這肯定是不行的,那么如果放進中間層呢?

有一個現(xiàn)成的庫可以幫我們做到中間層:CTMediator

這個庫的使用方法是,你可以給CTMediator加category作為對外的方法,這樣其他模塊使用的時候就會直接可以調(diào)用你的方法啦。然后CTMediator要如何把方法轉(zhuǎn)發(fā)給你呢?

你在category里面只要寫[self performTarget:目標(biāo)類名 action:目標(biāo)方法名 params:@{@"key":@"value"} shouldCacheTarget:NO];,CTMediator會去找Target_目標(biāo)名的class,然后找這個類的Action_動作名的方法來調(diào)用:

CTMediator

可以看一下performTarget的代碼是醬紫的:

{
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        Class targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }

    // generate action
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 這里是處理無響應(yīng)請求的地方之一,這個demo做得比較簡單,如果沒有可以響應(yīng)的target,就直接return了。實際開發(fā)過程中是可以事先給一個固定的target專門用于在這個時候頂上,然后處理這種請求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 這里是處理無響應(yīng)請求的地方,如果無響應(yīng),則嘗試調(diào)用對應(yīng)target的notFound方法統(tǒng)一處理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 這里也是處理無響應(yīng)請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發(fā)過程中,可以用前面提到的固定的target頂上的。
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            [self.cachedTarget removeObjectForKey:targetClassString];
            return nil;
        }
    }
}

這里通過這個庫改寫一下上面的例子:


A使用B的樣子

CTMediator的分類可以放在底層:

#import <CTMediator/CTMediator.h>
@import UIKit;

NS_ASSUME_NONNULL_BEGIN

@interface CTMediator (moduleb)

- (UIView *)createModuleBBubble;

@end

NS_ASSUME_NONNULL_END

==============================

#import "CTMediator+moduleb.h"

@implementation CTMediator (moduleb)

- (UIView *)createModuleBBubble {
    UIView *bubble = [self performTarget:@"B" action:@"createBubbleView" params:@{@"key" : @"value"} shouldCacheTarget:NO];
    return bubble;
}

@end

新建一個target B的文件,為了讓CTMediator在performTarget的時候可以找到它:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Target_B : NSObject

@end

NS_ASSUME_NONNULL_END
================================

#import "Target_B.h"
#import "ModuleBImpl.h"
@import UIKit;

@implementation Target_B

- (UIView *)Action_createBubbleView {
    return [ModuleBImpl createBubbleView];
}

@end

這個文件可以放在B模塊里面,功能其實類似替代了接口文件,所以這么做其實B就不用再實現(xiàn)接口的protocol,也就沒有定義相關(guān)的interface protocol了,完全通過target文件來控制有哪些action。


Summary for 模塊化

中間層的實現(xiàn)分兩種,注冊(可以參考阿里的BeeHive)以及反射(可以參考CTMediator)。

反射其實是你需要遵守它的規(guī)則,包括類名和方法名,這樣它通過runtime找到你的對應(yīng)關(guān)系;而注冊就是自己需要控制注冊時機之類的,但是更清晰,更透明。你不需要了解CTMediator的命名要求就可以做到。

其實兩者區(qū)別不大,都是解決了如何通過抽象找到一個類的實現(xiàn),如果你的App整體是由 DI 做的,還可以通過依賴注入來解耦(但其實很多對外的都是類方法,只要拿到class就可以了,在DI的場景可能不是很有用,DI適用于拿對象)。模塊化的重點其實還是如何劃分模塊以及具體拆分的時候怎么一步一步的拆開,具體中間層的實現(xiàn)有很多方式的。


組件化

強推一篇:https://juejin.im/post/6844903873023311886#heading-1

可插拔、復(fù)用、減少Q(mào)A測試,不要改動一點要把所有核心功能測一遍(業(yè)務(wù)的價值點)

在業(yè)務(wù)內(nèi),就像開篇舉的直播例子,頁面里面會有很多獨立的單元,當(dāng)然除了直播也會有這樣的,比如抖音快手的拍視頻。所以如果其他app要引用我們的直播模塊,但是它只希望頁面里有一個送禮組件,如果為了實現(xiàn)這個它需要引入所有和送禮組件耦合的組件,是非常不合理and頭禿的。

所以其實組件化是為了實現(xiàn)我們業(yè)務(wù)線內(nèi),更小力度的組件可配置,可插拔。也就是你可以選擇,你要放入哪些組件,并可以不引入其他不要的組件的文件。

類似模塊化,如果你想實現(xiàn)只要組件A,那么即使組件A使用了組件B,也是不可以直接在A里面import B的頭文件噠。比較重要的是,組件化的實現(xiàn)仍舊是依賴接口interface而非impl。

1)組件原則
組件通常分為兩種類型的組件:基礎(chǔ)組件,業(yè)務(wù)組件。

  • 業(yè)務(wù)組件依賴基礎(chǔ)組件
  • 基礎(chǔ)組件不可依賴業(yè)務(wù)組件。
  • 業(yè)務(wù)組件間不可相互依賴。

2)組件間的通信方式:
組件間通信方式,業(yè)內(nèi)主要有兩種實現(xiàn)方式:
1. 協(xié)議式框架,比如蘑菇街的這種方案。蘑菇街 App 的組件化之路
2. 中間者架構(gòu),比如casatwy的方案。casatwy關(guān)于蘑菇街組件化的討論

這里說一種實現(xiàn)叭,在直播的時候,所有組件都會對應(yīng)一個protocol,也就是這個組件對外提供的能力,然后組件們會被注入到DI,組件間的互相調(diào)用,是通過從DI拿實現(xiàn)了某個protocol的對象實現(xiàn)的。也就是這些protocol和DI是作為中間層common的部分,如果你只想引入組件A,可以只引入A和common part。

當(dāng)然還有很多種實現(xiàn),但重要的是,都不要直接引入別的組件的內(nèi)容。

我們目前用的是基于分層結(jié)構(gòu)的組件化,會抽出基礎(chǔ)組件放在底層,而不是每個組件都提供一個proto,主要是為了:

  • 有些組件其實并沒有必要一定要有一個proto,就會比較冗余。通過底層service來實現(xiàn)組件化,可能幾個組件對應(yīng)一個service
  • 如果每個組件都對外提供一個proto,并且將自己注冊進入DI也可以實現(xiàn)解耦,但是這樣的話,還是會不可避免的一個init會帶起一堆組件的init
  • 另外,有些組件頻繁的被依賴,其實是因為它提供了某個很基礎(chǔ)的功能,這種功能可能每個組件都需要,所以可以抽出來作為基礎(chǔ)的service,放在最底層對外暴露,這樣避免了頻繁的互相依賴,以及可以更完善對外暴露的功能。

比較基礎(chǔ)的一些組件,如果要是以之前DI的方式,不抽出成為基礎(chǔ)service放在底層庫,就會出現(xiàn)比如music模塊提供了一個很基礎(chǔ)的方法每個組件都有用到,那么每個都要持有一個musiccomponent,如果其他app引用我們的庫,他們?nèi)绻挥眠@個music組件,必須自己實現(xiàn)一個實現(xiàn)了這個protocol的對象,就但實際上他們只是用它的一個方法,這就很不合理以及麻煩,別的應(yīng)用引入我們庫的時候也很痛苦


Reference:

http://www.itdecent.cn/p/921ee8916569
https://www.cnblogs.com/wujy/p/5919105.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容