Minya 分層框架實現的思考(一):依賴轉移

知識小集發表於2018-04-13

Minya 框架是我們團隊之前構建的一套分層框架,在一個內部專案上驗證了一下。Minya 只是專案名稱,取自貢嘎雪山的英文名。這套分層框架是在 MVCS 的基礎上進行了改造。後來團隊轉向 RAC + MVVM,就擱置起來,沒有再用。之前一直想整理一下,將一些基礎的思想寫出來,供大家參考。然後就一直拖到現在。我後期也反思過,這個框架還存在不少問題,也會在這個系列裡也會詳細說明。

Minya 之前已開源,並放到 Github https://github.com/southpeak/Minya 上,有興趣的話可以給個 Star。不過專案已停更,後期不再維護。

我們在考量一個 App 分層框架的設計和使用時,主要會基於以下幾個原則:

  • 低耦合;
  • 單一職責,各層各司其職;
  • 可複用;
  • 結構清晰、程式碼均分;

另外,我們程式的執行主要是對資料做各種各樣的操作,例如從服務端獲取資料顯示給使用者,或者從使用者輸入中收集資料傳送到服務端。因此我們可以從資料的角度來考慮整個框架的設計。

資料的流向有兩個維度:縱向橫向。縱向是從垂直分層的角度來考慮,資料在 View服務端/本地儲存 之間傳輸;橫向是從水平角度來考慮,資料從一個 ViewController 流向另一個 ViewController,或者從一個模組流向另一個模組。這一系列文章將主要從縱向資料流這個維度來考慮分層框架的設計。同時將一步步繪製我們的框架圖,並結合程式碼,來說明 Minya 框架的設計、使用和存在的問題。整個系列基本分為三個部分:

  • 轉移依賴
  • 構建依賴和資料通訊
  • 問題

如果有閒情,可以賴著性子慢慢看。

MVCS

傳統的 MVC 可以滿足 App 開發的基本需求,但在實際的開發中,經常因為各種原因,導致 ViewController 層的程式碼過於龐雜,同時各層的職責不明確,最終影響到專案的維護。這也是業界討論的一個熱點:如何輕量化 ViewController。為此衍生出各種實用的框架,現在流行度比較高的應該是 MVVM。我們在最初考慮的時候用了 MVCS 框架,主要目的有以下幾個方面:

  • 明確每一層的職責;
  • 輕量化 ViewController,讓其成為資料的中轉站;
  • 將業務處理獨立於一層,並可根據實際情況將業務細分到不同的類中進行處理;
  • 均分程式碼,確保一個類或檔案的程式碼量不會太大。

這一框架基本結構圖及依賴關係如下:

圖1:MVCS 基本結構圖

對於每一層的職責,我們的基本考慮是:

  • Model 層:主要是用於表示資料,可以將其視為資料載體。在這一層中不做具體的業務處理,但可以執行一些簡單的資料處理任務。即為 瘦Model 模型。在上圖中沒有表示出來;
  • View 層:即展示層,主要是將資料展現給使用者,同時從使用者輸入中獲取資料;
  • ViewController 層:該層將只作為 View 層的容器,以及資料的中轉結點。這種中轉包括縱向資料和橫向資料的傳輸;
  • Store 層:業務處理層,所有的業務在這一層中來處理,這一層將只提供介面供上一層使用;
  • 資料訪問層:這一層主要面向服務端和本地資料,即圖中的 ServiceStorage,這一層不是我們的重點,在文中沒有過多的討論;

我們將下面三個部件統一為 Store,即 MVCS 中的 S 實際上包含了三個主要部件:StoreServiceStorage

職責明確好了,我們就需要考慮資料如何在各層之間傳輸了。資料在物件之間的傳輸主要有幾種方式:

  • 方法呼叫
  • KVO
  • Delegate
  • Notification
  • callback

如果在圖1的基礎上加上資料傳輸方式,結構圖看起來可能是下面這樣的(注意箭頭方向):

圖2:資料傳輸方式

圖中資料的傳輸,從上到下是 View 層通過 Delegate 傳遞 ViewControllerViewController 再通過方法呼叫將傳遞給 Store 做業務處理;從下到上是 Store 通過 callback 將資料回傳給 ViewControllerViewController再通過方法呼叫傳遞給 View

這裡有一些痛點:

  1. ViewControllerViewStore 是強依賴,需要知道兩者的很多資訊。如果處理不好,ViewController 所需要做的工作依然很多;
  2. ViewController 作為 View 層的 Delegate,實際上是讓 View 層也依賴於 ViewController
  3. View 層如果檢視層級很深,需要通過層層代理將資料傳出;

因此,我們最初的想法是:有沒有一種方式,降低 ViewControllerViewStore 的依賴,只需要知道兩者少量的介面,就能完成這種資料傳輸。同時,能避開 Delegate 這種資料傳遞方式呢?

Notification 可以實現這種需求,甚至可以直接跳過 ViewController 來傳遞資料,但這種方式顯然是不適合的。Notification 廣播的屬性決定了我們沒辦法將其控制在一個頁面內。

KVO 呢?我們決定嘗試一下。

KVO

蘋果爸爸自身提供的 KVO 是開發者的一大槽點,在這不用過多解釋。為了優化 KVO 糟糕的設計,社群有很多框架都對其進行了封裝,以提供一套更友好的 KVO 介面,像 FacebookKVOController,還有 Reactive CocoaRACObserve。在此,我們不去說明各種方案的優劣,只說結果:我們選擇了 Reactive CocoaKVO,並做了部分改造。

依賴轉移 和 Pipeline

有了改造後的 KVO 這一強大的工具,我們就可以嘗試改造一下資料傳輸方式了。改造後的圖如下所示:

圖3:KVO 改造後的資料傳輸方式

這裡的變化並不大,主要是 ViewController 通過 KVO 監聽 ViewStore 的屬性變化,然後來獲取並傳遞資料。所以對實質的問題並沒有改進的地方,ViewController 依然需要了解 ViewStore 很多資訊,同時也並沒有規避 View 層到 ViewController 資料層層傳遞的問題,所以這種改造沒有太大意義。

問題的癥結還是在於資料的傳輸必須經過 ViewController,這樣意味著 ViewController 必須強依賴於 ViewStore。那能不能讓資料繞過 ViewController ,通過其它方式在 ViewStore 之間傳輸呢?可能會有三種方案考慮:

  1. 通過 Notification 傳送接收通知的方式來傳輸資料;
  2. View 層和 Store 層之間建立依賴關係,直接傳輸;
  3. 建立一個第三方物件,讓 ViewStore 都依賴於這個物件,通過這個物件來傳輸資料;

第一種方案我們上面已經說過,Notification 可能會導致失控;第二種方案更不可行,我們使用 MVC 及各種衍生框架,就是為了讓 View業務層 相分離。所以,我們嘗試了第三種方案,建立一個第三方物件。

換一個角度來考慮我們的分層框架,其實每一層都是在處理各種資料,根據所需的資料及其變化執行相應的操作,所以對資料的依賴是強需求。每一層只需要關心自己特定的資料,就能完成各自的職責。比如說 View 層只要有了列表資料,就可以展示一個 TableViewViewController 只要有了下一個 ViewController 所需要的資料,就可以跳轉到下一個頁面;Store 層有了使用者名稱和密碼,就可以發起登入請求。那麼我們是不是可以構建一個 資料物件,這個資料物件包含 ViewViewControllerStore 各層所需要的所有資料,讓這三層都依賴於這個資料物件,然後各層按需從這個資料物件獲取資料呢?基於這種考慮,我們引入了 Pipeline 物件。

  • Pipeline:可以理解為資料管道,也可以理解為資料集散中心。它負責組織一個頁面分層層級所需要的所有資料。負責資料在 View 層與 Store 層之間的傳輸。實際上是一個資料物件,但不同於 Model 的職責,它主要負責資料傳輸,而不是資料表示。

注:這裡只是借用了管道這個名稱。和 Linux 中的管道不是一回事。

引入 Pipeline 後,我們的結構圖就可以變成下面這種形式:

圖4:MVCS + Pipeline

藉助於 PipelineKVO,我們就可以讓 View 層與 Store 層的資料傳輸繞開 ViewController ,通過這個資料管道來傳輸。以登入操作為例:使用者在介面上輸入 usernamepassword 後,點選登入按鈕後,將這兩個資料丟到 Pipeline 裡面,同時丟到 Pipeline 裡面的還有一個點選按鈕標識 flag (注意這裡有一個先後順序)。Store 層監聽 Pipeline 物件的 flag 屬性,發現其改變後,從 Pipeline 裡面取出 usernamepassword,發起登入請求,這個資料流轉的路徑如下:

圖5:pipeline 資料傳輸路徑

從示意圖可以看到,這次資料傳輸和 ViewController 基本上沒有啥關係。而且如果 Pipeline 設計的好的話,View 層每個檢視層級的資料可以直接丟到 Pipeline 中進行傳輸,而不需要層層上傳到外層檢視物件上。

通過這種依賴轉移,我們就可以弱化 ViewControllerView 層和 Store 層的依賴(注意是弱化,而不是消除,圖4中我們通過虛線表示這種弱依賴關係),View 層和 Store 層只需要提供少量的介面,就可以讓資料在三層間進行傳輸。

注:ViewController 本身也可能需要一些資料來執行某些操作,所以也可以依賴 Pipeline 並從中獲取資料。

Minya 框架中,我們宣告瞭一個 ViewController 的基類 MIViewController。在這個類中,我們構造了 ViewViewControllerStore 之間的弱依賴關係。我們先來看看這個類的主要程式碼:

MIViewController.h

@interface MIViewController : UIViewController

@property (nonatomic, strong, readonly, nonnull) id<MIStore> store;          //!< Store for the business logic
- (instancetype _Nullable)initWithStore:(id<MIStore> _Nonnull)store viewClass:(Class _Nonnull)viewClass callback:(MICallback _Nullable)callback;
- (instancetype _Nullable)initWithStore:(id<MIStore> _Nonnull)store viewClass:(Class _Nonnull)viewClass;

@end
複製程式碼

MIViewController.m

// MIViewController.m
@interface MIViewController ()

@property (nonatomic, assign) Class viewClass;                  //!< Container view class
@property (nonatomic, copy) MICallback callback;                //!< Callback for the previous ViewController

@end

#pragma mark - MIViewController implementation

@implementation MIViewController

#pragma mark - Life Cycle

- (instancetype)initWithStore:(id<MIStore>)store viewClass:(Class)viewClass {
    
    return [self initWithStore:store viewClass:viewClass callback:nil];
}

- (instancetype)initWithStore:(id<MIStore>)store viewClass:(Class)viewClass callback:(MICallback)callback {
    
    NSParameterAssert(store);
    NSAssert([viewClass isSubclassOfClass:[UIView class]], @"viewClass should be subclass of UIView");
    
    self = [super init];
    
    if (self) {
        
        _store = store;
        _viewClass = viewClass;
        _callback = [callback copy];
    }
    
    return self;
}

- (void)loadView {
    [super loadView];
    
    // 重設 ViewController 的根 View
    self.view = [[self.viewClass alloc] init];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Set up pipeline
    [self setupPipeline:self.store.pipeline];
    [self.view setupPipeline:self.store.pipeline];
    
    // Add observers of the pipeline data.
    [self addObservers];
}

// ...

@end
複製程式碼

在這個基類中,我們通過依賴注入(Dependency Injection)的方式將 ViewController 所依賴的 Store 物件及 View 檢視類傳進來,在 -loadView 中建立檢視物件並將其作為根檢視。而 ViewControllerStore 的瞭解僅限於 store.pipeline 屬性(以及一個發起資料請求的 -fetchData 方法),對 View 的瞭解僅限於 viewsetupPipeline 方法,除此之外 ViewController 對兩者一無所知。

通過這種處理,三層之間的關係可以滿足設計模式的一些基本原則。

這裡需要明確一下 pipeline 的職責:

  • 維護各層所需要的資料,這種資料包括需要展示的業務資料、使用者輸入資料、甚至於用於標識使用者點選、資料變更的標識資料;
  • 負責資料在各層之前的傳輸;
  • 不處理任何業務邏輯;

小結

在這篇文章中,我們主要從理論方面描述了通過引入 pipeline 來構建 MVCS + Pipeline 分層框架,從而降低 ViewControllerView 層及 Store 層的依賴。需要注意的是,這裡並沒有消除依賴,而只是將依賴轉移了。通過這種轉移,我們能讓各層更專注地處理自己的任務。

在下一篇中,我們會通過例項來介紹如何去構建這種依賴,如何去構造 pipeline 以及資料如何通過 pipeline 來傳輸。通過例項,可以給出一個更直觀的認識。

Minya 分層框架實現的思考(二):構建依賴及資料傳輸

Minya 分層框架實現的思考(三):問題

知識小集公眾號

知識小集是一個團隊公眾號,每週都會有原創文章分享,我們的文章都會在公眾號首發。知識小集微信群,短短几周時間,目前群友已經300+人,很快就要達到上限(抓住機會哦),關注公眾號獲取加群方式。

Minya 分層框架實現的思考(一):依賴轉移

相關文章