Minya
框架是我們團隊之前構建的一套分層框架,在一個內部專案上驗證了一下。Minya
只是專案名稱,取自貢嘎雪山的英文名。這套分層框架是在MVCS
的基礎上進行了改造。後來團隊轉向RAC + MVVM
,就擱置起來,沒有再用。之前一直想整理一下,將一些基礎的思想寫出來,供大家參考。然後就一直拖到現在。我後期也反思過,這個框架還存在不少問題,也會在這個系列裡也會詳細說明。
Minya
之前已開源,並放到 Githubhttps://github.com/southpeak/Minya
上,有興趣的話可以給個Star
。不過專案已停更,後期不再維護。
我們在考量一個 App
分層框架的設計和使用時,主要會基於以下幾個原則:
- 低耦合;
- 單一職責,各層各司其職;
- 可複用;
- 結構清晰、程式碼均分;
另外,我們程式的執行主要是對資料做各種各樣的操作,例如從服務端獲取資料顯示給使用者,或者從使用者輸入中收集資料傳送到服務端。因此我們可以從資料的角度來考慮整個框架的設計。
資料的流向有兩個維度:縱向 和 橫向。縱向是從垂直分層的角度來考慮,資料在 View
層 和 服務端/本地儲存 之間傳輸;橫向是從水平角度來考慮,資料從一個 ViewController
流向另一個 ViewController
,或者從一個模組流向另一個模組。這一系列文章將主要從縱向資料流這個維度來考慮分層框架的設計。同時將一步步繪製我們的框架圖,並結合程式碼,來說明 Minya
框架的設計、使用和存在的問題。整個系列基本分為三個部分:
- 轉移依賴
- 構建依賴和資料通訊
- 問題
如果有閒情,可以賴著性子慢慢看。
MVCS
傳統的 MVC
可以滿足 App
開發的基本需求,但在實際的開發中,經常因為各種原因,導致 ViewController
層的程式碼過於龐雜,同時各層的職責不明確,最終影響到專案的維護。這也是業界討論的一個熱點:如何輕量化 ViewController
。為此衍生出各種實用的框架,現在流行度比較高的應該是 MVVM
。我們在最初考慮的時候用了 MVCS
框架,主要目的有以下幾個方面:
- 明確每一層的職責;
- 輕量化
ViewController
,讓其成為資料的中轉站; - 將業務處理獨立於一層,並可根據實際情況將業務細分到不同的類中進行處理;
- 均分程式碼,確保一個類或檔案的程式碼量不會太大。
這一框架基本結構圖及依賴關係如下:
對於每一層的職責,我們的基本考慮是:
Model
層:主要是用於表示資料,可以將其視為資料載體。在這一層中不做具體的業務處理,但可以執行一些簡單的資料處理任務。即為 瘦Model 模型。在上圖中沒有表示出來;View
層:即展示層,主要是將資料展現給使用者,同時從使用者輸入中獲取資料;ViewController
層:該層將只作為View
層的容器,以及資料的中轉結點。這種中轉包括縱向資料和橫向資料的傳輸;Store
層:業務處理層,所有的業務在這一層中來處理,這一層將只提供介面供上一層使用;資料訪問層
:這一層主要面向服務端和本地資料,即圖中的Service
和Storage
,這一層不是我們的重點,在文中沒有過多的討論;
我們將下面三個部件統一為
Store
,即MVCS
中的S
實際上包含了三個主要部件:Store
、Service
、Storage
。
職責明確好了,我們就需要考慮資料如何在各層之間傳輸了。資料在物件之間的傳輸主要有幾種方式:
- 方法呼叫
- KVO
- Delegate
- Notification
- callback
如果在圖1的基礎上加上資料傳輸方式,結構圖看起來可能是下面這樣的(注意箭頭方向):
圖中資料的傳輸,從上到下是 View
層通過 Delegate
傳遞 ViewController
,ViewController
再通過方法呼叫將傳遞給 Store
做業務處理;從下到上是 Store
通過 callback
將資料回傳給 ViewController
,ViewController
再通過方法呼叫傳遞給 View
。
這裡有一些痛點:
ViewController
對View
和Store
是強依賴,需要知道兩者的很多資訊。如果處理不好,ViewController
所需要做的工作依然很多;ViewController
作為View
層的Delegate
,實際上是讓View
層也依賴於ViewController
;View
層如果檢視層級很深,需要通過層層代理將資料傳出;
因此,我們最初的想法是:有沒有一種方式,降低 ViewController
對 View
和 Store
的依賴,只需要知道兩者少量的介面,就能完成這種資料傳輸。同時,能避開 Delegate
這種資料傳遞方式呢?
Notification
可以實現這種需求,甚至可以直接跳過 ViewController
來傳遞資料,但這種方式顯然是不適合的。Notification
廣播的屬性決定了我們沒辦法將其控制在一個頁面內。
那 KVO
呢?我們決定嘗試一下。
KVO
蘋果爸爸自身提供的 KVO
是開發者的一大槽點,在這不用過多解釋。為了優化 KVO
糟糕的設計,社群有很多框架都對其進行了封裝,以提供一套更友好的 KVO
介面,像 Facebook
的 KVOController
,還有 Reactive Cocoa
的 RACObserve
。在此,我們不去說明各種方案的優劣,只說結果:我們選擇了 Reactive Cocoa
的 KVO
,並做了部分改造。
依賴轉移 和 Pipeline
有了改造後的 KVO
這一強大的工具,我們就可以嘗試改造一下資料傳輸方式了。改造後的圖如下所示:
這裡的變化並不大,主要是 ViewController
通過 KVO
監聽 View
和 Store
的屬性變化,然後來獲取並傳遞資料。所以對實質的問題並沒有改進的地方,ViewController
依然需要了解 View
和 Store
很多資訊,同時也並沒有規避 View
層到 ViewController
資料層層傳遞的問題,所以這種改造沒有太大意義。
問題的癥結還是在於資料的傳輸必須經過 ViewController
,這樣意味著 ViewController
必須強依賴於 View
和 Store
。那能不能讓資料繞過 ViewController
,通過其它方式在 View
和 Store
之間傳輸呢?可能會有三種方案考慮:
- 通過
Notification
傳送接收通知的方式來傳輸資料; - 在
View
層和Store
層之間建立依賴關係,直接傳輸; - 建立一個第三方物件,讓
View
和Store
都依賴於這個物件,通過這個物件來傳輸資料;
第一種方案我們上面已經說過,Notification
可能會導致失控;第二種方案更不可行,我們使用 MVC
及各種衍生框架,就是為了讓 View
層 和 業務層 相分離。所以,我們嘗試了第三種方案,建立一個第三方物件。
換一個角度來考慮我們的分層框架,其實每一層都是在處理各種資料,根據所需的資料及其變化執行相應的操作,所以對資料的依賴是強需求。每一層只需要關心自己特定的資料,就能完成各自的職責。比如說 View
層只要有了列表資料,就可以展示一個 TableView
; ViewController
只要有了下一個 ViewController
所需要的資料,就可以跳轉到下一個頁面;Store
層有了使用者名稱和密碼,就可以發起登入請求。那麼我們是不是可以構建一個 資料物件,這個資料物件包含 View
、ViewController
和 Store
各層所需要的所有資料,讓這三層都依賴於這個資料物件,然後各層按需從這個資料物件獲取資料呢?基於這種考慮,我們引入了 Pipeline
物件。
Pipeline
:可以理解為資料管道,也可以理解為資料集散中心。它負責組織一個頁面分層層級所需要的所有資料。負責資料在View
層與Store
層之間的傳輸。實際上是一個資料物件,但不同於Model
的職責,它主要負責資料傳輸,而不是資料表示。
注:這裡只是借用了管道這個名稱。和
Linux
中的管道不是一回事。
引入 Pipeline
後,我們的結構圖就可以變成下面這種形式:
藉助於 Pipeline
和 KVO
,我們就可以讓 View
層與 Store
層的資料傳輸繞開 ViewController
,通過這個資料管道來傳輸。以登入操作為例:使用者在介面上輸入 username
和 password
後,點選登入按鈕後,將這兩個資料丟到 Pipeline
裡面,同時丟到 Pipeline
裡面的還有一個點選按鈕標識 flag
(注意這裡有一個先後順序)。Store
層監聽 Pipeline
物件的 flag
屬性,發現其改變後,從 Pipeline
裡面取出 username
和 password
,發起登入請求,這個資料流轉的路徑如下:
從示意圖可以看到,這次資料傳輸和 ViewController
基本上沒有啥關係。而且如果 Pipeline
設計的好的話,View
層每個檢視層級的資料可以直接丟到 Pipeline
中進行傳輸,而不需要層層上傳到外層檢視物件上。
通過這種依賴轉移,我們就可以弱化 ViewController
對 View
層和 Store
層的依賴(注意是弱化,而不是消除,圖4中我們通過虛線表示這種弱依賴關係),View
層和 Store
層只需要提供少量的介面,就可以讓資料在三層間進行傳輸。
注:
ViewController
本身也可能需要一些資料來執行某些操作,所以也可以依賴Pipeline
並從中獲取資料。
在 Minya
框架中,我們宣告瞭一個 ViewController
的基類 MIViewController
。在這個類中,我們構造了 View
、ViewController
和 Store
之間的弱依賴關係。我們先來看看這個類的主要程式碼:
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
中建立檢視物件並將其作為根檢視。而 ViewController
對 Store
的瞭解僅限於 store.pipeline
屬性(以及一個發起資料請求的 -fetchData
方法),對 View
的瞭解僅限於 view
的setupPipeline
方法,除此之外 ViewController
對兩者一無所知。
通過這種處理,三層之間的關係可以滿足設計模式的一些基本原則。
這裡需要明確一下 pipeline
的職責:
- 維護各層所需要的資料,這種資料包括需要展示的業務資料、使用者輸入資料、甚至於用於標識使用者點選、資料變更的標識資料;
- 負責資料在各層之前的傳輸;
- 不處理任何業務邏輯;
小結
在這篇文章中,我們主要從理論方面描述了通過引入 pipeline
來構建 MVCS + Pipeline
分層框架,從而降低 ViewController
對 View
層及 Store
層的依賴。需要注意的是,這裡並沒有消除依賴,而只是將依賴轉移了。通過這種轉移,我們能讓各層更專注地處理自己的任務。
在下一篇中,我們會通過例項來介紹如何去構建這種依賴,如何去構造 pipeline
以及資料如何通過 pipeline
來傳輸。通過例項,可以給出一個更直觀的認識。
知識小集公眾號
知識小集是一個團隊公眾號,每週都會有原創文章分享,我們的文章都會在公眾號首發。知識小集微信群,短短几周時間,目前群友已經300+人,很快就要達到上限(抓住機會哦),關注公眾號獲取加群方式。