App組件化

原文鏈接: http://casatwy.com/iOS-Modulization.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io 作者是大神啊

iOS應(yīng)用架構(gòu)談 開篇
iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案
iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案
iOS應(yīng)用架構(gòu)談 本地持久化方案及動(dòng)態(tài)部署
iOS應(yīng)用架構(gòu)談 組件化方案

簡述

前幾天的一個(gè)晚上在infoQ的微信群里,來自蘑菇街的Limboy做了一個(gè)分享,講了蘑菇街的組件化之路。我不認(rèn)為這條組件化之路蘑菇街走對了。分享后我私聊了Limboy,Limboy似乎也明白了問題所在,我答應(yīng)他我會(huì)把我的方案寫成文章,于是這篇文章就出來了。

另外,按道理說組件化方案也屬于iOS應(yīng)用架構(gòu)談的一部分,但是當(dāng)初構(gòu)思架構(gòu)談時(shí),我沒打算寫組件化方案,因?yàn)槲彝诉€有這回事兒。。。后來寫到view的時(shí)候才想起來,所以在view的那篇文章最后補(bǔ)了一點(diǎn)內(nèi)容。而且覺得這個(gè)組件化方案太簡單,包括實(shí)現(xiàn)組件化方案的組件也很簡單,代碼算上注釋也才100行,我就偷懶放過了,畢竟寫一篇文章好累的啊。

本文的組件化方案demo在這里https://github.com/casatwy/CTMediator 拉下來后記得pod install 拉下來后記得pod install 拉下來后記得pod install
,這個(gè)Demo對業(yè)務(wù)敏感的邊界情況處理比較簡單,這需要根據(jù)不同App的特性和不同產(chǎn)品的需求才能做,所以只是為了說明組件化架構(gòu)用的。如果要應(yīng)用在實(shí)際場景中的話,可以根據(jù)代碼里給出的注釋稍加修改,就能用了。

蘑菇街的原文地址在這里:《蘑菇街 App 的組件化之路》,沒有耐心看完原文的朋友,我在這里簡要介紹一下蘑菇街的組件化是怎么做的:

App啟動(dòng)時(shí)實(shí)例化各組件模塊,然后這些組件向ModuleManager注冊Url,有些時(shí)候不需要實(shí)例化,使用class注冊。
當(dāng)組件A需要調(diào)用組件B時(shí),向ModuleManager傳遞URL,參數(shù)跟隨URL以GET方式傳遞,類似openURL。然后由ModuleManager負(fù)責(zé)調(diào)度組件B,最后完成任務(wù)。

這里的兩步中,每一步都存在問題。

第一步的問題在于,在組件化的過程中,注冊URL并不是充分必要條件,組件是不需要向組件管理器注冊Url的。而且注冊了Url之后,會(huì)造成不必要的內(nèi)存常駐,如果只是注冊Class,內(nèi)存常駐量就小一點(diǎn),如果是注冊實(shí)例,內(nèi)存常駐量就大了。至于蘑菇街注冊的是Class還是實(shí)例,Limboy分享時(shí)沒有說,文章里我也沒看出來,也有可能是我看漏了。不過這還并不能算是致命錯(cuò)誤,只能算是小缺陷。

真正的致命錯(cuò)誤在第二步。在iOS領(lǐng)域里,一定是組件化的中間件為openUrl提供服務(wù),而不是openUrl方式為組件化提供服務(wù)。

什么意思呢?

也就是說,一個(gè)App的組件化方案一定不是建立在URL上的,openURL的跨App調(diào)用是可以建立在組件化方案上的。當(dāng)然,如果App還沒有組件化,openURL方式也是可以建立的,就是丑陋一點(diǎn)而已。

為什么這么說?

因?yàn)榻M件化方案的實(shí)施過程中,需要處理的問題的復(fù)雜度,以及拆解、調(diào)度業(yè)務(wù)的過程的復(fù)雜度比較大,單純以openURL的方式是無法勝任讓一個(gè)App去實(shí)施組件化架構(gòu)的。如果在給App實(shí)施組件化方案的過程中是基于openURL的方案的話,有一個(gè)致命缺陷:非常規(guī)對象無法參與本地組件間調(diào)度。關(guān)于非常規(guī)對象
我會(huì)在詳細(xì)講解組件化方案時(shí)有一個(gè)辨析。

實(shí)際App場景下,如果本地組件間采用GET方式的URL調(diào)用,就會(huì)產(chǎn)生兩個(gè)問題:

根本無法表達(dá)非常規(guī)對象

比如你要調(diào)用一個(gè)圖片編輯模塊,不能傳遞UIImage到對應(yīng)的模塊上去的話,這是一個(gè)很悲催的事情。 當(dāng)然,這可以通過給方法新開一個(gè)參數(shù),然后傳遞過去來解決。比如原來是:

[a openUrl:"http://casa.com/detail?id=123&type=0"];

同時(shí)就也要提供這樣的方法:

[a openUrl:"http://casa.com/detail" params:@{ @"id":"123", @"type":"0", @"image":[UIImage imageNamed:@"test"]}]

如果不像上面這么做,復(fù)雜參數(shù)和非常規(guī)參數(shù)就無法傳遞。如果這么做了,那么事實(shí)上這就是拆分遠(yuǎn)程調(diào)用和本地調(diào)用的入口了,這就變成了我文章中提倡的做法,也是蘑菇街方案沒有做到的地方。

另外,在本地調(diào)用中使用URL的方式其實(shí)是不必要的,如果業(yè)務(wù)工程師在本地間調(diào)度時(shí)需要給出URL,那么就不可避免要提供params,在調(diào)用時(shí)要提供哪些params是業(yè)務(wù)工程師很容易懵逼的地方。。。在文章下半部分給出的demo代碼樣例已經(jīng)說明了業(yè)務(wù)工程師在本地間調(diào)用時(shí),是不需要知道URL的,而且demo代碼樣例也闡釋了如何解決業(yè)務(wù)工程師遇到傳params容易懵逼的問題。

URL注冊對于實(shí)施組件化方案是完全不必要的,且通過URL注冊的方式形成的組件化方案,拓展性和可維護(hù)性都會(huì)被打折

注冊URL的目的其實(shí)是一個(gè)服務(wù)發(fā)現(xiàn)的過程,在iOS領(lǐng)域中,服務(wù)發(fā)現(xiàn)的方式是不需要通過主動(dòng)注冊的,使用runtime就可以了。另外,注冊部分的代碼的維護(hù)是一個(gè)相對麻煩的事情,每一次支持新調(diào)用時(shí),都要去維護(hù)一次注冊列表。如果有調(diào)用被棄用了,是經(jīng)常會(huì)忘記刪項(xiàng)目的。runtime由于不存在注冊過程,那就也不會(huì)產(chǎn)生維護(hù)的操作,維護(hù)成本就降低了。
由于通過runtime做到了服務(wù)的自動(dòng)發(fā)現(xiàn),拓展調(diào)用接口的任務(wù)就僅在于各自的模塊,任何一次新接口添加,新業(yè)務(wù)添加,都不必去主工程做操作,十分透明。

小總結(jié)

蘑菇街采用了openURL的方式來進(jìn)行App的組件化是一個(gè)錯(cuò)誤的做法,使用注冊的方式發(fā)現(xiàn)服務(wù)是一個(gè)不必要的做法。而且這方案還有其它問題,隨著下文對組件化方案介紹的展開,相信各位自然心里有數(shù)。

正確的組件化方案

先來看一下方案的架構(gòu)圖

這幅圖是組件化方案的一個(gè)簡化版架構(gòu)描述,主要是基于Mediator模式和Target-Action模式,中間采用了runtime來完成調(diào)用。這套組件化方案將遠(yuǎn)程應(yīng)用調(diào)用和本地應(yīng)用調(diào)用做了拆分,而且是由本地應(yīng)用調(diào)用為遠(yuǎn)程應(yīng)用調(diào)用提供服務(wù),與蘑菇街方案正好相反。

調(diào)用方式

先說本地應(yīng)用調(diào)用,本地組件A在某處調(diào)用

[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]

向CTMediator發(fā)起跨組件調(diào)用,CTMediator根據(jù)獲得的target和action信息,通過objective-C的runtime轉(zhuǎn)化生成target實(shí)例以及對應(yīng)的action選擇子,然后最終調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯,完成需求。

在遠(yuǎn)程應(yīng)用調(diào)用中,遠(yuǎn)程應(yīng)用通過openURL的方式,由iOS系統(tǒng)根據(jù)info.plist里的scheme配置找到可以響應(yīng)URL的應(yīng)用(在當(dāng)前我們討論的上下文中,這就是你自己的應(yīng)用),應(yīng)用通過AppDelegate接收到URL之后,調(diào)用CTMediator的openUrl:方法將接收到的URL信息傳入。

當(dāng)然,CTMediator也可以用openUrl:options:的方式順便把隨之而來的option也接收,這取決于你本地業(yè)務(wù)執(zhí)行邏輯時(shí)的充要條件是否包含option數(shù)據(jù)。傳入U(xiǎn)RL之后,CTMediator通過解析URL,將請求路由到對應(yīng)的target和action,隨后的過程就變成了上面說過的本地應(yīng)用調(diào)用的過程了,最終完成響應(yīng)。

針對請求的路由操作很少會(huì)采用本地文件記錄路由表的方式,服務(wù)端經(jīng)常處理這種業(yè)務(wù),在服務(wù)端領(lǐng)域基本上都是通過正則表達(dá)式來做路由解析。App中做路由解析可以做得簡單點(diǎn),制定URL規(guī)范就也能完成,最簡單的方式就是scheme://target/action
這種,簡單做個(gè)字符串處理就能把target和action信息從URL中提取出來了。

組件僅通過Action暴露可調(diào)用接口

所有組件都通過組件自帶的Target-Action來響應(yīng),也就是說,模塊與模塊之間的接口被固化在了Target-Action這一層,避免了實(shí)施組件化的改造過程中,對Business的侵入,同時(shí)也提高了組件化接口的可維護(hù)性。

Paste_Image.png

大家可以看到,虛線圈起來的地方就是用于跨組件調(diào)用的target和action,這種方式避免了由BusinessA直接提供組件間調(diào)用會(huì)增加的復(fù)雜度,而且任何組件如果想要對外提供調(diào)用服務(wù),直接掛上target和action就可以了,業(yè)務(wù)本身在大多數(shù)場景下去進(jìn)行組件化改造時(shí),是基本不用動(dòng)的。

復(fù)雜參數(shù)和非常規(guī)參數(shù),以及組件化相關(guān)設(shè)計(jì)思路

這里我們需要針對術(shù)語做一個(gè)理解上的統(tǒng)一:
復(fù)雜參數(shù)
是指由普通類型
的數(shù)據(jù)組成的多層級參數(shù)。在本文中,我們定義只要是能夠被json解析的類型就都是普通類型
,包括NSNumber, NSString, NSArray, NSDictionary,以及相關(guān)衍生類型,比如來自系統(tǒng)的NSMutableArray或者你自己定義的都算。
總結(jié)一下就是:在本文討論的場景中,復(fù)雜參數(shù)的定義是由普通類型組成的具有復(fù)雜結(jié)構(gòu)的參數(shù)。普通類型的定義就是指能夠被json解析的類型。
非常規(guī)參數(shù)
是指由普通類型
以外的類型組成的參數(shù),例如UIImage等這些不能夠被json解析的類型。然后這些類型組成的參數(shù)在文中就被定義為非常規(guī)參數(shù)

總結(jié)一下就是:非常規(guī)參數(shù)
是包含非常規(guī)類型的參數(shù)。非常規(guī)類型
的定義就是不能被json解析的類型都叫非常規(guī)類型。

邊界情況:

假設(shè)多層級參數(shù)中有存在任何一個(gè)內(nèi)容是非常規(guī)參數(shù),本文中這種參數(shù)就也被認(rèn)為是非常規(guī)參數(shù)。

如果某個(gè)類型當(dāng)前不能夠被json解析,但通過某種轉(zhuǎn)化方式能夠轉(zhuǎn)化成json,那么這種類型在場景上下文中,我們也稱為普通類型。

舉個(gè)例子就是通過json描述的自定義view。如果這個(gè)view能夠通過某個(gè)組件被轉(zhuǎn)化成json,那么即使這個(gè)view本身并不是普通類型,在具有轉(zhuǎn)化器的上下文場景中,我們依舊認(rèn)為它是普通類型。

如果上下文場景中沒有轉(zhuǎn)化器,這個(gè)view就是非常規(guī)類型了。

假設(shè)轉(zhuǎn)化出的json不能夠被還原成view,比如組件A有轉(zhuǎn)化器,組件B中沒有轉(zhuǎn)化器,因此在組件間調(diào)用過程中json在B組件里不能被還原成view。在這種調(diào)用方向中,只要調(diào)用者能將非常規(guī)類型轉(zhuǎn)化成json的,我們就依然認(rèn)為這個(gè)view是普通類型。如果調(diào)用者是組件A,轉(zhuǎn)化器在組件B中,A傳遞view參數(shù)時(shí)是沒辦法轉(zhuǎn)化成json的,那么這個(gè)view就被認(rèn)為是非常規(guī)類型,哪怕它在組件B中能夠被轉(zhuǎn)化成json。

然后我來解釋一下為什么應(yīng)該由本地組件間調(diào)用來支持遠(yuǎn)程應(yīng)用調(diào)用:

在遠(yuǎn)程App調(diào)用時(shí),遠(yuǎn)程App是不可能通過URL來提供非常規(guī)參數(shù)的,最多只能以json string的方式經(jīng)過URLEncode之后再通過GET來提供復(fù)雜參數(shù),然后再在本地組件中解析json,最終完成調(diào)用。在組件間調(diào)用時(shí),通過performTarget:action:params:
是能夠提供非常規(guī)參數(shù)的,于是我們可以知道,遠(yuǎn)程App調(diào)用
時(shí)的上下文環(huán)境以及功能是本地組件間調(diào)用
時(shí)上下文環(huán)境以及功能的子集

因此這個(gè)邏輯注定了必須由本地組件間調(diào)用來為遠(yuǎn)程App調(diào)用來提供服務(wù),只有符合這個(gè)邏輯的設(shè)計(jì)思路才是正確的組件化方案的設(shè)計(jì)思路,其他跟這個(gè)不一致的思路一定就是錯(cuò)的。因?yàn)檫壿嬌献蛹癁楦讣峁┓?wù)說不通,所以強(qiáng)行這么做的話,用一個(gè)成語來總結(jié)就叫做倒行逆施。

另外,遠(yuǎn)程App調(diào)用和本地組件間調(diào)用必須要拆分開,遠(yuǎn)程App調(diào)用只能走CTMediator
提供的專用遠(yuǎn)程的方法,本地組件間調(diào)用只能走CTMediator
提供的專用本地的方法,兩者不能通過同一個(gè)接口來調(diào)用。
這里有兩個(gè)原因:

遠(yuǎn)程App調(diào)用處理入?yún)⒌倪^程比本地多了一個(gè)URL解析的過程,這是遠(yuǎn)程App調(diào)用特有的過程。這一點(diǎn)我前面說過,這里我就不細(xì)說了。

架構(gòu)師沒有充要條件條件可以認(rèn)為遠(yuǎn)程App調(diào)用對于無響應(yīng)請求的處理方式和本地組件間調(diào)用無響應(yīng)請求的處理方式在未來產(chǎn)品的演進(jìn)過程中是一致的

在遠(yuǎn)程App調(diào)用中,用戶通過url進(jìn)入app,當(dāng)app無法為這個(gè)url提供服務(wù)時(shí),常見的辦法是展示一個(gè)所謂的404界面,告訴用戶"當(dāng)前沒有相對應(yīng)的內(nèi)容,不過你可以在app里別的地方再逛逛"。這個(gè)場景多見于用戶使用的App版本不一致。比如有一個(gè)URL只有1.1版本的app能完整響應(yīng),1.0版本的app雖然能被喚起,但是無法完成整個(gè)響應(yīng)過程,那么1.0的app就要展示一個(gè)404了。

在組件間調(diào)用中,如果遇到了無法響應(yīng)的請求,就要分兩種場景考慮了。

場景1

如果這種無法響應(yīng)的請求發(fā)生場景是在開發(fā)過程中,比如兩個(gè)組件同時(shí)在開發(fā),組件A調(diào)用組件B時(shí),組件B還處于舊版本沒有發(fā)布新版本,因此響應(yīng)不了,那么這時(shí)候的處理方式可以相對隨意,只要能體現(xiàn)B模塊是舊版本就行了,最后在RC階段統(tǒng)測時(shí)是一定能夠發(fā)現(xiàn)的,只要App沒發(fā)版,怎么處理都來得及。

場景2

如果這種無法響應(yīng)的請求發(fā)生場景是在已發(fā)布的App中,有可能展示個(gè)404就結(jié)束了,那這就跟遠(yuǎn)程App調(diào)用時(shí)的404處理場景一樣。但也有可能需要為此做一些額外的事情,有可能因?yàn)樽隽祟~外的事情,就不展示404了,展示別的頁面了,這一切取決于產(chǎn)品經(jīng)理。

那么這種場景是如何發(fā)生的呢?

我舉一個(gè)例子:當(dāng)用戶在1.0版本時(shí)收藏了一個(gè)東西,然后用戶升級App到1.1版本。1.0版本的收藏項(xiàng)目在本地持久層存入的數(shù)據(jù)有可能是會(huì)跟1.1版本收藏時(shí)存入的數(shù)據(jù)是不一致的。此時(shí)用戶在1.1版本的app中對1.0版本收藏的東西做了一些操作,觸發(fā)了本地組件間調(diào)用,這個(gè)本地間調(diào)用又與收藏項(xiàng)目本身的數(shù)據(jù)相關(guān),那么這時(shí)這個(gè)調(diào)用就是有可能變成無響應(yīng)調(diào)用,此時(shí)的處理方式就不見得跟以前一樣展示個(gè)404頁面就結(jié)束了,因?yàn)橛脩粢呀?jīng)看到了收藏了的東西,結(jié)果你還告訴他找不到,用戶立刻懵逼。。。這時(shí)候的處理方式就會(huì)用很多種,至于產(chǎn)品經(jīng)理會(huì)選擇哪種,你作為架構(gòu)師是沒有辦法預(yù)測的。如果產(chǎn)品經(jīng)理提的需求落實(shí)到架構(gòu)上,對調(diào)用入口產(chǎn)生要求然而你的架構(gòu)又沒有拆分調(diào)用入口,對于你的選擇就只有兩個(gè):要么打回產(chǎn)品需求,要么加個(gè)班去拆分調(diào)用入口。

當(dāng)然,架構(gòu)師可以選擇打回產(chǎn)品經(jīng)理的需求,最終挑選一個(gè)自己的架構(gòu)能夠承載的需求。但是,如果這種是因?yàn)槟阍缙谠O(shè)計(jì)架構(gòu)時(shí)挖的坑而打回的產(chǎn)品需求,你不覺得丟臉么?

鑒于遠(yuǎn)程app調(diào)用和本地組件間調(diào)用下的無響應(yīng)請求處理方式不同,以及未來不可知的產(chǎn)品演進(jìn),拆分遠(yuǎn)程app調(diào)用入口和本地組件間調(diào)用入口是功在當(dāng)代利在千秋的事情。

組件化方案中的去model設(shè)計(jì)

組件間調(diào)用時(shí),是需要針對參數(shù)做去model化的。如果組件間調(diào)用不對參數(shù)做去model化的設(shè)計(jì),就會(huì)導(dǎo)致業(yè)務(wù)形式上被組件化了,實(shí)質(zhì)上依然沒有被獨(dú)立
。

假設(shè)模塊A和模塊B之間采用model化的方案去調(diào)用,那么調(diào)用方法時(shí)傳遞的參數(shù)就會(huì)是一個(gè)對象。

如果對象不是一個(gè)面向接口的通用對象,那么mediator的參數(shù)處理就會(huì)非常復(fù)雜,因?yàn)橐獏^(qū)分不同的對象類型。如果mediator不處理參數(shù),直接將對象以范型的方式轉(zhuǎn)交給模塊B,那么模塊B必然要包含對象類型的聲明。假設(shè)對象聲明放在模塊A,那么B和A之間的組件化只是個(gè)形式主義。如果對象類型聲明放在mediator,那么對于B而言,就不得不依賴mediator。但是,大家可以從上面的架構(gòu)圖中看到,對于響應(yīng)請求的模塊而言,依賴mediator并不是必要條件,因此這種依賴是完全不需要的,這種依賴的存在對于架構(gòu)整體而言,是一種污染。

如果參數(shù)是一個(gè)面向接口的對象,那么mediator對于這種參數(shù)的處理其實(shí)就沒必要了,更多的是直接轉(zhuǎn)給響應(yīng)方的模塊。而且接口的定義就不可能放在發(fā)起方的模塊中了,只能放在mediator中。響應(yīng)方如果要完成響應(yīng),就也必須要依賴mediator,然而前面我已經(jīng)說過,響應(yīng)方對于mediator的依賴是不必要的,因此參數(shù)其實(shí)也并不適合以面向接口的對象的方式去傳遞。

因此,使用對象化的參數(shù)無論是否面向接口,帶來的結(jié)果就是業(yè)務(wù)模塊形式上是被組件化了,但實(shí)質(zhì)上依然沒有被獨(dú)立。

在這種跨模塊場景中,參數(shù)最好還是以去model化的方式去傳遞,在iOS的開發(fā)中,就是以字典的方式去傳遞。這樣就能夠做到只有調(diào)用方依賴mediator,而響應(yīng)方不需要依賴mediator。然而在去model化的實(shí)踐中,由于這種方式自由度太大,我們至少需要保證調(diào)用方生成的參數(shù)能夠被響應(yīng)方理解,然而在組件化場景中,限制去model化方案的自由度的手段,相比于網(wǎng)絡(luò)層和持久層更加容易得多。

因?yàn)榻M件化天然具備了限制手段:參數(shù)不對就無法調(diào)用!無法調(diào)用時(shí)直接debug就能很快找到原因。所以接下來要解決的去model化方案的另一個(gè)問題就是:如何提高開發(fā)效率。

在去model的組件化方案中,影響效率的點(diǎn)有兩個(gè):調(diào)用方如何知道接收方需要哪些key的參數(shù)?調(diào)用方如何知道有哪些target可以被調(diào)用?其實(shí)后面的那個(gè)問題不管是不是去model的方案,都會(huì)遇到。為什么放在一起說,因?yàn)槲医酉聛硪f的解決方案可以把這兩個(gè)問題一起解決。

解決方案就是使用category

mediator這個(gè)repo維護(hù)了若干個(gè)針對mediator的category,每一個(gè)對應(yīng)一個(gè)target,每個(gè)category里的方法對應(yīng)了這個(gè)target下所有可能的調(diào)用場景,這樣調(diào)用者在包含mediator的時(shí)候,自動(dòng)獲得了所有可用的target-action,無論是調(diào)用還是參數(shù)傳遞,都非常方便。接下來我要解釋一下為什么是category而不是其他:

category本身就是一種組合模式,根據(jù)不同的分類提供不同的方法,此時(shí)每一個(gè)組件就是一個(gè)分類,因此把每個(gè)組件可以支持的調(diào)用用category封裝是很合理的。

在category的方法中可以做到參數(shù)的驗(yàn)證,在架構(gòu)中對于保證參數(shù)安全是很有必要的。當(dāng)參數(shù)不對時(shí),category就提供了補(bǔ)救的入口。

category可以很輕松地做請求轉(zhuǎn)發(fā),如果不采用category,請求轉(zhuǎn)發(fā)邏輯就非常難做了。

category統(tǒng)一了所有的組件間調(diào)用入口,因此無論是在調(diào)試還是源碼閱讀上,都為工程師提供了極大的方便。

由于category統(tǒng)一了所有的調(diào)用入口,使得在跨模塊調(diào)用時(shí),對于param的hardcode在整個(gè)App中的作用域僅存在于category中,在這種場景下的hardcode就已經(jīng)變成和調(diào)用宏或者調(diào)用聲明沒有任何區(qū)別了,因此是可以接受的。

這里是業(yè)務(wù)方使用category調(diào)用時(shí)的場景,大家可以看到非常方便,不用去記URL也不用糾結(jié)到底應(yīng)該傳哪些參數(shù)。

if (indexPath.row == 0) { 
    UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail]; 
    // 獲得view controller之后,在這種場景下,到底push還是present,其實(shí)是要由使用者決定的,mediator只要給出view controller的實(shí)例就好了 
    [self presentViewController:viewController animated:YES completion:nil]; 
} 
if (indexPath.row == 1) { 
    UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
    [self.navigationController pushViewController:viewController animated:YES]; 
} if (indexPath.row == 2) { 
    // 這種場景下,很明顯是需要被present的,所以不必返回實(shí)例,mediator直接present了
    [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
 } 
if (indexPath.row == 3) { 
    // 這種場景下,參數(shù)有問題,因此需要在流程中做好處理 
    [[CTMediator sharedInstance] CTMediator_presentImage:nil]; 
} 
if (indexPath.row == 4) { 
    [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
    // 做你想做的事
    NSLog(@"%@", info); 
    }]; 
}

本文對應(yīng)的demo展示了如何使用category來實(shí)現(xiàn)去model的組件調(diào)用。上面的代碼片段也是摘自這個(gè)demo。

基于其他考慮還要再做的一些額外措施

基于安全考慮

我們需要防止黑客通過URL的方式調(diào)用本屬于native的組件,比如支付寶的個(gè)人財(cái)產(chǎn)頁面。如果在調(diào)用層級上沒有區(qū)分好,沒有做好安全措施,黑客就有通過safari查看任何人的個(gè)人財(cái)產(chǎn)的可能。

安全措施其實(shí)有很多,大部分取決于App本身以及產(chǎn)品的要求。在架構(gòu)層面要做的最基礎(chǔ)的一點(diǎn)就是區(qū)分調(diào)用是來自于遠(yuǎn)程App還是本地組件,我在demo中的安全措施是采用給action添加native
前綴去做的,凡是帶有native前綴的就都只允許本地組件調(diào)用,如果在url階段發(fā)現(xiàn)調(diào)用了前綴為native的方法,那就可以采取響應(yīng)措施了。這也是將遠(yuǎn)程app調(diào)用入口和本地組件調(diào)用入口區(qū)分開來的重要原因之一。
當(dāng)然,為了確保安全的做法有很多,但只要拆出遠(yuǎn)程調(diào)用和本地調(diào)用,各種做法就都有施展的空間了。

基于動(dòng)態(tài)調(diào)度考慮

動(dòng)態(tài)調(diào)度的意思就是,今天我可能這個(gè)跳轉(zhuǎn)是要展示A頁面,但是明天可能同樣的跳轉(zhuǎn)就要去展示B頁面了。這個(gè)跳轉(zhuǎn)有可能是來自于本地組件間跳轉(zhuǎn)也有可能是來自于遠(yuǎn)程app。

做這個(gè)事情的切點(diǎn)在本文架構(gòu)中,有很多個(gè):

以url parse為切點(diǎn)
以實(shí)例化target時(shí)為切點(diǎn)
以category調(diào)度方法為切點(diǎn)
以target下的action為切點(diǎn)

如果以url parse為切點(diǎn)
的話,那么這個(gè)動(dòng)態(tài)調(diào)度就只能夠?qū)h(yuǎn)程App跳轉(zhuǎn)產(chǎn)生影響,失去了動(dòng)態(tài)調(diào)度本地跳轉(zhuǎn)的能力,因此是不適合的。

如果以實(shí)例化target時(shí)為切點(diǎn)
的話,就需要在代碼中針對所有target都做一次審查,看是否要被調(diào)度,這是沒必要的。假設(shè)10個(gè)調(diào)用請求中,只有1個(gè)要被動(dòng)態(tài)調(diào)度,那么就必須要審查10次,只有那1次審查通過了,才走動(dòng)態(tài)調(diào)度,這是一種相對比較粗暴的方法。

如果以category調(diào)度方法為切點(diǎn)
的話,那動(dòng)態(tài)調(diào)度就只能影響到本地件組件的跳轉(zhuǎn),因?yàn)閏ategory是只有本地才用的,所以也不適合。

以target下的action為切點(diǎn)
是最適合的,因?yàn)閯?dòng)態(tài)調(diào)度在一般場景下都是有范圍的,大多數(shù)是活動(dòng)頁需要?jiǎng)討B(tài)調(diào)度,今天這個(gè)活動(dòng)明天那個(gè)活動(dòng),或者今天活動(dòng)正在進(jìn)行明天活動(dòng)就結(jié)束了,所以產(chǎn)生動(dòng)態(tài)調(diào)度的需求。我們在可能產(chǎn)生動(dòng)態(tài)調(diào)度的action中審查當(dāng)前action是否需要被動(dòng)態(tài)調(diào)度,在常規(guī)調(diào)度中就沒必要審查了,例如個(gè)人主頁的跳轉(zhuǎn),商品詳情的跳轉(zhuǎn)等,這樣效率就能比較高。

大家會(huì)發(fā)現(xiàn),如果要做類似這種效率更高的動(dòng)態(tài)調(diào)度,target-action層被抽象出來就是必不可少的,然而蘑菇街并沒有抽象出target-action層,這也是其中的一個(gè)問題。

當(dāng)然,如果你的產(chǎn)品要求所有頁面都是存在動(dòng)態(tài)調(diào)度需求的,那就還是以實(shí)例化target時(shí)為切點(diǎn)
去調(diào)度了,這樣能做到審查每一次調(diào)度請求,從而實(shí)現(xiàn)動(dòng)態(tài)調(diào)度。

說完了調(diào)度切點(diǎn),接下來要說的就是如何完成審查流程。完整的審查流程有幾種,我每個(gè)都列舉一下:

App啟動(dòng)時(shí)下載調(diào)度列表,或者定期下載調(diào)度列表。然后審查時(shí)檢查當(dāng)前action是否存在要被動(dòng)態(tài)調(diào)度跳轉(zhuǎn)的action,如果存在,則跳轉(zhuǎn)到另一個(gè)action
每一次到達(dá)新的action時(shí),以action為參數(shù)調(diào)用API獲知是否需要被跳轉(zhuǎn),如果需要被跳轉(zhuǎn),則API告知要跳轉(zhuǎn)的action,然后再跳轉(zhuǎn)到API指定的action

這兩種做法其實(shí)都可以,如果產(chǎn)品對即時(shí)性的要求比較高,那么采用第二種方案,如果產(chǎn)品對即時(shí)性要求不那么高,第一種方案就可以了。由于本文的方案是沒有URL注冊列表的,因此服務(wù)器只要給出原始target-action和對應(yīng)跳轉(zhuǎn)的target-action就可以了,整個(gè)流程不是只有注冊URL列表才能達(dá)成的,而且這種方案比注冊URL列表要更易于維護(hù)一些。

另外,說采用url rewrite的手段來進(jìn)行動(dòng)態(tài)調(diào)度,也不是不可以。但是這里我需要辨析的是,URL的必要性僅僅體現(xiàn)在遠(yuǎn)程App調(diào)度中,是沒必要蔓延到本地組件間調(diào)用的。這樣,當(dāng)我們做遠(yuǎn)程App的URL路由時(shí)(目前的demo沒有提供URL路由功能,但是提供了URL路由操作的接入點(diǎn),可以根據(jù)業(yè)務(wù)需求插入這個(gè)功能),要關(guān)心的事情就能少很多,可以比較干凈。在這種場景下,單純以URL rewrite的方式其實(shí)就與上文提到的以url parse為切點(diǎn)
沒有區(qū)別了。

相比之下,蘑菇街的組件化方案有以下缺陷

蘑菇街沒有拆分遠(yuǎn)程調(diào)用和本地間調(diào)用

不拆分遠(yuǎn)程調(diào)用和本地間調(diào)用,就使得后續(xù)很多手段難以實(shí)施,這個(gè)我在前文中都已經(jīng)有論述了。另外再補(bǔ)充一下,這里的拆分不是針對來源做拆分。比如通過URL來區(qū)分是遠(yuǎn)程App調(diào)用還是本地調(diào)用,這只是區(qū)分了調(diào)用者的來源。

這里說的區(qū)分是指:遠(yuǎn)程調(diào)用走遠(yuǎn)程調(diào)用路徑,也就是openUrl>urlParse->perform->target-action。
本地組件間調(diào)用就走本地組件間調(diào)用路徑:perform->target-action。
這兩個(gè)是一定要作區(qū)分的,蘑菇街方案并沒有對此做好區(qū)分。

蘑菇街以遠(yuǎn)程調(diào)用的方式為本地間調(diào)用提供服務(wù)

這是本末倒置的做法,倒行逆施導(dǎo)致的是未來架構(gòu)難以為業(yè)務(wù)發(fā)展提供支撐。因?yàn)榍懊嬉呀?jīng)論述過,在iOS場景下,遠(yuǎn)程調(diào)用的實(shí)現(xiàn)是本地調(diào)用實(shí)現(xiàn)的子集,只有大的為小提供服務(wù),也就是本地調(diào)用為遠(yuǎn)程調(diào)用提供服務(wù),如果反過來就是倒行逆施了。

蘑菇街的本地間調(diào)用無法傳遞非常規(guī)參數(shù),復(fù)雜參數(shù)的傳遞方式非常丑陋

注意這里復(fù)雜參數(shù)
和非常規(guī)參數(shù)
的辨析。

由于采用遠(yuǎn)程調(diào)用的方式執(zhí)行本地調(diào)用,在前面已經(jīng)論述過兩者功能集的關(guān)系,因此這種做法無法滿足傳遞非常規(guī)參數(shù)的需求。而且如果基于這種方式不變的話,復(fù)雜參數(shù)的傳遞也只能依靠經(jīng)過urlencode的json string進(jìn)行,這種方式非常丑陋,而且也不便于調(diào)試。

蘑菇街必須要在app啟動(dòng)時(shí)注冊URL響應(yīng)者

這個(gè)條件在組件化方案中是不必要條件,demo也已經(jīng)證實(shí)了這一點(diǎn)。這個(gè)不必要的操作會(huì)導(dǎo)致不必要的維護(hù)成本,如果單純從只要完成業(yè)務(wù)就好
的角度出發(fā),這倒不是什么大問題。這就看架構(gòu)師對自己是不是要求嚴(yán)格了。

新增組件化的調(diào)用路徑時(shí),蘑菇街的操作相對復(fù)雜

在本文給出的組件化方案中,響應(yīng)者唯一要做的事情就是提供Target和Action,并不需要再做其它的事情。蘑菇街除此之外還要再做很多額外不必要措施,才能保證調(diào)用成功。

蘑菇街沒有針對target層做封裝

這種做法使得所有的跨組件調(diào)用請求直接hit到業(yè)務(wù)模塊,業(yè)務(wù)模塊必然因此變得臃腫難以維護(hù),屬于侵入式架構(gòu)。應(yīng)該將原本屬于調(diào)用相應(yīng)的部分拿出來放在target-action中,才能盡可能保證不將無關(guān)代碼侵入到原有業(yè)務(wù)組件中,才能保證業(yè)務(wù)組件未來的遷移和修改不受組件調(diào)用的影響,以及降低為項(xiàng)目的組件化實(shí)施而帶來的時(shí)間成本。

總結(jié)

本文提供的組件化方案是采用Mediator模式和蘋果體系下的Target-Action模式設(shè)計(jì)的。

然而這款方案有一個(gè)很小的缺陷在于對param的key的hardcode,這是為了達(dá)到最大限度的解耦和靈活度而做的權(quán)衡。在我的網(wǎng)絡(luò)層架構(gòu)和持久層架構(gòu)中,都沒有hardcode的場景,這也從另一個(gè)側(cè)面說明了組件化架構(gòu)的特殊性。

權(quán)衡時(shí),考慮到這部分hardcode的影響域僅僅存在于mediator的category中
。在這種情況下,hardcode對于調(diào)用者的調(diào)用是完全透明的。對于響應(yīng)者而言,處理方式等價(jià)于對API返回的參數(shù)的處理方式,且響應(yīng)者的處理方式也被限制在了Action中

因此這部分的hardcode的存在雖然確實(shí)有點(diǎn)不干凈,但是相比于這些不干凈而帶來的其他好處而言,在權(quán)衡時(shí)是可以接受的,如果不采用hardcode,那勢必就會(huì)導(dǎo)致請求響應(yīng)方也需要依賴mediator
,然而這在邏輯上是不必要
的。另外,在我的各個(gè)項(xiàng)目的實(shí)際使用過程中,這部分hardcode是沒有影響的。

另外要談的是,之所以會(huì)在組件化方案中出現(xiàn)harcode,而網(wǎng)絡(luò)層和持久層的去model化都沒有發(fā)生hardcode情況,是因?yàn)榻M件化調(diào)用的所有接受者和調(diào)用者都在同一片上下文里。網(wǎng)絡(luò)層有一方在服務(wù)端,持久層有一方在數(shù)據(jù)庫。再加上設(shè)計(jì)時(shí)針對hardcode部分的改進(jìn)手段其實(shí)已經(jīng)超出了語言本身的限制。也就是說,harcode受限于語言本身。objective-C也好,swift也好,它們的接口設(shè)計(jì)哲學(xué)是存在缺陷的。如果我們假設(shè)在golang的背景下,是完全可以用golang的接口體系去做一個(gè)最優(yōu)美的架構(gòu)方案出來的。不過這已經(jīng)不屬于本文的討論范圍了,有興趣的同學(xué)可以去了解一下相關(guān)知識(shí)。架構(gòu)設(shè)計(jì)有時(shí)就是這么無奈。

組件化方案在App業(yè)務(wù)穩(wěn)定,且規(guī)模(業(yè)務(wù)規(guī)模和開發(fā)團(tuán)隊(duì)規(guī)模)增長初期去實(shí)施非常重要,它助于將復(fù)雜App分而治之,也有助于多人大型團(tuán)隊(duì)的協(xié)同開發(fā)。但組件化方案
不適合在業(yè)務(wù)不穩(wěn)定的情況下過早實(shí)施,至少要等產(chǎn)品已經(jīng)經(jīng)過MVP階段時(shí)才適合實(shí)施組件化。因?yàn)闃I(yè)務(wù)不穩(wěn)定意味著鏈路不穩(wěn)定,在不穩(wěn)定的鏈路上實(shí)施組件化會(huì)導(dǎo)致將來主業(yè)務(wù)產(chǎn)生變化時(shí),全局性模塊調(diào)度和重構(gòu)會(huì)變得相對復(fù)雜。
當(dāng)決定要實(shí)施組件化方案時(shí),對于組件化方案的架構(gòu)設(shè)計(jì)優(yōu)劣直接影響到架構(gòu)體系能否長遠(yuǎn)地支持未來業(yè)務(wù)的發(fā)展,對App的組件化不只是僅僅的拆代碼和跨業(yè)務(wù)調(diào)頁面
,還要考慮復(fù)雜和非常規(guī)業(yè)務(wù)參數(shù)參與的調(diào)度,非頁面的跨組件功能調(diào)度,組件調(diào)度安全保障,組件間解耦,新舊業(yè)務(wù)的調(diào)用接口修改等問題。

蘑菇街的組件化方案只實(shí)現(xiàn)了跨業(yè)務(wù)頁面調(diào)用的需求,本質(zhì)上只實(shí)現(xiàn)了我在view層架構(gòu)的文章中跨業(yè)務(wù)頁面調(diào)用的內(nèi)容,這還沒有到成為組件化方案
的程度,且蘑菇街的組件化方案距離真正的App組件化的要求還是差了一段距離的,且存在設(shè)計(jì)邏輯缺陷,希望蘑菇街能夠加緊重構(gòu),打造真正的組件化方案。

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

相關(guān)閱讀更多精彩內(nèi)容

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