Minya 分层框架实现的思考(一):依赖转移

Minya 框架是我们团队之前构建的一套分层框架,在一个内部项目上验证了一下。Minya 只是项目名称,取自贡嘎雪山的英文名。这套分层框架是在 MVCS 的基础上进行了改造。后来团队转向 RAC + MVVM,就搁置起来,没有再用。之前一直想整理一下,将一些基础的思想写出来,供大家参考。然后就一直拖到现在。我后期也反思过,这个框架还存在不少问题,也会在这个系列里也会详细说明。

Minya 之前已开源,并放到 Github https://github.com/southpeak/Minya 上,有兴趣的话可以给个 Star。不过项目已停更,后期不再维护。

我们在考量一个 App 分层框架的设计和使用时,主要会基于以下几个原则:

  • 低耦合;
  • 单一职责,各层各司其职;
  • 可复用;
  • 结构清晰、代码均分;

另外,我们程序的运行主要是对数据做各种各样的操作,例如从服务端获取数据显示给用户,或者从用户输入中收集数据发送到服务端。因此我们可以从数据的角度来考虑整个框架的设计。

数据的流向有两个维度:纵向横向。纵向是从垂直分层的角度来考虑,数据在 View服务端/本地存储 之间传输;横向是从水平角度来考虑,数据从一个 ViewController 流向另一个 ViewController,或者从一个模块流向另一个模块。这一系列文章将主要从纵向数据流这个维度来考虑分层框架的设计。同时将一步步绘制我们的框架图,并结合代码,来说明 Minya 框架的设计、使用和存在的问题。整个系列基本分为三个部分:

  • 转移依赖
  • 构建依赖和数据通信
  • 问题

如果有闲情,可以赖着性子慢慢看。

MVCS

传统的 MVC 可以满足 App 开发的基本需求,但在实际的开发中,经常因为各种原因,导致 ViewController 层的代码过于庞杂,同时各层的职责不明确,最终影响到项目的维护。这也是业界讨论的一个热点:如何轻量化 ViewController。为此衍生出各种实用的框架,现在流行度比较高的应该是 MVVM。我们在最初考虑的时候用了 MVCS 框架,主要目的有以下几个方面:

  • 明确每一层的职责;
  • 轻量化 ViewController,让其成为数据的中转站;
  • 将业务处理独立于一层,并可根据实际情况将业务细分到不同的类中进行处理;
  • 均分代码,确保一个类或文件的代码量不会太大。

这一框架基本结构图及依赖关系如下:

对于每一层的职责,我们的基本考虑是:

  • Model 层:主要是用于表示数据,可以将其视为数据载体。在这一层中不做具体的业务处理,但可以执行一些简单的数据处理任务。即为 瘦Model 模型。在上图中没有表示出来;
  • View 层:即展示层,主要是将数据展现给用户,同时从用户输入中获取数据;
  • ViewController 层:该层将只作为 View 层的容器,以及数据的中转结点。这种中转包括纵向数据和横向数据的传输;
  • Store 层:业务处理层,所有的业务在这一层中来处理,这一层将只提供接口供上一层使用;
  • 数据访问层:这一层主要面向服务端和本地数据,即图中的 ServiceStorage,这一层不是我们的重点,在文中没有过多的讨论;

我们将下面三个部件统一为 Store,即 MVCS 中的 S 实际上包含了三个主要部件:StoreServiceStorage

职责明确好了,我们就需要考虑数据如何在各层之间传输了。数据在对象之间的传输主要有几种方式:

  • 方法调用
  • KVO
  • Delegate
  • Notification
  • callback

如果在图1的基础上加上数据传输方式,结构图看起来可能是下面这样的(注意箭头方向):

图中数据的传输,从上到下是 View 层通过 Delegate 传递 ViewControllerViewController 再通过方法调用将传递给 Store 做业务处理;从下到上是 Store 通过 callback 将数据回传给 ViewControllerViewController再通过方法调用传递给 View

这里有一些痛点:

  1. ViewControllerViewStore 是强依赖,需要知道两者的很多信息。如果处理不好,ViewController 所需要做的工作依然很多;
  2. ViewController 作为 View 层的 Delegate,实际上是让 View 层也依赖于 ViewController
  3. View 层如果视图层级很深,需要通过层层代理将数据传出;

因此,我们最初的想法是:有没有一种方式,降低 ViewControllerViewStore 的依赖,只需要知道两者少量的接口,就能完成这种数据传输。同时,能避开 Delegate 这种数据传递方式呢?

Notification 可以实现这种需求,甚至可以直接跳过 ViewController 来传递数据,但这种方式显然是不适合的。Notification 广播的属性决定了我们没办法将其控制在一个页面内。

KVO 呢?我们决定尝试一下。

KVO

苹果爸爸自身提供的 KVO 是开发者的一大槽点,在这不用过多解释。为了优化 KVO 糟糕的设计,社区有很多框架都对其进行了封装,以提供一套更友好的 KVO 接口,像 FacebookKVOController,还有 Reactive CocoaRACObserve。在此,我们不去说明各种方案的优劣,只说结果:我们选择了 Reactive CocoaKVO,并做了部分改造。

依赖转移 和 Pipeline

有了改造后的 KVO 这一强大的工具,我们就可以尝试改造一下数据传输方式了。改造后的图如下所示:

这里的变化并不大,主要是 ViewController 通过 KVO 监听 ViewStore 的属性变化,然后来获取并传递数据。所以对实质的问题并没有改进的地方,ViewController 依然需要了解 ViewStore 很多信息,同时也并没有规避 View 层到 ViewController 数据层层传递的问题,所以这种改造没有太大意义。

问题的症结还是在于数据的传输必须经过 ViewController,这样意味着 ViewController 必须强依赖于 ViewStore。那能不能让数据绕过 ViewController ,通过其它方式在 ViewStore 之间传输呢?可能会有三种方案考虑:

  1. 通过 Notification 发送接收通知的方式来传输数据;
  2. View 层和 Store 层之间建立依赖关系,直接传输;
  3. 建立一个第三方对象,让 ViewStore 都依赖于这个对象,通过这个对象来传输数据;

第一种方案我们上面已经说过,Notification 可能会导致失控;第二种方案更不可行,我们使用 MVC 及各种衍生框架,就是为了让 View业务层 相分离。所以,我们尝试了第三种方案,建立一个第三方对象。

换一个角度来考虑我们的分层框架,其实每一层都是在处理各种数据,根据所需的数据及其变化执行相应的操作,所以对数据的依赖是强需求。每一层只需要关心自己特定的数据,就能完成各自的职责。比如说 View 层只要有了列表数据,就可以展示一个 TableViewViewController 只要有了下一个 ViewController 所需要的数据,就可以跳转到下一个页面;Store 层有了用户名和密码,就可以发起登录请求。那么我们是不是可以构建一个 数据对象,这个数据对象包含 ViewViewControllerStore 各层所需要的所有数据,让这三层都依赖于这个数据对象,然后各层按需从这个数据对象获取数据呢?基于这种考虑,我们引入了 Pipeline 对象。

  • Pipeline:可以理解为数据管道,也可以理解为数据集散中心。它负责组织一个页面分层层级所需要的所有数据。负责数据在 View 层与 Store 层之间的传输。实际上是一个数据对象,但不同于 Model 的职责,它主要负责数据传输,而不是数据表示。

注:这里只是借用了管道这个名称。和 Linux 中的管道不是一回事。

引入 Pipeline 后,我们的结构图就可以变成下面这种形式:

借助于 PipelineKVO,我们就可以让 View 层与 Store 层的数据传输绕开 ViewController ,通过这个数据管道来传输。以登录操作为例:用户在界面上输入 usernamepassword 后,点击登录按钮后,将这两个数据丢到 Pipeline 里面,同时丢到 Pipeline 里面的还有一个点击按钮标识 flag (注意这里有一个先后顺序)。Store 层监听 Pipeline 对象的 flag 属性,发现其改变后,从 Pipeline 里面取出 usernamepassword,发起登录请求,这个数据流转的路径如下:

从示意图可以看到,这次数据传输和 ViewController 基本上没有啥关系。而且如果 Pipeline 设计的好的话,View 层每个视图层级的数据可以直接丢到 Pipeline 中进行传输,而不需要层层上传到外层视图对象上。

通过这种依赖转移,我们就可以弱化 ViewControllerView 层和 Store 层的依赖(注意是弱化,而不是消除,图4中我们通过虚线表示这种弱依赖关系),View 层和 Store 层只需要提供少量的接口,就可以让数据在三层间进行传输。

注:ViewController 本身也可能需要一些数据来执行某些操作,所以也可以依赖 Pipeline 并从中获取数据。

Minya 框架中,我们声明了一个 ViewController 的基类 MIViewController。在这个类中,我们构造了 ViewViewControllerStore 之间的弱依赖关系。我们先来看看这个类的主要代码:

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 中创建视图对象并将其作为根视图。而 ViewControllerStore 的了解仅限于 store.pipeline 属性(以及一个发起数据请求的 -fetchData 方法),对 View 的了解仅限于 viewsetupPipeline 方法,除此之外 ViewController 对两者一无所知。

通过这种处理,三层之间的关系可以满足设计模式的一些基本原则。

这里需要明确一下 pipeline 的职责:

  • 维护各层所需要的数据,这种数据包括需要展示的业务数据、用户输入数据、甚至于用于标识用户点击、数据变更的标识数据;
  • 负责数据在各层之前的传输;
  • 不处理任何业务逻辑;

小结

在这篇文章中,我们主要从理论方面描述了通过引入 pipeline 来构建 MVCS + Pipeline 分层框架,从而降低 ViewControllerView 层及 Store 层的依赖。需要注意的是,这里并没有消除依赖,而只是将依赖转移了。通过这种转移,我们能让各层更专注地处理自己的任务。

在下一篇中,我们会通过实例来介绍如何去构建这种依赖,如何去构造 pipeline 以及数据如何通过 pipeline 来传输。通过实例,可以给出一个更直观的认识。

Minya 分层框架实现的思考(二):构建依赖及数据传输

Minya 分层框架实现的思考(三):问题

知识小集公众号

知识小集是一个团队公众号,每周都会有原创文章分享,我们的文章都会在公众号首发。知识小集微信群,短短几周时间,目前群友已经300+人,很快就要达到上限(抓住机会哦),关注公众号获取加群方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值