前言
在客户端开发项目中,MVC仍然是主流架构,但是MVC也存在十分明显的弊端:Controller作为中介者常常需要负担大量的业务处理逻辑,所以MVC也被戏称为MasiveViewController架构。缓解这个问题其实有很多途径,例如:
用胖模型分担Controller的模型数据处理工作,提供尽量熟成的业务数据;引入Manager或提取Service模块,负责特定业务模块数据管理;将膨胀的Controller业务分摊到多个ChildViewController;通过Category对Controller业务逻辑做分文件处理;此外,MVC架构模式还普遍存在单元测试推进困难的问题,该问题还是来源于Controller负担过重,由于Controller通常需要依赖View层和Model层模块,引入Manager和Service则依赖模块更加繁多,因此测试Controller时通常需要Mock很多模块,打很多的Stub以剔除依赖模块的影响。另外,Controller的单元测试需要考虑Controller复杂的生命周期。
MVVM可以说是为了解决MVC的以上两个弊端而存在的。
一、MVVM架构
ViewModel是MVVM架构的核心逻辑层。ViewModel用于表征View的特性,并通过数据双向绑定ViewModel和View,使ViewModel可以驱动View刷新界面,同时View接收的用户交互动作也可以更新ViewModel的数据。双向绑定下的ViewModel和View是完全解耦的,因此单元测试工作比起MVC架构简单得多。MVVM中Controller的职能只局限于ViewModel和View的双向绑定,Controller逻辑层变得很薄,因此周期问题基本可以忽略不计。
MVVM之所以能达到以上效果,很大程度上是因为它将View和ViewModel之间的双向数据流下沉到了框架层。不过这也给MVVM带来了最大的缺点:数据流动控制的实现细节隐藏于核心框架层。近乎黑盒的双向数据绑定实现,有时会给代码调试带来不小的麻烦。另外,随着核心逻辑大量转移到ViewModel,同样会带来ViewModel规模膨胀的问题,另外需要注意,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机制。各逻辑分工大致如下:
Model:模型层;View:视图层;ViewModel:表征视图特性;Controller:通过KVO设置View观察ViewModel的各个属性,则ViewModel属性值变更会驱动View刷新。另外,将来自View的用户交互触发的Action或者回调消息转发到ViewModel中处理;但是在实现时你会发现,KVO是通过KeyPath来配置的,这个KeyPath有个很致命的弱点,它是字符串!这会有什么问题呢?想象一下,有一天你要重构代码,发现某个ViewModel属性名设置不太合理,你用RefactorRename工具给这个属性重命名了,此时所有通过KVO绑定ViewModel的该属性的业务逻辑都会出问题,这个时候你只能再手动修改该属性的数据绑定代码中对应的KeyPath字符串。另外,字符串终究是字符串,IDE不能为字符串提供编译时检查以及提示,所以可以预见开发体验极差。而且我认为KVO比较适用于上层业务实现,如果将其下沉到框架层则很容易和上层业务逻辑发生冲突。最简单的例子,如果上层模块实现observeValueForKeyPath:时,没有调用[superobserveValueForKeyPath:],那底层的数据双向绑定框架就直接被旁路了。
KVO方案被Out了,还有没有更好的实现方案呢?这就是下面所要探讨的问题。
三、轻量级MVVM架构方案
首先要引入Observable和Observer的概念,注意这里的Observable和Observer和RAC和RxSwift中Observable和Observer并不一致,甚至有点相反的意味。
Observable:可被观察对象;Observer:观察者,可以订阅Observable,当Observable刷新数据时,会触发Observer刷新;非常直观地,有了Observable和Observer就可以打通数据(ViewModel)驱动界面刷新的单向数据流。那么从界面的用户交互Action或委托回调到ViewModel的反向数据流呢?其实也是可以通过Observable和Observer来打通,因为Action的本质其实也是传递数据,只要将来自View的用户交互Action所传递的数据定义为Observable,将ViewModel定义为Observer即可以打通反向数据流。总之:
ViewModel在数据驱动界面刷新数据流中扮演Observable的角色,此时View扮演Observer的角色;ViewModel在用户交互驱动数据更新数据流中扮演Observer,View扮演Observable;虽然用的是观察者模式,但是这里不使用Notification和KVO,而是采用最简单粗暴的方法,Observable强持有所有订阅该Observable的Observer,Observable值更新时直接触发所有Observer所注册的操作逻辑。看到这里可能会有这样的疑问,这不就循环引用了么?其实并不是,因为Observable的数据粒度要比ViewModel和View低一个等级,也就是说扮演Observable角色是指持有若干个Observable成员,扮演Observer角色是指持有若干个Observer成员。这样一来,ViewModel和View就不会存在循环引用的问题。
3.1基本接口
接下来是设计接口。首先按照Observable和Observer的定义,将其分别定义为两套协议:
Observable协议定义了可被观察者的基本特征:
Observable对应一个值value(公开API);可以通过调用addObserver:方法向Observable添加观察者(供Observer调用);可以通过调用removeObserver:方法从Observable移除观察者(供Observer调用);
protocolObserver;///可观察对象,value成员更新setter会驱动注册的观察者刷新。注册观察者后,观察者被可观察对象强持有protocolObservableNSObject///值property(strong,nonatomic,nullable)idvalue;///添加观察者-(void)addObserver:(idObserver)observer;///移除观察者-(void)removeObserver:(idObserver)observer;endObserver协议定义了观察者的基本特征:可以通过访问subscribe属性订阅Observable(公开API);可以通过调用invoke:方法触发刷新(供Observable调用);
protocolObservable;///观察者protocolObserverNSObject///订阅可观察对象property(copy,nonatomic,readonly)void(^subscribe)(idObservableobservable);///触发值刷新-(void)invoke:(id)newValue;end基于两个协议再进一步定义两个具体类型分别实现这两套协议。可以发现公开API都通过属性提供,之所以设计为这种形式,是为了在开发过程中使用优雅的链式调用风格。///可观察对象
interfaceObservable:NSObjectObservable///构建property(copy,nonatomic,class,readonly)Observable*(^create)(id_NullabledefaultValue);end///观察者所注册的操作typedefvoid(^ObserverHandler)(idnewValue);///观察者interfaceObserver:NSObjectObserver///构建property(copy,nonatomic,class,readonly)Observer*(^create)(void);///处理值刷新property(copy,nonatomic,readonly)Observer*(^handle)(ObserverHandler);end3.2基本实现实现代码也非常简单,四个字概括:简单粗暴。Observable只管理一个值,而且必须是id类型。需要注意,Observable是具有原子性的(不是属性atomic那种原子性),也就是说,该框架只能区分Observable的值“改变”或者“不改变”,不存在Observable的值“只改变了其中一部分属性”这种状态。
interfaceObservable()property(strong,nonatomic)NSMutableArray*observers;endimplementationObservablesynthesizevalue=_value;-(void)setValue:(id)value{if(![self.valueisEqual:value]){_value=value;for(idObserverobserverinself.observers){[observerinvoke:value];}}}staticObservable*(^create)(id)=^Observable*(iddefaultValue){Observable*observable=[[Observablealloc]init];observable.value=defaultValue;returnobservable;};+(Observable*(^)(id))create{returncreate;}-(void)addObserver:(idObserver)observer{[self.observersaddObject:observer];}-(void)removeObserver:(idObserver)observer{[self.observersremoveObject:observer];}-(NSMutableArray*)observers{if(!_observers){_observers=[[NSMutableArrayalloc]init];}return_observers;}endObserver实现同样简单粗暴。观察者持有一个Block,Observer的invoke:方法只是简单调用了该Block。在Observer订阅Observable时需要指定该Block的实现。问题又来了,这不就有循环引用的风险了么?没错,就是有循环引用的风险。但是只需要在调用subscribe时,在Block实现中使用__weak和__strong避免强引用self即可,就是基本的Block防止循环引用的套路。虽然套路简单,但是需要注意这条规则一定要遵循。interfaceObserver()property(copy,nonatomic)Observer*(^handle)(ObserverHandler);property(copy,nonatomic)ObserverHandlerhandler;endimplementationObserversynthesizesubscribe=_subscribe;-(void(^)(idObservable))subscribe{if(!_subscribe){__weaktypeof(self)weakSelf=self;_subscribe=^(idObservableobservable){__strongtypeof(weakSelf)strongSelf=weakSelf;[observableaddObserver:strongSelf];};}return_subscribe;}-(void)invoke:(id)newValue{if(self.handler){self.handler(newValue);}}staticObserver*(^create)(void)=^Observer*(){Observer*observer=[[Observeralloc]init];returnobserver;};+(Observer*(^)(void))create{returncreate;}-(Observer*(^)(ObserverHandler))handle{if(!_handle){__weaktypeof(self)weakSelf=self;_handle=^Observer*(ObserverHandlerhandler){__strongtypeof(weakSelf)strongSelf=weakSelf;strongSelf.handler=handler;returnstrongSelf;};}return_handle;}end3.3能力扩展基本实现框架有了,不过仅有Observable和Observer的话,貌似只能组织最简单的数据流拓扑,即从单个Observable分发到多个Observer,其实开发过程中还希望具备多个Observable合成单个Observable的能力。为此定义ObservableCombiner用于实现Observable合成。ObservableCombiner继承Observable类型,可以通过调用其