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

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

上一篇我們主要從理論上講述如何通過 轉移依賴 來輕量化我們的 ViewController,同時在 View 層和 Store 層之間傳輸資料。在這一篇中,我們將通過 Demo 來更清晰地描述 Minya 框架的實際操作,包括如何去構造 pipeline,如何去構建三層物件對 pipeline 的依賴,以及資料如何通訊。

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

Demo 的基本效果可檢視公眾號(這裡無法上傳視訊)。

這裡主要展示了兩個頁面,其對應的 ViewController 分別是 SearchViewControllerInterestingnessViewController。後面的示例主要以這兩個類為主。

由於介面是基於 Frickr 的,所以如果想執行起來,需要去 Frickr 上註冊一個應用,以獲取 API KEY 和金鑰,並填充到 AppDelegate.m 的下面一行程式碼中。(另外還請自備梯子)

[[FlickrKit sharedFlickrKit] initializeWithAPIKey:@"This is your API key" sharedSecret:@"This is the secret"];
複製程式碼

Scene

在進入主題之前,先來了解一下 Scene 物件。我們將一個頁面描述為一個 Scene 物件,其程式碼如下:

@interface MIScene : NSObject

@property (nonatomic, copy, nonnull) NSString *viewName;             //!< view name
@property (nonatomic, copy, nonnull) NSString *controllerName;       //!< controller name
@property (nonatomic, copy, nonnull) NSString *storeName;            //!< store name

+ (instancetype _Nullable)sceneWithView:(NSString * _Nonnull)viewName controller:(NSString * _Nonnull)controllerName store:(NSString * _Nonnull)storeName;

@end
複製程式碼

這個類很簡單,只是用來組合一個頁面的三層物件。這個類並不是必須的,只是為了表達清晰。注意這裡三個屬性的型別都是 NSString,意味著我們將通過反射機制來建立一個 ViewController 物件及其關聯的 StoreView(同時也意味了更多的硬編碼,我們會在後面說明)。

具體的建立過程在 MIMediator 中,程式碼如下所示:

- (UIViewController *)viewControllerWithScene:(MIScene *)scene context:(NSDictionary<NSString *,id> *)context callback:(MICallback)callback {
    
    Class controllerClass = NSClassFromString(scene.controllerName);
    Class storeClass = NSClassFromString(scene.storeName);
    Class viewClass = NSClassFromString(scene.viewName);
    
    id<MIStore> store = [[storeClass alloc] initWithContext:context];
    
    return [[controllerClass alloc] initWithStore:store viewClass:viewClass callback:callback];
}
複製程式碼

MIMediator 是頁面跳轉的一箇中介者,主要是負責橫向資料流操作,在這不多解釋。

因此,建立及使用一個 Scene 看起來是下面這樣:

MIScene *scene = [MIScene sceneWithView:@"SearchView" controller:@"SearchViewController" store:@"SearchStore"];
    UIViewController *viewController = [[MIMediator sharedMediator] viewControllerWithScene:scene context:nil];
    UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:viewController];
複製程式碼

正如上一篇後面所說,ViewControllerView 層和 Store 層的瞭解僅限於一兩個介面,而三層同時依賴於同一個 pipeline,這就意味著如果兩個 View 都依賴於同一個 pipeline,那麼這兩個 View 可以相互替換,同理 ViewControllerStore 也一樣。這樣,我們就可以根據 pipeline 來拼裝三層物件。即如果 View 調整了,但整體展示的資料還是那些,那我們的 ViewControllerStore 都不需要變動,在建立 Scene 時,我們換一個 View 名就行了。(當然這是一種理想狀態)。

構造 pipeline

由於 ViewViewControllerStore 都是依賴於 pipeline,所以 pipeline 可以說是整個框架的核心。如何去構造 pipeline,將決定整個結構的靈活性。可以從兩個角度來考慮這個問題:

  1. 根據 View 層來構造 pipeline

對於一個 App 來說,View 層是和使用者直接互動的,它即是使用者輸入資料的來源,也是業務資料的表述。採集和顯示哪些資料,都需要根據 View 來確定。另外,一個複雜的 View 層可能由多個甚至多層子 View 構成,每個子 View 有不同的資料需求,所以它能更精細地去表述資料,包括頁面點選這種 flag 資料。因此,我們可以根據檢視的樹形結構,構造一棵類似的 pipeline 樹形結構。如下圖所示:

圖1:根據檢視層級結構來構建 pipeline

這種方案還有一個好處,每一個子檢視只需要依賴於其對應的 pipeline 即可,而不需要依賴於整個 pipeline

不過這種方案的問題在於,一旦 View 變更,將直接影響到 pipeline 的構造,進而可能影響 ViewControllerStorepipeline 屬性的監聽。

  1. 根據業務來構造 pipeline

這種方案是把業務相關的一組資料整合在一個 pipeline 物件裡面(一個業務可能對應多個 View),再把一個頁面裡面的多個 pipeline 組織成一棵 pipeline 樹。

這種方式的優點是 pipeline 相對獨立於 View層,除了一些 View 相關的資料外,pipeline 不會受 View 過多的影響。缺點是這種 pipeline 對資料的表述比較粗曠,View 層可以監聽到一些與其無關的資料。

在實際開發過程中,可以根據實際情況來確定使用哪種方案構造 pipeline。構造 pipeline 的主要任務在 store 層中完成,因為這裡是資料的處理中心。以 InterestingnessStore 為例,我們將 InterestingnessStore 的業務邏輯拆分到兩個 store 中,每個 store 維護其自身的 pipeline,然後在 InterestingnessStore 中構建起 pipeline 的層級結構。

InterestingnessPipeline.h

@interface InterestingnessPipeline : MIPipeline

@property (nonatomic, strong) TopImagePipeline *imagePipeline;
@property (nonatomic, strong) PhotoListPipeline *photoListPipeline;

- (void)setShowImageAtIndex:(NSUInteger)index;

@end
複製程式碼

InterestingnessStore.m

@interface InterestingnessStore ()

@property (nonatomic, strong) InterestingnessPipeline *interestPipeline;    // Pipeline

@property (nonatomic, strong) TopImageStore *imageStore;                    // Top image store
@property (nonatomic, strong) PhotoListStore *photoStore;                   // Photo List store

@end

@implementation InterestingnessStore

// ...

- (InterestingnessPipeline *)interestPipeline {
    if (!_interestPipeline) {
        _interestPipeline = [[InterestingnessPipeline alloc] init];
        
        _interestPipeline.imagePipeline = self.imageStore.imagePipeline;
        _interestPipeline.photoListPipeline = self.photoStore.photoListPipeline;
    }
    return _interestPipeline;
}

@end
複製程式碼

以上 pipeline 層級結構與 View 的層級結構對應,即我們採用方案一來構造 pipeline

建立依賴

store 層中根據需求構造好 pipeline 後,就需要建立 ViewControllerView 層對 pipeline 的依賴了,這個過程並不複雜,但是比較繁瑣。這一操作主要是通過 ViewController 向上分發 pipeline 來完成的。我們再來看看 MIViewController 的實現。


@implementation MIViewController

// ...

- (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

複製程式碼

- viewDidLoad 方法裡面呼叫了兩個 - setupPipeline

  • 第一個是 ViewController 自身的方法,這可以構建 ViewControllerpipeline 的依賴,當然如果 ViewControllerpipeline 沒有資料需求,則子類可以不實現這個方法;
  • 第二個是 ViewController 的根檢視的設定方法,這個方法將 pipeline 傳遞給 View 層,View 層可以根據實際需要再去設定自身的 pipeline,以及將子 pipeline 分發各子 View,例如 InterestingnessView 的實現:
@interface InterestingnessView ()

@property (nonatomic, strong) InterestingnessPipeline *pipeline;

@property (nonatomic, strong) TopImageView *topImageView;
@property (nonatomic, strong) PhotoListView *photoListView;

@end

#pragma mark - InterestingnessView implementation

@implementation InterestingnessView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self addSubview:self.topImageView];
        [self addSubview:self.photoListView];
        
        // ...
    }
    return self;
}

- (void)setupPipeline:(__kindof MIPipeline *)pipeline {
    self.pipeline = pipeline;
    
    // 分發二級 pipeline
    [self.topImageView setupPipeline:self.pipeline.imagePipeline];
    [self.photoListView setupPipeline:self.pipeline.photoListPipeline];
}

// ...

@end
複製程式碼

至此,ViewControllerViewpipeline 的依賴關係構建完成。構建完成後,各層就只需要與 pipeline 打交道了,從 pipeline 中讀取資料,或者把資料寫入 pipeline 中。

資料傳輸

最後來看看最主要的部分:資料傳輸。回到最上面的 Demo,我們在第二個頁面中點選列表中的一個單元格,頂部的圖片資訊就跟著變化。我們來看看這種變化時如何產生的。我們先從目標檢視即 TopImageView 說起,這個 View 監聽了 TopImagePipelineurl 屬性:

// Observe `url` property of the pipeline
    [MIObserve(self.pipeline, url) changed:^(id  _Nonnull newValue) {
        @strongify(self)
        [self mi_updateView];
    }];
複製程式碼

也就是說,只要這個 url 發生了改變,TopImageView 就能監聽到這種改變並相應地去更新檢視,而至於是誰觸發了這種變更,TopImageView 並不關心,同時 TopImageView 並沒有提供額外的介面讓父檢視或者 ViewController 來呼叫。

這裡有一點需要行說明一下,Flickrflickr.photos.getRecent 介面(獲取列表)並沒有返回圖片的 url,所以我們需要通過圖片的 photoID,再去呼叫一次 flickr.photos.getSizes 介面,以獲取圖片 url,於是便有了這邊繁瑣的流程。正好也根據這個示例來說明問題。

我們再來看看事件的發起源 photoListView,在 PhotoListView 中,選中 table view 的操作只有一行程式碼:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    self.pipeline.inputSelectedPhotoIndex = indexPath.row;
}
複製程式碼

然後,這裡 PhotoListView 只是簡單的設定了它的 pipelineinputSelectedPhotoIndex 屬性,然後就沒有任何操作了。也就是說 PhotoListView 僅僅重新設定了所選的行索引,後面如何處理便和 PhotoListView 沒有關係了。

額,然後好像就斷線了。兩者是怎麼連線起來的呢?我們來看看誰監聽了 inputSelectedPhotoIndex 屬性。全域性搜尋,我們可以看到 InterestingnessStore 物件監聽了 inputSelectedPhotoIndex 屬性:

[MIObserve(self.interestPipeline.photoListPipeline, inputSelectedPhotoIndex) changed:^(NSNumber * _Nonnull newValue) {
        
    @strongify(self)
    [self.interestPipeline setShowImageAtIndex:[newValue integerValue]];
}];
複製程式碼

InterestingnessPipelinesetShowImageAtIndex 方法(這個方法只做簡單的處理)中,重新設定了其子 imagePipelinephotoID 屬性。

然後 TopImageStore 監聽了 imagePipelinephotoID 屬性,

[MIObserve(self.imagePipeline, photoID) changed:^(NSString * _Nonnull newValue) {
    @strongify(self)
    [self fetchData];
}];
複製程式碼

並依此發起網路請求,最終獲得圖片的 url

- (void)fetchData {
    
    // ...
    
    @weakify(self)
    [self.getSizesService requestWithParameters:@{@"photo_id": self.imagePipeline.photoID} success:^(NSString * _Nullable data) {
        @strongify(self)
        self.imagePipeline.url = data;
    } fail:^(id  _Nullable data, NSError * _Nullable error) {
        // You can do something if the request failed.
    }];
}
複製程式碼

在此更新 imagePipelineurl 後,TopImageView 監聽到這一變更,並從 imagePipeline 中獲取到 url 後,去載入圖片。至此整個流程打通。我們畫冊個簡單的圖來梳理一下流程。

圖2:資料流

整個過程中,每個物件只關心自己需要監聽 pipeline 中哪些屬性並做出相應的操作,以及執行某些操作後去修改 pipeline 對應的屬性,而不必去理會其它事情,專心做好自己的事實就好。不過這種特性也帶來一些不好的後果,你可能已經發現,在後一篇中也會講到這個問題。

小結

在這一篇中,我們結合 Demo 實踐瞭如何去構造 pipeline,建立三層物件對 pipeline 的依賴,以及建立依賴後,如何實現各層之間的資料通訊。實際上整個流程並不複雜,只是第一次看可能有些地方不太好理解。

由於這套框架並沒有太多的實戰,所以更多的是一個實驗性的不成熟框架。其中還有一些細節,由於篇幅限制,在此不多做介紹,有興趣可以看看原始碼,一起探討。另外我自己後來總結了一下,還存在很多問題,這些問題我會在下一篇中說明。

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

知識小集公眾號

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

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

相關文章