[轉(zhuǎn)]iOS應(yīng)用架構(gòu)談 組件化方案

前言:

本文轉(zhuǎn)自前同事casa的博文,這篇文章是基于runtime實現(xiàn)的iOS組件化方案落君,其實iOS組件化方案基本是兩派绎速,一派是蘑菇街、一號店這些用的通過depplink自定義協(xié)議做的組件化和應(yīng)用內(nèi)IPC焙蚓,casa的組件化方案是基于runtime實現(xiàn)的組件化纹冤,組件之間都是通過Target-Action進行調(diào)用洒宝,實現(xiàn)了完全解耦,對于組件間的參數(shù)傳遞也做了去model化萌京,對model也沒有了依賴,雖然本人對去model化持保留意見∠鹋樱總的來說這篇是iOS組件化比較好的方案之一,android組件化業(yè)界目前方案基本比較統(tǒng)一,絕大部分都采用了deeplink的自定義uri的方案强挫,自己項目中也都是采用了這種實現(xiàn)方案,android中也可以使用基于adnroid runtime方案八匠,但是使用自定義協(xié)議方式實施組件化雖然在參數(shù)傳遞和組件耦合方面還是存在一些硬傷,但是也是有一定好處的抡四,最大的優(yōu)點就是可以將ios和android組件化方案統(tǒng)一,實現(xiàn)統(tǒng)一的組件化管理厌处,為后續(xù)做服務(wù)降級缆娃、灰度發(fā)布比較方便,下面還是給大家分享casa的這篇好文,自己不才當(dāng)初讀了好幾遍才理解宅广。
原文地址:https://casatwy.com/iOS-Modulization.html

簡述

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

另外佑附,按道理說組件化方案也屬于iOS應(yīng)用架構(gòu)談的一部分,但是當(dāng)初構(gòu)思架構(gòu)談時权均,我沒打算寫組件化方案橡类,因為我忘了還有這回事兒。。庶诡。后來寫到view的時候才想起來,所以在view的那篇文章最后補了一點內(nèi)容喇澡。而且覺得這個組件化方案太簡單,包括實現(xiàn)組件化方案的組件也很簡單,代碼算上注釋也才100行秀睛,我就偷懶放過了居凶,畢竟寫一篇文章好累的啊。

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

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

  1. App啟動時實例化各組件模塊,然后這些組件向ModuleManager注冊Url数尿,有些時候不需要實例化歼捐,使用class注冊贷盲。
  2. 當(dāng)組件A需要調(diào)用組件B時,向ModuleManager傳遞URL呻疹,參數(shù)跟隨URL以GET方式傳遞,類似openURL并思。然后由ModuleManager負責(zé)調(diào)度組件B,最后完成任務(wù)输涕。

這里的兩步中寸士,每一步都存在問題碴卧。

第一步的問題在于住册,在組件化的過程中垢箕,注冊URL并不是充分必要條件,組件是不需要向組件管理器注冊Url的帅掘。而且注冊了Url之后吱窝,會造成不必要的內(nèi)存常駐照激,如果只是注冊Class汰寓,內(nèi)存常駐量就小一點,如果是注冊實例,內(nèi)存常駐量就大了荣茫。至于蘑菇街注冊的是Class還是實例,Limboy分享時沒有說,文章里我也沒看出來,也有可能是我看漏了毛萌。不過這還并不能算是致命錯誤,只能算是小缺陷狸演。

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

什么意思呢?

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

為什么這么說涮坐?

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

實際App場景下尤莺,如果本地組件間采用GET方式的URL調(diào)用缔杉,就會產(chǎn)生兩個問題:

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

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

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

同時就也要提供這樣的方法:

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

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

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

  • URL注冊對于實施組件化方案是完全不必要的震庭,且通過URL注冊的方式形成的組件化方案,拓展性和可維護性都會被打折

注冊URL的目的其實是一個服務(wù)發(fā)現(xiàn)的過程渣磷,在iOS領(lǐng)域中,服務(wù)發(fā)現(xiàn)的方式是不需要通過主動注冊的蜗字,使用runtime就可以了担神。另外,注冊部分的代碼的維護是一個相對麻煩的事情男韧,每一次支持新調(diào)用時介杆,都要去維護一次注冊列表。如果有調(diào)用被棄用了,是經(jīng)常會忘記刪項目的积锅。runtime由于不存在注冊過程,那就也不會產(chǎn)生維護的操作窜护,維護成本就降低了储耐。

由于通過runtime做到了服務(wù)的自動發(fā)現(xiàn)腹尖,拓展調(diào)用接口的任務(wù)就僅在于各自的模塊乐设,任何一次新接口添加讼庇,新業(yè)務(wù)添加,都不必去主工程做操作近尚,十分透明蠕啄。

小總結(jié)

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

正確的組件化方案

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

             --------------------------------------
             | [CTMediator sharedInstance]        |
             |                                    |
             |                openUrl:       <<<<<<<<<  (AppDelegate)  <<<<  Call From Other App With URL
             |                                    |
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |                parseUrl            |
             |                                    |
             |                   |                |
             |                   |                |
.................................|...............................
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |  performTarget:action:params: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<  Call From Native Module
             |                                    |
             |                   |                |
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |             -------------          |
             |             |           |          |
             |             |  runtime  |          |
             |             |           |          |
             |             -------------          |
             |               .       .            |
             ---------------.---------.------------
                           .           .
                          .             .
                         .               .
                        .                 .
                       .                   .
                      .                     .
                     .                       .
                    .                         .
-------------------.-----------      ----------.---------------------
|                 .           |      |          .                   |
|                .            |      |           .                  |
|               .             |      |            .                 |
|              .              |      |             .                |
|                             |      |                              |
|           Target            |      |           Target             |
|                             |      |                              |
|         /   |   \           |      |         /   |   \            |
|        /    |    \          |      |        /    |    \           |
|                             |      |                              |
|   Action Action Action ...  |      |   Action Action Action ...   |
|                             |      |                              |
|                             |      |                              |
|                             |      |                              |
|Business A                   |      | Business B                   |
-------------------------------      --------------------------------

這幅圖是組件化方案的一個簡化版架構(gòu)描述哈街,主要是基于Mediator模式和Target-Action模式,中間采用了runtime來完成調(diào)用拒迅。這套組件化方案將遠程應(yīng)用調(diào)用和本地應(yīng)用調(diào)用做了拆分骚秦,而且是由本地應(yīng)用調(diào)用為遠程應(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實例以及對應(yīng)的action選擇子蒙揣,然后最終調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯靶溜,完成需求开瞭。

在遠程應(yīng)用調(diào)用中,遠程應(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)用CTMediatoropenUrl:方法將接收到的URL信息傳入葱色。當(dāng)然,CTMediator也可以用openUrl:options:的方式順便把隨之而來的option也接收娘香,這取決于你本地業(yè)務(wù)執(zhí)行邏輯時的充要條件是否包含option數(shù)據(jù)苍狰。傳入URL之后,CTMediator通過解析URL烘绽,將請求路由到對應(yīng)的target和action淋昭,隨后的過程就變成了上面說過的本地應(yīng)用調(diào)用的過程了,最終完成響應(yīng)安接。

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

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

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

            --------------------------------
            |                              |
            |           Business A         |
            |                              |
            ---  ----------  ----------  ---
              |  |        |  |        |  |
              |  |        |  |        |  |
   ...........|  |........|  |........|  |...........
   .          |  |        |  |        |  |          .
   .          |  |        |  |        |  |          .
   .        ---  ---    ---  ---    ---  ---        .
   .        |      |    |      |    |      |        .
   .        |action|    |action|    |action|        .
   .        |      |    |      |    |      |        .
   .        ---|----    -----|--    --|-----        .
   .           |             |        |             .
   .           |             |        |             .
   .       ----|------     --|--------|--           .
   .       |         |     |            |           .
   .       |Target_A1|     |  Target_A2 |           .
   .       |         |     |            |           .
   .       -----------     --------------           .
   .                                                .
   .                                                .
   ..................................................

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

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

這里我們需要針對術(shù)語做一個理解上的統(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ù)中有存在任何一個內(nèi)容是非常規(guī)參數(shù),本文中這種參數(shù)就也被認(rèn)為是非常規(guī)參數(shù)嚼摩。
  • 如果某個類型當(dāng)前不能夠被json解析钦讳,但通過某種轉(zhuǎn)化方式能夠轉(zhuǎn)化成json矿瘦,那么這種類型在場景上下文中,我們也稱為普通類型愿卒。

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

  • 如果上下文場景中沒有轉(zhuǎn)化器柜候,這個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)為這個view是普通類型。如果調(diào)用者是組件A碌嘀,轉(zhuǎn)化器在組件B中涣旨,A傳遞view參數(shù)時是沒辦法轉(zhuǎn)化成json的,那么這個view就被認(rèn)為是非常規(guī)類型股冗,哪怕它在組件B中能夠被轉(zhuǎn)化成json霹陡。

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

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

因此這個邏輯注定了必須由本地組件間調(diào)用來為遠程App調(diào)用來提供服務(wù)泣崩,只有符合這個邏輯的設(shè)計思路才是正確的組件化方案的設(shè)計思路少梁,其他跟這個不一致的思路一定就是錯的。因為邏輯上子集為父集提供服務(wù)說不通矫付,所以強行這么做的話凯沪,用一個成語來總結(jié)就叫做倒行逆施。

另外买优,遠程App調(diào)用和本地組件間調(diào)用必須要拆分開妨马,遠程App調(diào)用只能走CTMediator提供的專用遠程的方法挺举,本地組件間調(diào)用只能走CTMediator提供的專用本地的方法,兩者不能通過同一個接口來調(diào)用烘跺。

這里有兩個原因:

  • 遠程App調(diào)用處理入?yún)⒌倪^程比本地多了一個URL解析的過程湘纵,這是遠程App調(diào)用特有的過程。這一點我前面說過滤淳,這里我就不細說了梧喷。
  • 架構(gòu)師沒有充要條件條件可以認(rèn)為遠程App調(diào)用對于無響應(yīng)請求的處理方式和本地組件間調(diào)用無響應(yīng)請求的處理方式在未來產(chǎn)品的演進過程中是一致的

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

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

場景1

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

場景2

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

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

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

當(dāng)然读整,架構(gòu)師可以選擇打回產(chǎn)品經(jīng)理的需求簿训,最終挑選一個自己的架構(gòu)能夠承載的需求。但是米间,如果這種是因為你早期設(shè)計架構(gòu)時挖的坑而打回的產(chǎn)品需求强品,你不覺得丟臉么?

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


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

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

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

如果對象不是一個面向接口的通用對象,那么mediator的參數(shù)處理就會非常復(fù)雜纲熏,因為要區(qū)分不同的對象類型妆丘。如果mediator不處理參數(shù)锄俄,直接將對象以范型的方式轉(zhuǎn)交給模塊B,那么模塊B必然要包含對象類型的聲明勺拣。假設(shè)對象聲明放在模塊A奶赠,那么B和A之間的組件化只是個形式主義。如果對象類型聲明放在mediator药有,那么對于B而言毅戈,就不得不依賴mediator。但是愤惰,大家可以從上面的架構(gòu)圖中看到苇经,對于響應(yīng)請求的模塊而言,依賴mediator并不是必要條件宦言,因此這種依賴是完全不需要的扇单,這種依賴的存在對于架構(gòu)整體而言,是一種污染奠旺。

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

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

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

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

在去model的組件化方案中,影響效率的點有兩個:調(diào)用方如何知道接收方需要哪些key的參數(shù)蒋搜?調(diào)用方如何知道有哪些target可以被調(diào)用篡撵?其實后面的那個問題不管是不是去model的方案,都會遇到豆挽。為什么放在一起說育谬,因為我接下來要說的解決方案可以把這兩個問題一起解決。

解決方案就是使用category

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

  • category本身就是一種組合模式僵缺,根據(jù)不同的分類提供不同的方法,此時每一個組件就是一個分類踩叭,因此把每個組件可以支持的調(diào)用用category封裝是很合理的磕潮。
  • 在category的方法中可以做到參數(shù)的驗證,在架構(gòu)中對于保證參數(shù)安全是很有必要的容贝。當(dāng)參數(shù)不對時自脯,category就提供了補救的入口。
  • category可以很輕松地做請求轉(zhuǎn)發(fā)斤富,如果不采用category膏潮,請求轉(zhuǎn)發(fā)邏輯就非常難做了。
  • category統(tǒng)一了所有的組件間調(diào)用入口满力,因此無論是在調(diào)試還是源碼閱讀上焕参,都為工程師提供了極大的方便。
  • 由于category統(tǒng)一了所有的調(diào)用入口油额,使得在跨模塊調(diào)用時叠纷,對于param的hardcode在整個App中的作用域僅存在于category中,在這種場景下的hardcode就已經(jīng)變成和調(diào)用宏或者調(diào)用聲明沒有任何區(qū)別了潦嘶,因此是可以接受的涩嚣。

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

    if (indexPath.row == 0) {
        UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];

        // 獲得view controller之后校摩,在這種場景下看峻,到底push還是present阶淘,其實是要由使用者決定的,mediator只要給出view controller的實例就好了
        [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的溪窒,所以不必返回實例,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來實現(xiàn)去model的組件調(diào)用。上面的代碼片段也是摘自這個demo灼狰。


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

基于安全考慮

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

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

當(dāng)然,為了確保安全的做法有很多颜矿,但只要拆出遠程調(diào)用和本地調(diào)用寄猩,各種做法就都有施展的空間了。

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

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

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

  1. 以url parse為切點
  2. 以實例化target時為切點
  3. 以category調(diào)度方法為切點
  4. 以target下的action為切點

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

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

如果以category調(diào)度方法為切點的話巡语,那動態(tài)調(diào)度就只能影響到本地件組件的跳轉(zhuǎn)翎蹈,因為category是只有本地才用的,所以也不適合男公。

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

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

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

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

  1. App啟動時下載調(diào)度列表疚颊,或者定期下載調(diào)度列表狈孔。然后審查時檢查當(dāng)前action是否存在要被動態(tài)調(diào)度跳轉(zhuǎn)的action,如果存在材义,則跳轉(zhuǎn)到另一個action
  2. 每一次到達新的action時均抽,以action為參數(shù)調(diào)用API獲知是否需要被跳轉(zhuǎn),如果需要被跳轉(zhuǎn)其掂,則API告知要跳轉(zhuǎn)的action油挥,然后再跳轉(zhuǎn)到API指定的action

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

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

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

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

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

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

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

這是本末倒置的做法,倒行逆施導(dǎo)致的是未來架構(gòu)難以為業(yè)務(wù)發(fā)展提供支撐腊脱。因為前面已經(jīng)論述過访得,在iOS場景下,遠程調(diào)用的實現(xiàn)是本地調(diào)用實現(xiàn)的子集陕凹,只有大的為小提供服務(wù)悍抑,也就是本地調(diào)用為遠程調(diào)用提供服務(wù),如果反過來就是倒行逆施了杜耙。

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

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

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

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

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

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

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

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

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

總結(jié)

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

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

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

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

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

組件化方案在App業(yè)務(wù)穩(wěn)定路鹰,且規(guī)模(業(yè)務(wù)規(guī)模和開發(fā)團隊規(guī)模)增長初期去實施非常重要贷洲,它助于將復(fù)雜App分而治之,也有助于多人大型團隊的協(xié)同開發(fā)晋柱。但組件化方案不適合在業(yè)務(wù)不穩(wěn)定的情況下過早實施优构,至少要等產(chǎn)品已經(jīng)經(jīng)過MVP階段時才適合實施組件化。因為業(yè)務(wù)不穩(wěn)定意味著鏈路不穩(wěn)定趣斤,在不穩(wěn)定的鏈路上實施組件化會導(dǎo)致將來主業(yè)務(wù)產(chǎn)生變化時俩块,全局性模塊調(diào)度和重構(gòu)會變得相對復(fù)雜。

當(dāng)決定要實施組件化方案時浓领,對于組件化方案的架構(gòu)設(shè)計優(yōu)劣直接影響到架構(gòu)體系能否長遠地支持未來業(yè)務(wù)的發(fā)展玉凯,對App的組件化不只是僅僅的拆代碼和跨業(yè)務(wù)調(diào)頁面,還要考慮復(fù)雜和非常規(guī)業(yè)務(wù)參數(shù)參與的調(diào)度联贩,非頁面的跨組件功能調(diào)度漫仆,組件調(diào)度安全保障,組件間解耦泪幌,新舊業(yè)務(wù)的調(diào)用接口修改等問題盲厌。

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

2016-03-14 20:26 補

沒想到limboy如此迅速地發(fā)文回應(yīng)了右蒲。文章地址在這里:蘑菇街 App 的組件化之路 續(xù)阀湿。然后我花了一些時間重新看了limboy的第一篇文章。我覺得在本文開頭我對蘑菇街的組件化方案描述過于簡略了瑰妄,而且我還忽略了原來是有ModuleManager的陷嘴,所以在這里我重新描述一番。

蘑菇街是以兩種方式來做跨組件操作的

第一種是通過MGJRouterregisterURLPattern:toHandler:進行注冊间坐,將URL和block綁定灾挨。這個方法前面一個參數(shù)傳遞的是URL,例如mgj://detail?id=:id這種眶诈,后面的toHandler:傳遞的是一個^(NSDictionary *routerParameters){// 此處可以做任何事}的block涨醋。

當(dāng)組件執(zhí)行[MGJRouter openURL:@"mgj://detail?id=404"]時,根據(jù)之前registerURLPattern:toHandler:的信息逝撬,找到之前通過toHandler:收集的block浴骂,然后將URL中帶的GET參數(shù),此處是id=404宪潮,傳入block中執(zhí)行溯警。如果在block中執(zhí)行NSLog(routerParameters)的話趣苏,就會看到@{@"id":@"404"},因此block中的業(yè)務(wù)就能夠得到執(zhí)行梯轻。

然后為了業(yè)務(wù)方能夠不生寫URL食磕,蘑菇街列出了一系列宏或者字符串常量(具體是宏還是字符串我就不是很確定,沒看過源碼喳挑,但limboy文章中有提到通過一個后臺系統(tǒng)生成一個裝滿URL的源碼文件)來表征URL彬伦。在openURL時,無論是遠程應(yīng)用調(diào)用還是本地組件間調(diào)用伊诵,只要傳遞的參數(shù)不復(fù)雜单绑,就都會采用openURL的方式去喚起頁面,因為復(fù)雜的參數(shù)和非常規(guī)參數(shù)這種調(diào)用方式就無法支持了曹宴。

缺陷在于:這種注冊的方式其實是不必要的搂橙,而且還白白使用URLblock占用了內(nèi)存。另外還有一個問題就是笛坦,即便是簡單參數(shù)的傳遞区转,如果參數(shù)比較多,業(yè)務(wù)工程師不看原始URL字符串是無法知道要傳遞哪些參數(shù)的版扩。

蘑菇街之所以采用id=:id的方式废离,我猜是為了怕業(yè)務(wù)工程師傳遞多個參數(shù)順序不同會導(dǎo)致問題,而使用的占位符礁芦。這種做法在持久層生成sql字符串時比較常見厅缺。不過這個功能我沒在limboy的文章中看到有寫,不知道實現(xiàn)了沒有宴偿。

在本文提供的組件化方案中,因為沒有注冊诀豁,所以就沒有內(nèi)存的問題窄刘。因為通過category提供接口調(diào)用,就沒有參數(shù)的問題舷胜。對于蘑菇街來說娩践,這種做法其實并沒有做到拆分遠程應(yīng)用調(diào)用和本地組件間調(diào)用的目的,而不拆分會導(dǎo)致的問題我在文章中已經(jīng)論述過了烹骨,這里就不多說了翻伺。


由于前面openURL的方式不能夠傳遞非常規(guī)參數(shù),因此有了第二種注冊方式:新開了一個對象叫做ModuleManager沮焕,提供了一個registerClass:forProtocol:的方法吨岭,在應(yīng)用啟動時,各組件都會有一個專門的ModuleEntry被喚起峦树,然后ModuleEntry@protocolClass進行配對辣辫。因此ModuleManager中就有了一個字典來記錄這個配對旦事。

當(dāng)有涉及非常規(guī)參數(shù)的調(diào)用時,業(yè)務(wù)方就不會去使用[MGJRouter openURL:@"mgj://detail?id=404"]的方案了急灭,轉(zhuǎn)而采用ModuleManagerclassForProtocol:方法姐浮。業(yè)務(wù)傳入一個@protocolModuleManager,然后ModuleManager通過之前注冊過的字典查找到對應(yīng)的Class返回給業(yè)務(wù)方葬馋,然后業(yè)務(wù)方再自己執(zhí)行allocinit方法得到一個符合剛才傳入@protocol的對象卖鲤,然后再執(zhí)行相應(yīng)的邏輯。

這里的ModuleManager其實跟之前的MGJRouter一樣畴嘶,是沒有任何必要去注冊協(xié)議和類名的蛋逾。而且無論是服務(wù)提供者調(diào)用registerClass:forProtocol:也好,服務(wù)的調(diào)用者調(diào)用classForProtocol:掠廓,都必須依賴于同一個protocol换怖。蘑菇街把所有的protocol放入了一個publicProtocol.h的文件中,因此調(diào)用方和響應(yīng)方都必須依賴于同一個文件蟀瞧。這個我在文章中也論述過:響應(yīng)方在提供服務(wù)的時候沉颂,是不需要依賴任何人的。


所以針對蘑菇街的這篇文章我是這么回應(yīng)的:

  • 蘑菇街所謂分開了遠程應(yīng)用調(diào)用和本地組件調(diào)用是不成立的悦污,蘑菇街分開的只是普通參數(shù)調(diào)用非常規(guī)參數(shù)調(diào)用铸屉。不去區(qū)分遠程應(yīng)用調(diào)用和本地組件間調(diào)用的缺陷我在文中已經(jīng)論述過了,這里不多說切端。
  • 蘑菇街確實不只有openURL方式彻坛,還提供了ModuleManager方式,然而所謂的我們其實是分為「組件間調(diào)用」和「頁面間跳轉(zhuǎn)」兩個維度踏枣,只要 app 響應(yīng)某個 URL昌屉,無論是 app 內(nèi)還是 app 外都可以,而「組件間」調(diào)用走的完全是另一條路茵瀑,所以也不會有安全上的問題间驮。其實也是不成立的,因為openURL方式也出現(xiàn)在了本地組件間調(diào)用中马昨,這在他第一篇文章里的組件間通信小節(jié)中就已經(jīng)說了采用openURL方式調(diào)用了竞帽,這是有可能產(chǎn)生安全問題的。而且這段話也承認(rèn)了openURL方式被用于本地組件間調(diào)用鸿捧,又印證了我剛才說的第一點屹篓。
  • 根據(jù)上面兩點,蘑菇街在openURL場景下匙奴,還是出現(xiàn)了以遠程調(diào)用的方式為本地間調(diào)用提供服務(wù)的問題堆巧,這個問題我也已經(jīng)在文中論述過了。
  • 蘑菇街在本地間調(diào)用同時采用了openURL方案和protocol - class方案,所以其實之前我指出蘑菇街本地間調(diào)用不能傳遞非常規(guī)參數(shù)和復(fù)雜參數(shù)是不對的恳邀,應(yīng)該是蘑菇街在本地間調(diào)用時如果是普通參數(shù)懦冰,那就采用openURL,如果是非常規(guī)參數(shù)谣沸,那就采用protocol - class了刷钢,這個做法對于本地間調(diào)用的管理和維護,顯而易見是不利的乳附。内地。。
  • limboy說必須要在 app 啟動時注冊 URL 響應(yīng)者這步不可避免赋除,但沒有說原因阱缓。我的demo已經(jīng)證實了注冊是不必要的,所以我想聽聽limboy如何解釋原因举农。
  • 你的架構(gòu)圖畫錯了
mgj

按照你的方案來看荆针,紅圈的地方是不可能沒有依賴的。颁糟。航背。

另外,limboy也對本文方案提出了一些看法:

認(rèn)為category在某種意義上也是一個注冊過程棱貌。

蘑菇街的注冊和我這里的category其實是兩回事玖媚,而且我無論如何也無法理解把category和注冊URL等價聯(lián)系的邏輯??

一個很簡單的事實就可以證明兩者完全不等價了:我的方案如果沒有category,照樣可以跑婚脱,就是業(yè)務(wù)方調(diào)用丑陋一點今魔。蘑菇街如果不注冊URL,整個流程就跑不起來了~

認(rèn)為openURL的好處是可以更少地關(guān)心業(yè)務(wù)邏輯障贸,本文方案的好處是可以很方便地完成參數(shù)傳遞错森。

我沒覺得本文方案關(guān)心的業(yè)務(wù)邏輯比openURL更多,因為兩者比較起來篮洁,都是傳參數(shù)發(fā)調(diào)用請求问词,在關(guān)心業(yè)務(wù)邏輯的條件下,兩者完全一樣嘀粱。唯一的不同就是,我能傳非常規(guī)參數(shù)而openURL不能辰狡。本文方案的整個過程中锋叨,在調(diào)用者這一方是完全沒有涉及到任何屬于響應(yīng)者的業(yè)務(wù)邏輯的。

認(rèn)為protocol/URL注冊將target-action抽象出調(diào)用接口是等價的

這其實只是效果等價了宛篇,兩者真正的區(qū)別在于:protocol對業(yè)務(wù)產(chǎn)生了侵入娃磺,且不符合黑盒模型。

  • 我來解釋一下protocol侵入業(yè)務(wù)的原因

由于業(yè)務(wù)中的某個對象需要被調(diào)用叫倍,因此必須要符合某個可被調(diào)用的protocol偷卧,然而這個protocol又不存在于當(dāng)前業(yè)務(wù)領(lǐng)域豺瘤,于是當(dāng)前業(yè)務(wù)就不得不依賴publicProtocol。這對于將來的業(yè)務(wù)遷移是有非常大的影響的听诸。

  • 另外再解釋一下為什么不符合黑盒模型

蘑菇街的protocol方式使對象要在調(diào)用者處使用坐求,由于調(diào)用者并不包含對象原本所處的業(yè)務(wù)領(lǐng)域,當(dāng)完成任務(wù)需要多個這樣的對象的時候晌梨,就需要多次通過protocol獲得class來實例化多個對象桥嗤,最終才能完成需求。

但是target-action模式保證了在執(zhí)行組件間調(diào)用的響應(yīng)時仔蝌,執(zhí)行的上下文處于響應(yīng)者環(huán)境中泛领,這跟蘑菇街的protocol方案相比就是最大的差別。因為從黑盒理論上講敛惊,調(diào)用者只管發(fā)起請求渊鞋,請求的執(zhí)行應(yīng)該由響應(yīng)者來負責(zé),因此執(zhí)行邏輯必須存在于響應(yīng)者的上下文內(nèi)瞧挤,而不能存在于調(diào)用者的上下文內(nèi)锡宋。

舉個具體一點的例子就是,當(dāng)你發(fā)起了一個網(wǎng)頁請求皿伺,后端取好數(shù)據(jù)渲染好頁面员辩,無論獲取數(shù)據(jù)涉及多少渠道,獲取數(shù)據(jù)的邏輯都在服務(wù)端完成鸵鸥,然后再返回給瀏覽器展示奠滑。這個是正確的做法,target-action模式也是這么做的妒穴。

但是蘑菇街的方案就變成了這樣:你發(fā)起了一個網(wǎng)絡(luò)請求宋税,后端返回的不是數(shù)據(jù),返回的竟然是一個數(shù)據(jù)獲取對象(DAO)讼油,然后你再通過DAO去取數(shù)據(jù)杰赛,去渲染頁面,如果渲染頁面的過程涉及多個DAO矮台,那么你還要再發(fā)起更多請求乏屯,拿到的還是DAO,然后再拿這個DAO去獲取數(shù)據(jù)瘦赫,然后渲染頁面辰晕。這是一種非常詭異的做法。确虱。含友。

如果說這么做是為了應(yīng)對執(zhí)行業(yè)務(wù)的過程中,需要根據(jù)中間階段的返回值來決定接下來的邏輯走向的話,那也應(yīng)該是多次調(diào)用獲得數(shù)據(jù)窘问,然后決定接下來的業(yè)務(wù)走向辆童,而不是每次拿到的都是DAO啊。惠赫。把鉴。使用target-action方式來應(yīng)對這種場景其實也很自然啊~

所以綜上所述,蘑菇街的方案是存在很大問題的汉形,希望蘑菇街繼續(xù)改正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纸镊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子概疆,更是在濱河造成了極大的恐慌逗威,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岔冀,死亡現(xiàn)場離奇詭異凯旭,居然都是意外死亡,警方通過查閱死者的電腦和手機使套,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門罐呼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人侦高,你說我怎么就攤上這事嫉柴。” “怎么了奉呛?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵计螺,是天一觀的道長。 經(jīng)常有香客問我瞧壮,道長登馒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任咆槽,我火速辦了婚禮陈轿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘秦忿。我一直安慰自己麦射,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布灯谣。 她就那樣靜靜地躺著法褥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪酬屉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音呐萨,去河邊找鬼杀饵。 笑死,一個胖子當(dāng)著我的面吹牛谬擦,可吹牛的內(nèi)容都是我干的切距。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼惨远,長吁一口氣:“原來是場噩夢啊……” “哼谜悟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起北秽,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤葡幸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贺氓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔚叨,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年辙培,在試婚紗的時候發(fā)現(xiàn)自己被綠了蔑水。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡扬蕊,死狀恐怖搀别,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尾抑,我是刑警寧澤歇父,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站蛮穿,受9級特大地震影響庶骄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜践磅,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一单刁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧府适,春花似錦羔飞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疟暖,卻和暖如春卡儒,著一層夾襖步出監(jiān)牢的瞬間田柔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工骨望, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留硬爆,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓擎鸠,卻偏偏與公主長得像缀磕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子劣光,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容