iOS:實現一套輕量級 MVVM 框架

前言

在客戶端開發項目中,MVC 仍然是主流架構,但是 MVC 也存在十分明顯的弊端:Controller 作爲中介者常常需要負擔大量的業務處理邏輯,所以 MVC 也被戲稱爲 Masive View Controller 架構。緩解這個問題其實有很多途徑,例如:

此外,MVC 架構模式還普遍存在單元測試推進困難的問題,該問題還是來源於 Controller 負擔過重,由於 Controller 通常需要依賴 View 層和 Model 層模塊,引入 Manager 和 Service 則依賴模塊更加繁多,因此測試 Controller 時通常需要 Mock 很多模塊,打很多的 Stub 以剔除依賴模塊的影響。另外,Controller 的單元測試需要考慮 Controller 複雜的生命週期。

MVVM 可以說是爲了解決 MVC 的以上兩個弊端而存在的。

一、MVVM 架構

View Model 是 MVVM 架構的核心邏輯層。View Model 用於表徵 View 的特性,並通過數據雙向綁定 View Model 和 View,使 View Model 可以驅動 View 刷新界面,同時 View 接收的用戶交互動作也可以更新 View Model 的數據。雙向綁定下的 View Model 和 View 是完全解耦的,因此單元測試工作比起 MVC 架構簡單得多。MVVM 中 Controller 的職能只侷限於 View Model 和 View 的雙向綁定,Controller 邏輯層變得很薄,因此週期問題基本可以忽略不計。

MVVM 之所以能達到以上效果,很大程度上是因爲它將 View 和 View Model 之間的雙向數據流下沉到了框架層。不過這也給 MVVM 帶來了最大的缺點:數據流動控制的實現細節隱藏於核心框架層。近乎黑盒的雙向數據綁定實現,有時會給代碼調試帶來不小的麻煩。另外,隨着核心邏輯大量轉移到 View Model,同樣會帶來 View Model 規模膨脹的問題,另外需要注意,MVVM 通常不能減少代碼量,MVC 仍然是最省代碼的客戶端架構。總之就是,MVVM 值得嘗試,但也沒有想象中神奇。

在強大的 MVVM 框架支持下是可以達到更省代碼的效果的。

二、MVVM 架構實現

iOS 客戶端開發中應用最爲廣泛的 MVVM 框架應該是 Objective-C 語言實現的 RAC 框架和 Swift 語言實現的 RxSwift 框架。RAC 框架給人的第一感覺就是重,在現有的 MVC 架構項目中引入 RAC 框架基本是顛覆性的,需要學習響應式編程、函數式編程、鏈式編程,需要熟悉 RAC 對 UIKit 框架的封裝,更遑論還有一堆基於 RAC 的衍生框架。RxSwift 則還好一點,因爲 RxSwift 的實現本身非常契合 Swift 語言的特點,不過依然是有點重。總之,在傳統 MVC 項目中啓用 RAC 或 RxSwift,絕對不比引入新編程語言的 Flutter、RN 來得簡單。

那麼可不可以用常規的,更輕量的方式來實現 MVVM 框架層邏輯呢?通常第一個想到的就是觀察者模式,恰好 Objective-C 有強大的 KVO 機制。各邏輯分工大致如下:

但是在實現時你會發現,KVO 是通過 Key Path 來配置的,這個 Key Path 有個很致命的弱點,它是字符串!這會有什麼問題呢?想象一下,有一天你要重構代碼,發現某個 View Model 屬性名設置不太合理,你用 Refactor Rename 工具給這個屬性重命名了,此時所有通過 KVO 綁定 View Model 的該屬性的業務邏輯都會出問題,這個時候你只能再手動修改該屬性的數據綁定代碼中對應的 Key Path 字符串。另外,字符串終究是字符串,IDE 不能爲字符串提供編譯時檢查以及提示,所以可以預見開發體驗極差。而且我認爲 KVO 比較適用於上層業務實現,如果將其下沉到框架層則很容易和上層業務邏輯發生衝突。最簡單的例子,如果上層模塊實現observeValueForKeyPath:時,沒有調用[super observeValueForKeyPath:],那底層的數據雙向綁定框架就直接被旁路了。

KVO 方案被 Out 了,還有沒有更好的實現方案呢?這就是下面所要探討的問題。

三、輕量級 MVVM 架構方案

首先要引入 Observable 和 Observer 的概念,注意這裏的 Observable 和 Observer 和 RAC 和 RxSwift 中 Observable 和 Observer 並不一致,甚至有點相反的意味。

非常直觀地,有了 Observable 和 Observer 就可以打通數據(View Model)驅動界面刷新的單向數據流。那麼從界面的用戶交互 Action 或委託回調到 View Model 的反向數據流呢?其實也是可以通過 Observable 和 Observer 來打通,因爲 Action 的本質其實也是傳遞數據,只要將來自 View 的用戶交互 Action 所傳遞的數據定義爲 Observable,將 View Model 定義爲 Observer 即可以打通反向數據流。總之:

雖然用的是觀察者模式,但是這裏不使用 Notification 和 KVO,而是採用最簡單粗暴的方法,Observable 強持有所有訂閱該 Observable 的 Observer,Observable 值更新時直接觸發所有 Observer 所註冊的操作邏輯。看到這裏可能會有這樣的疑問,這不就循環引用了麼?其實並不是,因爲 Observable 的數據粒度要比 View Model 和 View 低一個等級,也就是說扮演 Observable 角色是指持有若干個 Observable 成員,扮演 Observer 角色是指持有若干個 Observer 成員。這樣一來,View Model 和 View 就不會存在循環引用的問題。

3.1 基本接口

接下來是設計接口。首先按照 Observable 和 Observer 的定義,將其分別定義爲兩套協議:

Observable協議定義了可被觀察者的基本特徵:

@protocol Observer;
/// 可觀察對象,value 成員更新 setter 會驅動註冊的觀察者刷新。註冊觀察者後,觀察者被可觀察對象強持有
@protocol Observable <NSObject>

/// 值
@property(strong, nonatomic, nullable) id value;

/// 添加觀察者
-(void)addObserver:(id<Observer>)observer;

/// 移除觀察者
-(void)removeObserver:(id<Observer>)observer;

@end

Observer協議定義了觀察者的基本特徵:

@protocol Observable;
/// 觀察者
@protocol Observer <NSObject>

/// 訂閱可觀察對象
@property(copy, nonatomic, readonly) void (^subscribe)(id<Observable> observable);

/// 觸發值刷新
-(void)invoke:(id)newValue;

@end

基於兩個協議再進一步定義兩個具體類型分別實現這兩套協議。可以發現公開 API 都通過屬性提供,之所以設計爲這種形式,是爲了在開發過程中使用優雅的鏈式調用風格。

/// 可觀察對象
@interface Observable : NSObject<Observable>

/// 構建
@property(copy, nonatomic, class, readonly) Observable *(^create)(id _Nullable defaultValue);

@end

/// 觀察者所註冊的操作
typedef void(^ObserverHandler)(id newValue);

/// 觀察者
@interface Observer : NSObject <Observer>

/// 構建
@property(copy, nonatomic, class, readonly) Observer *(^create)(void);

/// 處理值刷新
@property(copy, nonatomic, readonly) Observer * (^handle)(ObserverHandler);

@end

3.2 基本實現

實現代碼也非常簡單,四個字概括:簡單粗暴。Observable只管理一個值,而且必須是id類型。需要注意,Observable是具有原子性的(不是屬性atomic那種原子性),也就是說,該框架只能區分Observable的值 “改變” 或者“不改變”,不存在Observable的值 “只改變了其中一部分屬性” 這種狀態。

@interface Observable ()

@property(strong, nonatomic) NSMutableArray *observers;

@end

@implementation Observable

@synthesize value = _value;

-(void)setValue:(id)value{
    if(![self.value isEqual:value]){
        _value = value;
        for(id<Observer> observer in self.observers){
            [observer invoke:value];
        }
    }
}

static Observable *(^create)(id) = ^Observable *(id defaultValue){
    Observable *observable = [[Observable alloc] init];
    observable.value = defaultValue;
    return observable;
};

+(Observable *(^)(id))create{
    return create;
}


-(void)addObserver:(id<Observer>)observer{
    [self.observers addObject:observer];
}

-(void)removeObserver:(id<Observer>)observer{
    [self.observers removeObject:observer];
}

-(NSMutableArray *)observers{
    if(!_observers){
        _observers = [[NSMutableArray alloc] init];
    }
    return _observers;
}

@end

Observer實現同樣簡單粗暴。觀察者持有一個 Block,Observerinvoke:方法只是簡單調用了該 Block。在Observer訂閱Observable時需要指定該 Block 的實現。問題又來了,這不就有循環引用的風險了麼?沒錯,就是有循環引用的風險。但是隻需要在調用subscribe時,在 Block 實現中使用__weak__strong避免強引用self即可,就是基本的 Block 防止循環引用的套路。雖然套路簡單,但是需要注意這條規則一定要遵循

@interface Observer ()

@property(copy, nonatomic) Observer * (^handle)(ObserverHandler);

@property(copy, nonatomic) ObserverHandler handler;

@end

@implementation Observer

@synthesize subscribe = _subscribe;

-(void (^)(id<Observable>))subscribe{
    if(!_subscribe){
        __weak typeof(self) weakSelf = self;
        _subscribe = ^(id<Observable> observable){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            [observable addObserver:strongSelf];
        };
    }
    return _subscribe;
}

-(void)invoke:(id)newValue{
    if(self.handler){
        self.handler(newValue);
    }
}

static Observer *(^create)(void) = ^Observer *(){
    Observer *observer = [[Observer alloc] init];
    return observer;
};

+(Observer *(^)(void))create{
    return create;
}

-(Observer * (^)(ObserverHandler))handle{
    if(!_handle){
        __weak typeof(self) weakSelf = self;
        _handle = ^Observer *(ObserverHandler handler){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            strongSelf.handler = handler;
            return strongSelf;
        };
    }
    return _handle;
}

@end

3.3 能力擴展

基本實現框架有了,不過僅有ObservableObserver的話,貌似只能組織最簡單的數據流拓撲,即從單個Observable分發到多個Observer,其實開發過程中還希望具備多個Observable合成單個Observable的能力。爲此定義ObservableCombiner用於實現Observable合成。

ObservableCombiner繼承Observable類型,可以通過調用其combine屬性合成多個Observable,同時ObservableCombiner具備一定Observer的特徵(但不是遵循Observer協議),其handleObserversubscribe是相同的原理,只是傳參上,前者是NSArray表示所有合成Observable的值的數組。當合成的Observable值更新時,會觸發handle所註冊的 Block。

/// 合成可觀察對象的觸發策略
typedef NS_ENUM(NSUInteger, CombineStrategy) {
    /// 第一次值更新才刷新
    CombineStrategyFirst,
    /// 所有值更新才刷新
    CombineStrategyAll,
    /// 任何值更新均刷新
    CombineStrategyEvery,
};

/// 合成可觀察對象處理值刷新
typedef _Nullable id (^CombinerHandler)(NSArray *newValues);

/// 合成多個可觀察對象
@interface ObservableCombiner : Observable

/// 安全獲取值
@property(copy, nonatomic, readonly, class) id _Nullable (^safeValue)(NSArray *, NSInteger);

/// 構建
@property(copy, nonatomic, readonly, class) ObservableCombiner *(^create)(CombineStrategy strategy);

/// 合併可觀察對象
@property(copy, nonatomic, readonly) ObservableCombiner * (^combine)(id<Observable> observable);

/// 處理值刷新
@property(copy, nonatomic, readonly) ObservableCombiner * (^handle)(CombinerHandler);

@end

實現代碼直接貼在文章最後,這裏不作詳細介紹了。

後面再嘗試擴展支持從多個Observable映射到多個Observable的能力。

四、輕量級 MVVM 框架 Demo

基於該框架的開發同樣需要以組織數據流的思想作爲指導,框架提供的公開 API 基本是用於組織數據流,核心操作是構建(create)、訂閱(subscribe)和處理(handle)。爲便於描述將 View Model 驅動 View 刷新數據流簡稱爲正向數據流,將來自 View 的界面交互動作驅動 View Model 更新數據流簡稱爲反向數據流,代碼邏輯分佈基本如下:

看起來很繞,實際上原則可以歸結爲四條:

注意:雖然在 Controller 中處理正向數據流,但是處理邏輯必須是非常簡單的操作,基本是原子操作,符合操作可以通過在 View 層定義並實現接口,對外向 Controller 提供。其實最合理的佈局是在 View 中構建和處理正向數據流 Observer,不過當 View Model 可以提供非常熟成的數據時,Controller 通過一兩句代碼就可以調起 View 刷新視圖,則沒有必要因此引入實現邏輯過於簡單的新接口。

接下來以登錄業務來演示輕量級 MVVM 框架的使用。業務描述如下:

首先定義 View Model:

@interface LoginViewModel : NSObject

//MARK: 數據驅動UI刷新
@property(strong, nonatomic) Observable *username;

@property(strong, nonatomic) Observable *password;

@property(strong, nonatomic) Observable *instruction;

@property(strong, nonatomic) ObservableCombiner *usernameValid;

@property(strong, nonatomic) ObservableCombiner *passwordValid;

@property(strong, nonatomic) ObservableCombiner *loginEnabled;

//MARK: 用戶交互動作訂閱
@property(strong, nonatomic) Observer *usernameDidChange;

@property(strong, nonatomic) Observer *passwordDidChange;

@property(strong, nonatomic) Observer *loginTouched;

@end

其次定義 View:

@interface LoginView : UIView

@property(strong, nonatomic) UITextField *usernameTextField;

@property(strong, nonatomic) UITextField *passwordTextField;

@property(strong, nonatomic) UILabel *instructionLabel;

@property(strong, nonatomic) UIButton *loginButton;

@property(strong, nonatomic) Observable *usernameDidChange;

@property(strong, nonatomic) Observable *passwordDidChange;

@property(strong, nonatomic) Observable *loginButtonTouched;

@end

4.1 數據流組織

正向數據流組織及反向數據流處理是 View Model 的核心邏輯。組織正向數據流的代碼如下,通過代碼可以非常直觀地閱讀出以下關鍵信息,從而生成非常清晰的正向數據流拓撲:

-(void)doDataWeaving{
    self.usernameValid
        .combine(self.username)
        .combine(self.passwordValid)
        .handle(^id _Nullable(NSArray * _Nonnull newValues) {
            NSString *username = ObservableCombiner.safeValue(newValues, 0);
            if(!username.length){
                self.instruction.value = @"*用戶名不能爲空";
                return @(NO);
            }
            
            if(username.length < 6){
                self.instruction.value = @"*用戶名必須超過6個字符";
                return @(NO);
            }
            
            if(username.length > 24){
                self.instruction.value = @"*用戶名不能超過24個字符";
                return @(NO);
            }
            
            BOOL passwordValid = [ObservableCombiner.safeValue(newValues, 1) boolValue];
            if(passwordValid){
                self.instruction.value = @"";
            }
            return @(YES);
        });
    
    self.passwordValid
        .combine(self.usernameValid)
        .combine(self.password)
        .handle(^id _Nullable(NSArray * _Nonnull newValues) {
            NSString *password = ObservableCombiner.safeValue(newValues, 1);
            BOOL usernameValid = [ObservableCombiner.safeValue(newValues, 0) boolValue];
            
            BOOL passwordValid;
            NSString *passwordInstruction;
            if(!password.length){
                passwordInstruction = @"*密碼不能爲空";
                passwordValid = NO;
            }else if(password.length < 6){
                passwordInstruction = @"*密碼必須超過6個字符";
                passwordValid = NO;
            }else if(password.length > 24){
                passwordInstruction = @"*密碼不能超過24個字符";
                passwordValid = NO;
            }else{
                passwordInstruction = @"";
                passwordValid = YES;
            }
            
            if(usernameValid){
                self.instruction.value = passwordInstruction;
            }
            return @(passwordValid);
        });
    
    self.loginEnabled
        .combine(self.usernameValid)
        .combine(self.passwordValid)
        .handle(^id _Nullable(NSArray * _Nonnull newValues) {
            BOOL usernameValid = [ObservableCombiner.safeValue(newValues, 0) boolValue];
            BOOL passworkValid = [ObservableCombiner.safeValue(newValues, 1) boolValue];
            return @(usernameValid && passworkValid);
        });
}

處理反向數據流的代碼如下,代碼比較簡單不作贅述:

-(void)doActionHandlings{
    WS
    // 用戶交互處理
    self.usernameDidChange = Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.username.value = newValue;
    });
    self.passwordDidChange = Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.password.value = newValue;
    });
    self.loginTouched = Observer.create().handle(^(id  _Nonnull newValue) {
        self.instruction.value = [NSString stringWithFormat:@"登錄成功(*^▽^*)"];
    });
}

4.2 訂閱的實現

訂閱是 Controller 絕對的核心邏輯,包括正向數據流和反向數據流訂閱。正向數據流訂閱的實現代碼如下,包括:

/// 數據驅動UI刷新
-(void)doDataBindings{
    WS
    Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.loginView.usernameTextField.text = newValue;
    }).subscribe(self.loginViewModel.username);
    
    Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.loginView.passwordTextField.text = newValue;
    }).subscribe(self.loginViewModel.password);
    
    Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.loginView.instructionLabel.text = newValue;
    }).subscribe(self.loginViewModel.instruction);
    
    Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.loginView.usernameTextField.backgroundColor = [newValue boolValue] ? [UIColor whiteColor] : LightRed;
    }).subscribe(self.loginViewModel.usernameValid);
    
    Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.loginView.passwordTextField.backgroundColor = [newValue boolValue] ? [UIColor whiteColor] : LightRed;
    }).subscribe(self.loginViewModel.passwordValid);
    
    Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.loginView.loginButton.enabled = [newValue boolValue];
        self.loginView.loginButton.backgroundColor = [newValue boolValue] ? ThemeColor : LightGray;
        [self.loginView.loginButton setTitleColor:[newValue boolValue] ? [UIColor whiteColor] : [UIColor darkGrayColor] forState:UIControlStateNormal];
    }).subscribe(self.loginViewModel.loginEnabled);
}

反向數據流訂閱的實現代碼如下;

/// 用戶交互動作訂閱
-(void)doActionBindings{
    self.loginViewModel.usernameDidChange.subscribe(self.loginView.usernameDidChange);
    
    self.loginViewModel.passwordDidChange.subscribe(self.loginView.passwordDidChange);
    
    self.loginViewModel.loginTouched.subscribe(self.loginView.loginButtonTouched);
}

從代碼上看,不難發現,該框架是一點都不省代碼,同等的 MVC 架構實現比上面的實現在代碼空間行數上少 50% 左右,但是基於該框架實現的業務代碼數據流非常清晰,代碼邏輯分佈更加均勻,View Model 處理純粹的業務邏輯,也非常契合引入 Manager 和 Service 模塊分流數據管理負擔的優化方式。

五、總結

雖然只定義了ObservableObserverObservableCombiner,但是它已經具備了構建 MVVM 架構的基本能力了。首先,肉眼可見的,它足夠輕量。其次,不存在前文所述 KVO 方案的缺陷。最後,它麻雀雖小,其實五臟俱全,正確使用該框架可以獲得漂亮工整的代碼邏輯結構。在後面 Demo 開發過程中實際應用該框架時,感覺開發體驗總體還是不錯的。

當然本方案還是有非常明顯的缺陷的,例如:

總之,本文的輕量級 MVVM 框架方案可以用來體驗 iOS 客戶端開發中的 MVVM 架構模式的應用,或者理解 MVVM 架構的原理。本方案優點和缺點同等明顯,由於目前缺乏完備的測試,以及對複雜業務場景的實踐案例支撐,暫時不打算直接應用到開發項目中

附錄

附錄一:Combiner 實現

// ObservableCombiner.m

@interface ObservableCombiner ()

@property(strong, nonatomic) NSMutableArray *observables;

@property(assign, nonatomic) CombineStrategy strategy;

@property(assign, nonatomic) NSUInteger accessFlags;

@property(copy, nonatomic) ObservableCombiner * (^combine)(id<Observable> observable);

@property(copy, nonatomic) ObservableCombiner * (^handle)(CombinerHandler);

@property(copy, nonatomic) CombinerHandler handler;

@end

@implementation ObservableCombiner

static ObservableCombiner *(^create)(CombineStrategy strategy) = ^ObservableCombiner *(CombineStrategy strategy){
    ObservableCombiner *combiner = [[ObservableCombiner alloc] init];
    combiner.strategy = strategy;
    return combiner;
};

+(ObservableCombiner *(^)(CombineStrategy))create{
    return create;
}

-(ObservableCombiner * (^)(id<Observable>))combine{
    if(!_combine){
        __weak typeof(self) weakSelf = self;
        _combine = ^ObservableCombiner * (id<Observable> observable){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            
            NSInteger index = strongSelf.observables.count;
            id<Observer> observer = Observer.create().handle(^(id  _Nonnull newValue) {
                __strong typeof(weakSelf) strongSelf = weakSelf;
                [strongSelf handleNewValue:newValue index:index];
            });
            
            [observable addObserver:observer];
            [strongSelf.observables addObject:observable];
            return strongSelf;
        };
    }
    return _combine;
}

-(ObservableCombiner * (^)(CombinerHandler))handle{
    if(!_handle){
        __weak typeof(self) weakSelf = self;
        _handle = ^ObservableCombiner *(CombinerHandler handler){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            strongSelf.handler = handler;
            return strongSelf;
        };
    }
    return _handle;
}

-(NSMutableArray *)observables{
    if(!_observables){
        _observables = [[NSMutableArray alloc] init];
    }
    return _observables;
}

-(void)handleNewValue:(id)newValue index:(NSInteger)index{
    // 根據不同的策略觸發完成事件
    BOOL isFirst = !self.accessFlags;
    self.accessFlags |= (<< index);
    switch (self.strategy) {
        case CombineStrategyFirst:{
            if(isFirst){
                self.value = self.handler([self getAllValues]);
            }
        }break;
        
        case CombineStrategyEvery:{
            self.value = self.handler([self getAllValues]);
        }break;
        
        case CombineStrategyAll:{
            NSUInteger allFlags = pow(2, self.observables.count) - 1;
            BOOL isAll = (allFlags & self.accessFlags) == allFlags;
            if(isAll){
                self.value = self.handler([self getAllValues]);
            }
        }break;
            
        default:
            break;
    }
}

-(NSArray *)getAllValues{
    NSMutableArray *result = [[NSMutableArray alloc] init];
    for(id<Observable> observable in self.observables){
        [result addObject:observable.value ?: [NSNull null]];
    }
    return result;
}

static id _Nullable (^safeValue)(NSArray *, NSInteger) = ^id (NSArray *values, NSInteger index){
    return values[index] == [NSNull null] ? nil : values[index];
};

+(id  _Nullable (^)(NSArray * _Nonnull, NSInteger))safeValue{
    return safeValue;
}

@end

附錄二:源碼

[1] 源碼及 Demo 地址: https://github.com/Luminixus/DataDrivenMVVM.git

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KXAD7XaP23ymoZihzNIt8A