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