iOS架構設計解耦的嘗試之模組間通訊

一水發表於2017-01-11

iOS架構設計解耦的嘗試之模組間通訊

改系列文章是2016年折騰的一個總結,對於這一年中思考和解決的一些問題做一些梳理和總結。

前兩篇文章主要是說了業務邏輯介面還有模組化的事情。隨著系統內部邏輯單元(可能是模組,也可能是為了解耦拆解出來用來承載職責的類等常見的實現)的增多。勢必會引入另外的一個問題,就是邏輯單元之間的互動增加和邏輯單元之間通訊成本的提高。在iOS架構設計系列之解耦的嘗試之變異的MVVM,一文中我們在將整個業務邏輯層從MCV向MVVM演變的時候也遇到了這個問題,當時是本著作孽自己造輪子的心態,通過構建EventBus元件來解決。同樣,針對於邏輯單元之間通訊成本增加的問題,也需要尋找一個合適的解決方案。

問題場景描述

ios多模組管理一文中,描述一種進行系統模組拆解和管理的思路。將職能不同的業務,拆解成了獨立的模組。而且每個模組通過程式碼隔離,做到了互相之間影響的最小化。但是他們之間怎麼互動呢?換種說法就是業務模組應該暴漏什麼樣的外部介面,以方便其他業務模組來呼叫?

在終端上業務邏輯主要是圍繞著介面展開的。在iOS中的表現就是各式各樣的ViewController。而在以往的編碼實踐中,所謂業務模組間的互動就是VC之間的相互呼叫。我們常見的是這樣的:

UIViewController* aVC = [UIViewController new];
//配置aVC需要引數
....
[self.navigationController pushViewController:aVC animated:YES];複製程式碼

或者這樣的

UIViewController* aVC = [UIViewController new];
//配置aVC需要的引數
...
[self presentViewController:aVC animated:YES];複製程式碼

我們通過對於指定業務模組的程式碼級引用來呼叫對方的服務。而這種依賴屬於介面依賴,稍微符合介面隔離的設計。但不是一個符合迪米特法則(最小知識法則)的設計。呼叫方由於對於服務提供方有介面依賴,因而就造成了以下的潛在問題:

  1. 連結過程中必須引入服務提供方所在的模組,無法提供打包過程中動態下掉某些業務模組的需求。
  2. 服務提供方類介面變動,將直接呼叫方。
  3. 呼叫方知道了服務提供方的太多有冗餘資訊。

這些問題,我們通過程式碼級別的模組隔離基本上解決了。正如前文所說,你要真正把模組之間的互動影響降低的最小,最好的解決方案就是建造『資訊孤島』,而資訊孤島就會造成模組之間『雞犬之聲相聞老死不相往來』,這也非我等所願。他們之間還要保持一個最小的通訊,來完成服務的呼叫。這樣我們在程式碼隔離之後,就要解決兩個問題:

  1. 模組發現,就是說我一個模組怎麼被其他模組發現,或者說我一個模組做些什麼事情,外部模組使用的時候,才能知道有我的存在。
  2. 服務呼叫,模組做為服務提供方,需要能夠真實的提供所標稱的服務。

機制與策略分離

解決這兩個問題,我們首先要說一個觀點就是機制與策略分離。我們希望設計的是一整套能夠滿足上述要求的協議,其次才是實現,最後才是在我們的APP中的具體應用。這也是我這一年來的一個非常重要的總結。並且在逐漸開源出來的一些庫中也體現著這個設計。具體說一下,所謂機制即是抽象出來的規則,比如:

f(x)=x^2 x屬於R複製程式碼

所謂策略即是在具體場景中的應用,比如當x=2的時候:

f(2)=4 x=2複製程式碼

很明顯剛才說的三個層次中協議與實現做成了一個機制與策略分離。而實現與應用又組成了另外的一個機制與策略分離。我比較喜歡這種巢狀的解決方案,你解決了一個通用性的問題,然後巢狀使用,就能夠解決更多的問題,只需要付出少量的思維成本。

協議是問題解決方案的描述,或者說要解決這個問題大家都應該遵守的規則。就像網路的tcp協議,你要基於tcp通訊你就需要遵循這個協議。

實現是針對於某類環境的實施方案,比如linux上對於TCP的實現還有windows上對於TCP的實現。雖然都是一個協議,但是大家的實現方式不一樣,有基於c寫的,有基於c++寫的.

而應用是真對具體的問題域提出的實施方案,比如我們做了一個喲呵校園的聊天軟體使用了tcp進行socket通訊。

解決方案設計

模組間通訊協議URL

那我們首先要做的就是針對模組間通訊問題構思一個協議。一個為了解決模組間通訊問題大家都遵守的規則。其實關於這個問題在今年下半年,業界飄來一股router風。大家都在模組化之後的通訊問題上作出了不同的嘗試。而且甚至為此進行了一場部落格間的辯論。仔細分析一下,就能發現大家雖各有意見,但是基本上都同意使用URL的方案來解決這個問題。所爭執的不同在於實現方案上的差異。而此處的URL正是我們所謂的協議部分。

統一資源定位符(或稱統一資源定位器/定位地址、URL地址等[1],英語:Uniform / Universal Resource Locator,常縮寫為URL),有時也被俗稱為網頁地址(網址)。如同在網路上的門牌,是因特網上標準的資源的地址(Address)。它最初是由蒂姆·伯納斯-李發明用來作為全球資訊網的地址。現在它已經被全球資訊網聯盟編制為因特網標準RFC 1738。

為何如此?

回到最開始我們描述的問題中第一點: 模組發現。其實也就是模組這種資源的定位問題,這個和URL設計的初衷是不謀而合的。URL整套的設計思路就是在整體的網際網路中解決資訊孤島,讓各個資訊孤島之間能夠進行資源發現和資源呼叫而設計。而我們目前所要處理的模組間通訊問題,其實是這個巨集大問題域的一個子集。因而選用URL協議,是一個非常順理成章的事情。另外一點,這裡真心沒必要重新造一個類似於URL的協議的輪子出來的。URL協議中能夠非常完美的解決這個問題。

在iOS中基於URL協議的模組間通訊實現DZURLRoute

其實業界這個route的實現已經有千千萬萬了,為啥我還要再寫一個?一個是因為原有的一些庫的模型和我所想象的不吻合,一個是因為我實現不想削足適履去適配他們的模型。所以本著造輪子的作孽心態還是自己寫。其實也不是非常複雜。

URL協議解決了模組發現的問題,但是是個靜態的txt,並不具備exe的能力。我們可以通過定義一個類似於:

yoho://innerfuction/viewcontroller/showuserinfo?uid=22&xx=33複製程式碼

來讓一個模組對外宣稱支援顯示使用者詳細資訊的服務。但是我們要如何使用這個服務呢?很多Route的實現是通過URL直接將對應的ViewController返回,然後由呼叫方再去呼叫介面配置ViewController,而後呼叫方進行push或者present。而我認為這種方式不是很合理,做為呼叫發應該儘可能少的知道服務提供方的資訊。服務怎麼被彈起,應該是由服務方自己決定的,而不是呼叫方。最好只知道一個URL還有支援什麼樣的服務就好了,最好能把互動介面精簡、精簡、再精簡。

而思考了一下很多route庫之所以沒能夠做到模組只對外暴漏URL就可以的一個很重要的原因,就是在ViewController被彈出的時候,iOS需要一個調起的ViewController

UIViewController* aVC = [UIViewController new];
//配置aVC需要引數
....
[self.navigationController pushViewController:aVC animated:YES];複製程式碼

就是必須知道當前介面是在哪裡,你才能去push下一個介面。只有知道這個self.navigationController上下文資訊才行。所以很多事情只能在呼叫方來處理。我覺得這種方式制約著被呼叫方需要拿到服務提供方的一個例項才行。每一個問題背後都有一個解決方案。於是我在自己造的輪子中使用了全域性UI堆疊的方式解決了這個問題。

通過構造了被呼叫的上下文資訊類DZURLRequestContext,用於攜帶呼叫方的上下文資訊來解決這個問題。上下文資訊中攜帶了當前UI的堆疊資訊,能夠方便定位用哪個VC做為起點,來彈出下一個頁面。當然使用這個context還有另外一個原因就是因為URL中能夠傳輸的引數型別是受限的,只能傳輸NSString型別,對於一個例項則不能傳輸。為了傳輸例項引數也需要這樣的一個context環境。

這樣在呼叫一個頁面的服務的時候,就能夠做到如下所示極致簡單:

[[DZURLRoute defaultRoute] routeURL:DZURLRouteQueryLink(kYHURLSacnQRCode, @{})];複製程式碼

當然該庫首先是解決了通過URL呼叫的問題,而後才是上面說的這些優化問題。同時,也針對很多不同的應用場景提供瞭解決方案。更加具體的資訊可以參考DZURLRoute

而具體的應用問題,就是APP內部自己的事情了,不展開敘述。基本上都是呼叫庫介面的事情,沒有太多的表述價值。

Others

做個預告吧,也算是對自己的一個敦促,下一篇說一下在DZURLRoute中關於UI堆疊的問題是怎麼解決的。

歡迎關注iOS開發公共賬號 iOS開發知識 :掃描下方二維碼關注

iOS架構設計解耦的嘗試之模組間通訊

版權宣告:轉載請保留作者資訊,惡意轉載作者將保留追究法律責任的權利。BY:yishuiliunian

相關文章