iOS應用架構談 開篇
iOS應用架構談 view層的組織和調用方案
iOS應用架構談 網絡層設計方案
iOS應用架構談 本地持久化方案及動態部署
iOS應用架構談 組件化方案html
前幾天的一個晚上在infoQ的微信羣裏,來自蘑菇街的Limboy作了一個分享,講了蘑菇街的組件化之路。我不認爲這條組件化之路蘑菇街走對了。分享後我私聊了Limboy,Limboy彷佛也明白了問題所在,我答應他我會把個人方案寫成文章,因而這篇文章就出來了。ios
另外,按道理說組件化方案也屬於iOS應用架構談的一部分,可是當初構思架構談時,我沒打算寫組件化方案,由於我忘了還有這回事兒。。。後來寫到view的時候纔想起來,因此在view的那篇文章最後補了一點內容。並且以爲這個組件化方案太簡單,包括實現組件化方案的組件也很簡單,代碼算上註釋也才100行,我就偷懶放過了,畢竟寫一篇文章好累的啊。git
本文的組件化方案demo在這裏
https://github.com/casatwy/CTMediator 拉下來後記得pod install 拉下來後記得pod install 拉下來後記得pod install
,這個Demo對業務敏感的邊界狀況處理比較簡單,這須要根據不一樣App的特性和不一樣產品的需求才能作,因此只是爲了說明組件化架構用的。若是要應用在實際場景中的話,能夠根據代碼裏給出的註釋稍加修改,就能用了。github
蘑菇街的原文地址在這裏:《蘑菇街 App 的組件化之路》,沒有耐心看完原文的朋友,我在這裏簡要介紹一下蘑菇街的組件化是怎麼作的:golang
這裏的兩步中,每一步都存在問題。正則表達式
第一步的問題在於,在組件化的過程當中,註冊URL並非充分必要條件,組件是不須要向組件管理器註冊Url的。並且註冊了Url以後,會形成沒必要要的內存常駐,若是隻是註冊Class,內存常駐量就小一點,若是是註冊實例,內存常駐量就大了。至於蘑菇街註冊的是Class仍是實例,Limboy分享時沒有說,文章裏我也沒看出來,也有多是我看漏了。不過這還並不能算是致命錯誤,只能算是小缺陷。sql
真正的致命錯誤在第二步。在iOS領域裏,必定是組件化的中間件爲openUrl提供服務,而不是openUrl方式爲組件化提供服務。數據庫
什麼意思呢?json
也就是說,一個App的組件化方案必定不是創建在URL上的,openURL的跨App調用是能夠創建在組件化方案上的。固然,若是App尚未組件化,openURL方式也是能夠創建的,就是醜陋一點而已。swift
爲何這麼說?
由於組件化方案的實施過程當中,須要處理的問題的複雜度,以及拆解、調度業務的過程的複雜度比較大,單純以openURL的方式是沒法勝任讓一個App去實施組件化架構的。若是在給App實施組件化方案的過程當中是基於openURL的方案的話,有一個致命缺陷:很是規對象沒法參與本地組件間調度。關於很是規對象
我會在詳細講解組件化方案時有一個辨析。
實際App場景下,若是本地組件間採用GET方式的URL調用,就會產生兩個問題:
根本沒法表達很是規對象
好比你要調用一個圖片編輯模塊,不能傳遞UIImage到對應的模塊上去的話,這是一個很悲催的事情。 固然,這能夠經過給方法新開一個參數,而後傳遞過去來解決。好比原來是:
[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"] }]
若是不像上面這麼作,複雜參數和很是規參數就沒法傳遞。若是這麼作了,那麼事實上這就是拆分遠程調用和本地調用的入口了,這就變成了我文章中提倡的作法,也是蘑菇街方案沒有作到的地方。
另外,在本地調用中使用URL的方式實際上是沒必要要的,若是業務工程師在本地間調度時須要給出URL,那麼就不可避免要提供params,在調用時要提供哪些params是業務工程師很容易懵逼的地方。。。在文章下半部分給出的demo代碼樣例已經說明了業務工程師在本地間調用時,是不須要知道URL的,並且demo代碼樣例也闡釋瞭如何解決業務工程師遇到傳params容易懵逼的問題。
URL註冊對於實施組件化方案是徹底沒必要要的,且經過URL註冊的方式造成的組件化方案,拓展性和可維護性都會被打折
註冊URL的目的實際上是一個服務發現的過程,在iOS領域中,服務發現的方式是不須要經過主動註冊的,使用runtime就能夠了。另外,註冊部分的代碼的維護是一個相對麻煩的事情,每一次支持新調用時,都要去維護一次註冊列表。若是有調用被棄用了,是常常會忘記刪項目的。runtime因爲不存在註冊過程,那就也不會產生維護的操做,維護成本就下降了。
因爲經過runtime作到了服務的自動發現,拓展調用接口的任務就僅在於各自的模塊,任何一次新接口添加,新業務添加,都沒必要去主工程作操做,十分透明。
蘑菇街採用了openURL的方式來進行App的組件化是一個錯誤的作法,使用註冊的方式發現服務是一個沒必要要的作法。並且這方案還有其它問題,隨着下文對組件化方案介紹的展開,相信各位天然內心有數。
先來看一下方案的架構圖
--------------------------------------
| [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 | ------------------------------- --------------------------------
這幅圖是組件化方案的一個簡化版架構描述,主要是基於Mediator模式和Target-Action模式,中間採用了runtime來完成調用。這套組件化方案將遠程應用調用和本地應用調用作了拆分,並且是由本地應用調用爲遠程應用調用提供服務,與蘑菇街方案正好相反。
調用方式
先說本地應用調用,本地組件A在某處調用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]
向CTMediator
發起跨組件調用,CTMediator
根據得到的target和action信息,經過objective-C的runtime轉化生成target實例以及對應的action選擇子,而後最終調用到目標業務提供的邏輯,完成需求。
在遠程應用調用中,遠程應用經過openURL的方式,由iOS系統根據info.plist裏的scheme配置找到能夠響應URL的應用(在當前咱們討論的上下文中,這就是你本身的應用),應用經過AppDelegate
接收到URL以後,調用CTMediator
的openUrl:
方法將接收到的URL信息傳入。固然,CTMediator
也能夠用openUrl:options:
的方式順便把隨之而來的option也接收,這取決於你本地業務執行邏輯時的充要條件是否包含option數據。傳入URL以後,CTMediator
經過解析URL,將請求路由到對應的target和action,隨後的過程就變成了上面說過的本地應用調用的過程了,最終完成響應。
針對請求的路由操做不多會採用本地文件記錄路由表的方式,服務端常常處理這種業務,在服務端領域基本上都是經過正則表達式來作路由解析。App中作路由解析能夠作得簡單點,制定URL規範就也能完成,最簡單的方式就是scheme://target/action
這種,簡單作個字符串處理就能把target和action信息從URL中提取出來了。
組件僅經過Action暴露可調用接口
全部組件都經過組件自帶的Target-Action來響應,也就是說,模塊與模塊之間的接口被固化在了Target-Action這一層,避免了實施組件化的改造過程當中,對Business的侵入,同時也提升了組件化接口的可維護性。
--------------------------------
| |
| Business A |
| |
--- ---------- ---------- ---
| | | | | |
| | | | | |
...........| |........| |........| |...........
. | | | | | | .
. | | | | | | .
. --- --- --- --- --- --- .
. | | | | | | .
. |action| |action| |action| .
. | | | | | | .
. ---|---- -----|-- --|----- .
. | | | .
. | | | .
. ----|------ --|--------|-- .
. | | | | .
. |Target_A1| | Target_A2 | .
. | | | | .
. ----------- -------------- .
. .
. .
..................................................
你們能夠看到,虛線圈起來的地方就是用於跨組件調用的target和action,這種方式避免了由BusinessA直接提供組件間調用會增長的複雜度,並且任何組件若是想要對外提供調用服務,直接掛上target和action就能夠了,業務自己在大多數場景下去進行組件化改造時,是基本不用動的。
複雜參數和很是規參數,以及組件化相關設計思路
這裏咱們須要針對術語作一個理解上的統一:
複雜參數
是指由普通類型
的數據組成的多層級參數。在本文中,咱們定義只要是可以被json解析的類型就都是普通類型
,包括NSNumber, NSString, NSArray, NSDictionary,以及相關衍生類型,好比來自系統的NSMutableArray或者你本身定義的都算。
總結一下就是:在本文討論的場景中,複雜參數的定義是由普通類型組成的具備複雜結構的參數。普通類型的定義就是指可以被json解析的類型。
很是規參數
是指由普通類型
之外的類型組成的參數,例如UIImage等這些不可以被json解析的類型。而後這些類型組成的參數在文中就被定義爲很是規參數
。
總結一下就是:很是規參數
是包含很是規類型的參數。很是規類型
的定義就是不能被json解析的類型都叫很是規類型。
邊界狀況:
舉個例子就是經過json描述的自定義view。若是這個view可以經過某個組件被轉化成json,那麼即便這個view自己並非普通類型,在具備轉化器的上下文場景中,咱們依舊認爲它是普通類型。
而後我來解釋一下爲何應該由本地組件間調用來支持遠程應用調用:
在遠程App調用時,遠程App是不可能經過URL來提供很是規參數的,最多隻能以json string的方式通過URLEncode以後再經過GET來提供複雜參數,而後再在本地組件中解析json,最終完成調用。在組件間調用時,經過performTarget:action:params:
是可以提供很是規參數的,因而咱們能夠知道,遠程App調用
時的上下文環境以及功能是本地組件間調用
時上下文環境以及功能的子集
。
所以這個邏輯註定了必須由本地組件間調用來爲遠程App調用來提供服務,只有符合這個邏輯的設計思路纔是正確的組件化方案的設計思路,其餘跟這個不一致的思路必定就是錯的。由於邏輯上子集爲父集提供服務說不通,因此強行這麼作的話,用一個成語來總結就叫作倒行逆施。
另外,遠程App調用和本地組件間調用必需要拆分開,遠程App調用只能走CTMediator
提供的專用遠程的方法,本地組件間調用只能走CTMediator
提供的專用本地的方法,二者不能經過同一個接口來調用。
這裏有兩個緣由:
遠程App調用處理入參的過程比本地多了一個URL解析的過程,這是遠程App調用特有的過程。這一點我前面說過,這裏我就不細說了。
架構師沒有充要條件條件能夠認爲遠程App調用對於無響應請求的處理方式和本地組件間調用無響應請求的處理方式在將來產品的演進過程當中是一致的
在遠程App調用中,用戶經過url進入app,當app沒法爲這個url提供服務時,常見的辦法是展現一個所謂的404界面,告訴用戶"當前沒有相對應的內容,不過你能夠在app裏別的地方再逛逛"。這個場景多見於用戶使用的App版本不一致。好比有一個URL只有1.1版本的app能完整響應,1.0版本的app雖然能被喚起,可是沒法完成整個響應過程,那麼1.0的app就要展現一個404了。
在組件間調用中,若是遇到了沒法響應的請求,就要分兩種場景考慮了。
場景1
若是這種沒法響應的請求發生場景是在開發過程當中,好比兩個組件同時在開發,組件A調用組件B時,組件B還處於舊版本沒有發佈新版本,所以響應不了,那麼這時候的處理方式能夠相對隨意,只要能體現B模塊是舊版本就好了,最後在RC階段統測時是必定可以發現的,只要App沒發版,怎麼處理都來得及。
場景2
若是這種沒法響應的請求發生場景是在已發佈的App中,有可能展現個404就結束了,那這就跟遠程App調用時的404處理場景同樣。但也有可能須要爲此作一些額外的事情,有可能由於作了額外的事情,就不展現404了,展現別的頁面了,這一切取決於產品經理。
那麼這種場景是如何發生的呢?
我舉一個例子:當用戶在1.0版本時收藏了一個東西,而後用戶升級App到1.1版本。1.0版本的收藏項目在本地持久層存入的數據有多是會跟1.1版本收藏時存入的數據是不一致的。此時用戶在1.1版本的app中對1.0版本收藏的東西作了一些操做,觸發了本地組件間調用,這個本地間調用又與收藏項目自己的數據相關,那麼這時這個調用就是有可能變成無響應調用,此時的處理方式就不見得跟之前同樣展現個404頁面就結束了,由於用戶已經看到了收藏了的東西,結果你還告訴他找不到,用戶馬上懵逼。。。這時候的處理方式就會用不少種,至於產品經理會選擇哪一種,你做爲架構師是沒有辦法預測的。若是產品經理提的需求落實到架構上,對調用入口產生要求然而你的架構又沒有拆分調用入口,對於你的選擇就只有兩個:要麼打回產品需求,要麼加個班去拆分調用入口。
固然,架構師能夠選擇打回產品經理的需求,最終挑選一個本身的架構可以承載的需求。可是,若是這種是由於你早期設計架構時挖的坑而打回的產品需求,你不以爲丟臉麼?
鑑於遠程app調用和本地組件間調用下的無響應請求處理方式不一樣,以及將來不可知的產品演進,拆分遠程app調用入口和本地組件間調用入口是功在當代利在千秋的事情。
組件化方案中的去model設計
組件間調用時,是須要針對參數作去model化的。若是組件間調用不對參數作去model化的設計,就會致使業務形式上被組件化了,實質上依然沒有被獨立
。
假設模塊A和模塊B之間採用model化的方案去調用,那麼調用方法時傳遞的參數就會是一個對象。
若是對象不是一個面向接口的通用對象,那麼mediator的參數處理就會很是複雜,由於要區分不一樣的對象類型。若是mediator不處理參數,直接將對象以範型的方式轉交給模塊B,那麼模塊B必然要包含對象類型的聲明。假設對象聲明放在模塊A,那麼B和A之間的組件化只是個形式主義。若是對象類型聲明放在mediator,那麼對於B而言,就不得不依賴mediator。可是,你們能夠從上面的架構圖中看到,對於響應請求的模塊而言,依賴mediator並非必要條件,所以這種依賴是徹底不須要的,這種依賴的存在對於架構總體而言,是一種污染。
若是參數是一個面向接口的對象,那麼mediator對於這種參數的處理其實就不必了,更多的是直接轉給響應方的模塊。並且接口的定義就不可能放在發起方的模塊中了,只能放在mediator中。響應方若是要完成響應,就也必需要依賴mediator,然而前面我已經說過,響應方對於mediator的依賴是沒必要要的,所以參數其實也並不適合以面向接口的對象的方式去傳遞。
所以,使用對象化的參數不管是否面向接口,帶來的結果就是業務模塊形式上是被組件化了,但實質上依然沒有被獨立。
在這種跨模塊場景中,參數最好仍是以去model化的方式去傳遞,在iOS的開發中,就是以字典的方式去傳遞。這樣就可以作到只有調用方依賴mediator,而響應方不須要依賴mediator。然而在去model化的實踐中,因爲這種方式自由度太大,咱們至少須要保證調用方生成的參數可以被響應方理解,然而在組件化場景中,限制去model化方案的自由度的手段,相比於網絡層和持久層更加容易得多。
由於組件化自然具有了限制手段:參數不對就沒法調用!沒法調用時直接debug就能很快找到緣由。因此接下來要解決的去model化方案的另外一個問題就是:如何提升開發效率。
在去model的組件化方案中,影響效率的點有兩個:調用方如何知道接收方須要哪些key的參數?調用方如何知道有哪些target能夠被調用?其實後面的那個問題無論是否是去model的方案,都會遇到。爲何放在一塊兒說,由於我接下來要說的解決方案能夠把這兩個問題一塊兒解決。
解決方案就是使用category
mediator這個repo維護了若干個針對mediator的category,每個對應一個target,每一個category裏的方法對應了這個target下全部可能的調用場景,這樣調用者在包含mediator的時候,自動得到了全部可用的target-action,不管是調用仍是參數傳遞,都很是方便。接下來我要解釋一下爲何是category而不是其餘:
category自己就是一種組合模式,根據不一樣的分類提供不一樣的方法,此時每個組件就是一個分類,所以把每一個組件能夠支持的調用用category封裝是很合理的。
在category的方法中能夠作到參數的驗證,在架構中對於保證參數安全是頗有必要的。當參數不對時,category就提供了補救的入口。
category能夠很輕鬆地作請求轉發,若是不採用category,請求轉發邏輯就很是難作了。
category統一了全部的組件間調用入口,所以不管是在調試仍是源碼閱讀上,都爲工程師提供了極大的方便。
因爲category統一了全部的調用入口,使得在跨模塊調用時,對於param的hardcode在整個App中的做用域僅存在於category中,在這種場景下的hardcode就已經變成和調用宏或者調用聲明沒有任何區別了,所以是能夠接受的。
這裏是業務方使用category調用時的場景,你們能夠看到很是方便,不用去記URL也不用糾結到底應該傳哪些參數。
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) { // 這種場景下,參數有問題,所以須要在流程中作好處理 [[CTMediator sharedInstance] CTMediator_presentImage:nil]; } if (indexPath.row == 4) { [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) { // 作你想作的事 NSLog(@"%@", info); }]; }
本文對應的demo展現瞭如何使用category來實現去model的組件調用。上面的代碼片斷也是摘自這個demo。
基於安全考慮
咱們須要防止黑客經過URL的方式調用本屬於native的組件,好比支付寶的我的財產頁面。若是在調用層級上沒有區分好,沒有作好安全措施,黑客就有經過safari查看任何人的我的財產的可能。
安全措施其實有不少,大部分取決於App自己以及產品的要求。在架構層面要作的最基礎的一點就是區分調用是來自於遠程App仍是本地組件,我在demo中的安全措施是採用給action添加native
前綴去作的,凡是帶有native前綴的就都只容許本地組件調用,若是在url階段發現調用了前綴爲native的方法,那就能夠採起響應措施了。這也是將遠程app調用入口和本地組件調用入口區分開來的重要緣由之一。
固然,爲了確保安全的作法有不少,但只要拆出遠程調用和本地調用,各類作法就都有施展的空間了。
基於動態調度考慮
動態調度的意思就是,今天我可能這個跳轉是要展現A頁面,可是明天可能一樣的跳轉就要去展現B頁面了。這個跳轉有多是來自於本地組件間跳轉也有多是來自於遠程app。
作這個事情的切點在本文架構中,有不少個:
若是以url parse爲切點
的話,那麼這個動態調度就只可以對遠程App跳轉產生影響,失去了動態調度本地跳轉的能力,所以是不適合的。
若是以實例化target時爲切點
的話,就須要在代碼中針對全部target都作一次審查,看是否要被調度,這是不必的。假設10個調用請求中,只有1個要被動態調度,那麼就必需要審查10次,只有那1次審查經過了,才走動態調度,這是一種相對比較粗暴的方法。
若是以category調度方法爲切點
的話,那動態調度就只能影響到本地件組件的跳轉,由於category是隻有本地才用的,因此也不適合。
以target下的action爲切點
是最適合的,由於動態調度在通常場景下都是有範圍的,大多數是活動頁須要動態調度,今天這個活動明天那個活動,或者今天活動正在進行明天活動就結束了,因此產生動態調度的需求。咱們在可能產生動態調度的action中審查當前action是否須要被動態調度,在常規調度中就不必審查了,例如我的主頁的跳轉,商品詳情的跳轉等,這樣效率就能比較高。
你們會發現,若是要作相似這種效率更高的動態調度,target-action層被抽象出來就是必不可少的,然而蘑菇街並無抽象出target-action層,這也是其中的一個問題。
固然,若是你的產品要求全部頁面都是存在動態調度需求的,那就仍是以實例化target時爲切點
去調度了,這樣能作到審查每一次調度請求,從而實現動態調度。
說完了調度切點,接下來要說的就是如何完成審查流程。完整的審查流程有幾種,我每一個都列舉一下:
這兩種作法其實均可以,若是產品對即時性的要求比較高,那麼採用第二種方案,若是產品對即時性要求不那麼高,第一種方案就能夠了。因爲本文的方案是沒有URL註冊列表的,所以服務器只要給出原始target-action和對應跳轉的target-action就能夠了,整個流程不是隻有註冊URL列表才能達成的,並且這種方案比註冊URL列表要更易於維護一些。
另外,說採用url rewrite的手段來進行動態調度,也不是不能夠。可是這裏我須要辨析的是,URL的必要性僅僅體如今遠程App調度中,是不必蔓延到本地組件間調用的。這樣,當咱們作遠程App的URL路由時(目前的demo沒有提供URL路由功能,可是提供了URL路由操做的接入點,能夠根據業務需求插入這個功能),要關心的事情就能少不少,能夠比較乾淨。在這種場景下,單純以URL rewrite的方式其實就與上文提到的以url parse爲切點
沒有區別了。
不拆分遠程調用和本地間調用,就使得後續不少手段難以實施,這個我在前文中都已經有論述了。另外再補充一下,這裏的拆分不是針對來源作拆分。好比經過URL來區分是遠程App調用仍是本地調用,這只是區分了調用者的來源。
這裏說的區分是指:遠程調用走遠程調用路徑,也就是openUrl
->urlParse
->perform
->target-action
。本地組件間調用就走本地組件間調用路徑:perform
->target-action
。這兩個是必定要做區分的,蘑菇街方案並無對此作好區分。
這是本末倒置的作法,倒行逆施致使的是將來架構難覺得業務發展提供支撐。由於前面已經論述過,在iOS場景下,遠程調用的實現是本地調用實現的子集,只有大的爲小提供服務,也就是本地調用爲遠程調用提供服務,若是反過來就是倒行逆施了。
注意這裏複雜參數
和很是規參數
的辨析。
因爲採用遠程調用的方式執行本地調用,在前面已經論述過二者功能集的關係,所以這種作法沒法知足傳遞很是規參數的需求。並且若是基於這種方式不變的話,複雜參數的傳遞也只能依靠通過urlencode的json string進行,這種方式很是醜陋,並且也不便於調試。
這個條件在組件化方案中是沒必要要條件,demo也已經證明了這一點。這個沒必要要的操做會致使沒必要要的維護成本,若是單純從只要完成業務就好
的角度出發,這倒不是什麼大問題。這就看架構師對本身是否是要求嚴格了。
在本文給出的組件化方案中,響應者惟一要作的事情就是提供Target和Action,並不須要再作其它的事情。蘑菇街除此以外還要再作不少額外沒必要要措施,才能保證調用成功。
這種作法使得全部的跨組件調用請求直接hit到業務模塊,業務模塊必然所以變得臃腫難以維護,屬於侵入式架構。應該將本來屬於調用相應的部分拿出來放在target-action中,才能儘量保證不將無關代碼侵入到原有業務組件中,才能保證業務組件將來的遷移和修改不受組件調用的影響,以及下降爲項目的組件化實施而帶來的時間成本。
本文提供的組件化方案是採用Mediator模式和蘋果體系下的Target-Action模式設計的。
然而這款方案有一個很小的缺陷在於對param的key的hardcode,這是爲了達到最大限度的解耦和靈活度而作的權衡。在個人網絡層架構和持久層架構中,都沒有hardcode的場景,這也從另外一個側面說明了組件化架構的特殊性。
權衡時,考慮到這部分hardcode的影響域僅僅存在於mediator的category中
。在這種狀況下,hardcode對於調用者的調用是徹底透明的。對於響應者而言,處理方式等價於對API返回的參數的處理方式,且響應者的處理方式也被限制在了Action中
。
所以這部分的hardcode的存在雖然確實有點不乾淨,可是相比於這些不乾淨而帶來的其餘好處而言,在權衡時是能夠接受的,若是不採用hardcode,那勢必就會致使請求響應方也須要依賴mediator
,然而這在邏輯上是沒必要要
的。另外,在個人各個項目的實際使用過程當中,這部分hardcode是沒有影響的。
另外要談的是,之因此會在組件化方案中出現harcode,而網絡層和持久層的去model化都沒有發生hardcode狀況,是由於組件化調用的全部接受者和調用者都在同一片上下文裏。網絡層有一方在服務端,持久層有一方在數據庫。再加上設計時針對hardcode部分的改進手段其實已經超出了語言自己的限制。也就是說,harcode受限於語言自己。objective-C也好,swift也好,它們的接口設計哲學是存在缺陷的。若是咱們假設在golang的背景下,是徹底能夠用golang的接口體系去作一個最優美的架構方案出來的。不過這已經不屬於本文的討論範圍了,有興趣的同窗能夠去了解一下相關知識。架構設計有時就是這麼無奈。
組件化方案在App業務穩定,且規模(業務規模和開發團隊規模)增加初期去實施很是重要,它助於將複雜App分而治之,也有助於多人大型團隊的協同開發。但組件化方案
不適合在業務不穩定的狀況下過早實施,至少要等產品已經通過MVP階段時才適合實施組件化。由於業務不穩定意味着鏈路不穩定,在不穩定的鏈路上實施組件化會致使未來主業務產生變化時,全局性模塊調度和重構會變得相對複雜。
當決定要實施組件化方案時,對於組件化方案的架構設計優劣直接影響到架構體系可否長遠地支持將來業務的發展,對App的組件化不僅是僅僅的拆代碼和跨業務調頁面
,還要考慮複雜和很是規業務參數參與的調度,非頁面的跨組件功能調度,組件調度安全保障,組件間解耦,新舊業務的調用接口修改等問題。
蘑菇街的組件化方案只實現了跨業務頁面調用的需求,本質上只實現了我在view層架構的文章中跨業務頁面調用的內容,這尚未到成爲組件化方案
的程度,且蘑菇街的組件化方案距離真正的App組件化的要求仍是差了一段距離的,且存在設計邏輯缺陷,但願蘑菇街可以加緊重構,打造真正的組件化方案。
沒想到limboy如此迅速地發文迴應了。文章地址在這裏:蘑菇街 App 的組件化之路 續。而後我花了一些時間從新看了limboy的第一篇文章。我以爲在本文開頭我對蘑菇街的組件化方案描述過於簡略了,並且我還忽略了原來是有ModuleManager
的,因此在這裏我從新描述一番。
第一種是經過
MGJRouter
的registerURLPattern:toHandler:
進行註冊,將URL和block綁定。這個方法前面一個參數傳遞的是URL,例如mgj://detail?id=:id
這種,後面的toHandler:
傳遞的是一個^(NSDictionary *routerParameters){// 此處能夠作任何事}
的block。
當組件執行[MGJRouter openURL:@"mgj://detail?id=404"]
時,根據以前registerURLPattern:toHandler:
的信息,找到以前經過toHandler:
收集的block,而後將URL中帶的GET參數,此處是id=404
,傳入block中執行。若是在block中執行NSLog(routerParameters)
的話,就會看到@{@"id":@"404"}
,所以block中的業務就可以獲得執行。
而後爲了業務方可以不生寫URL,蘑菇街列出了一系列宏或者字符串常量(具體是宏仍是字符串我就不是很肯定,沒看過源碼,但limboy文章中有提到經過一個後臺系統生成一個裝滿URL的源碼文件)來表徵URL。在openURL
時,不管是遠程應用調用仍是本地組件間調用,只要傳遞的參數不復雜,就都會採用openURL
的方式去喚起頁面,由於複雜的參數和很是規參數這種調用方式就沒法支持了。
缺陷在於:這種註冊的方式實際上是沒必要要的,並且還白白使用URL
和block
佔用了內存。另外還有一個問題就是,即使是簡單參數的傳遞,若是參數比較多,業務工程師不看原始URL字符串是沒法知道要傳遞哪些參數的。
蘑菇街之因此採用id=:id
的方式,我猜是爲了怕業務工程師傳遞多個參數順序不一樣會致使問題,而使用的佔位符。這種作法在持久層生成sql字符串時比較常見。不過這個功能我沒在limboy的文章中看到有寫,不知道實現了沒有。
在本文提供的組件化方案中,由於沒有註冊,因此就沒有內存的問題。由於經過category提供接口調用,就沒有參數的問題。對於蘑菇街來講,這種作法其實並無作到拆分遠程應用調用和本地組件間調用的目的,而不拆分會致使的問題我在文章中已經論述過了,這裏就很少說了。
因爲前面openURL的方式不可以傳遞很是規參數,所以有了第二種註冊方式:新開了一個對象叫作
ModuleManager
,提供了一個registerClass:forProtocol:
的方法,在應用啓動時,各組件都會有一個專門的ModuleEntry
被喚起,而後ModuleEntry
將@protocol
和Class
進行配對。所以ModuleManager
中就有了一個字典來記錄這個配對。
當有涉及很是規參數的調用時,業務方就不會去使用[MGJRouter openURL:@"mgj://detail?id=404"]
的方案了,轉而採用ModuleManager
的classForProtocol:
方法。業務傳入一個@protocol
給ModuleManager
,而後ModuleManager
經過以前註冊過的字典查找到對應的Class
返回給業務方,而後業務方再本身執行alloc
和init
方法獲得一個符合剛纔傳入@protocol
的對象,而後再執行相應的邏輯。
這裏的ModuleManager
其實跟以前的MGJRouter
同樣,是沒有任何須要去註冊協議和類名的。並且不管是服務提供者調用registerClass:forProtocol:
也好,服務的調用者調用classForProtocol:
,都必須依賴於同一個protocol。蘑菇街把全部的protocol放入了一個publicProtocol.h的文件中,所以調用方和響應方都必須依賴於同一個文件。這個我在文章中也論述過:響應方在提供服務的時候,是不須要依賴任何人的。
普通參數調用
和很是規參數調用
。不去區分遠程應用調用和本地組件間調用的缺陷我在文中已經論述過了,這裏很少說。
openURL
方式,還提供了ModuleManager
方式,然而所謂的咱們實際上是分爲「組件間調用」和「頁面間跳轉」兩個維度,只要 app 響應某個 URL,不管是 app 內仍是 app 外均可以,而「組件間」調用走的徹底是另外一條路,因此也不會有安全上的問題。
其實也是不成立的,由於openURL
方式也出如今了本地組件間調用中,這在他第一篇文章裏的組件間通訊
小節中就已經說了採用openURL
方式調用了,這是有可能產生安全問題的。並且這段話也認可了openURL
方式被用於本地組件間調用,又印證了我剛纔說的第一點。
openURL
場景下,仍是出現了以遠程調用的方式爲本地間調用提供服務
的問題,這個問題我也已經在文中論述過了。
openURL
方案和protocol - class
方案,因此其實以前我指出蘑菇街本地間調用不能傳遞很是規參數和複雜參數是不對的
,應該是蘑菇街在本地間調用時若是是普通參數,那就採用openURL
,若是是很是規參數,那就採用protocol - class
了,這個作法對於本地間調用的管理和維護,顯而易見是不利的。。。
必需要在 app 啓動時註冊 URL 響應者
這步不可避免,但沒有說緣由。個人demo已經證明了註冊是沒必要要的,因此我想聽聽limboy如何解釋緣由。
按照你的方案來看,紅圈的地方是不可能沒有依賴的。。。
認爲
category
在某種意義上也是一個註冊過程。
蘑菇街的註冊和我這裏的category實際上是兩回事,並且我不管如何也沒法理解把category和註冊URL等價聯繫的邏輯😂
一個很簡單的事實就能夠證實二者徹底不等價了:個人方案若是沒有category,照樣能夠跑,就是業務方調用醜陋一點。蘑菇街若是不註冊URL,整個流程就跑不起來了~
認爲openURL的好處是
能夠更少地關心業務邏輯
,本文方案的好處是能夠很方便地完成參數傳遞。
我沒以爲本文方案關心的業務邏輯比openURL
更多,由於二者比較起來,都是傳參數發調用請求,在關心業務邏輯
的條件下,二者徹底同樣。惟一的不一樣就是,我能傳很是規參數而openURL
不能。本文方案的整個過程當中,在調用者這一方是徹底沒有涉及到任何屬於響應者的業務邏輯的。
認爲
protocol/URL註冊
和將target-action抽象出調用接口
是等價的
這其實只是效果等價了,二者真正的區別在於:protocol對業務產生了侵入,且不符合黑盒模型。
因爲業務中的某個對象須要被調用,所以必需要符合某個可被調用的protocol,然而這個protocol又不存在於當前業務領域,因而當前業務就不得不依賴publicProtocol。這對於未來的業務遷移是有很是大的影響的。
蘑菇街的protocol方式使對象要在調用者處使用,因爲調用者並不包含對象本來所處的業務領域,當完成任務須要多個這樣的對象的時候,就須要屢次經過protocol得到class來實例化多個對象,最終才能完成需求。
可是target-action
模式保證了在執行組件間調用的響應時,執行的上下文處於響應者環境中,這跟蘑菇街的protocol方案相比就是最大的差異。由於從黑盒理論上講,調用者只管發起請求,請求的執行應該由響應者來負責,所以執行邏輯必須存在於響應者的上下文內,而不能存在於調用者的上下文內。
舉個具體一點的例子就是,當你發起了一個網頁請求,後端取好數據渲染好頁面,不管獲取數據涉及多少渠道,獲取數據的邏輯都在服務端完成,而後再返回給瀏覽器展現。這個是正確的作法,target-action模式也是這麼作的。
可是蘑菇街的方案就變成了這樣:你發起了一個網絡請求,後端返回的不是數據,返回的居然是一個數據獲取對象(DAO),而後你再經過DAO去取數據,去渲染頁面,若是渲染頁面的過程涉及多個DAO,那麼你還要再發起更多請求,拿到的仍是DAO,而後再拿這個DAO去獲取數據,而後渲染頁面。這是一種很是詭異的作法。。。
若是說這麼作是爲了應對執行業務的過程當中,須要根據中間階段的返回值來決定接下來的邏輯走向的話,那也應該是屢次調用得到數據,而後決定接下來的業務走向,而不是每次拿到的都是DAO啊。。。使用target-action方式來應對這種場景其實也很天然啊~