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+人,很快就要达到上限(抓住机会哦),关注公众号获取加群方式。