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

上一篇我们主要从理论上讲述如何通过 转移依赖 来轻量化我们的 ViewController,同时在 View 层和 Store 层之间传输数据。在这一篇中,我们将通过 Demo 来更清晰地描述 Minya 框架的实际操作,包括如何去构造 pipeline,如何去构建三层对象对 pipeline 的依赖,以及数据如何通信。

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

Demo 的基本效果可查看公众号(这里无法上传视频)。

这里主要展示了两个页面,其对应的 ViewController 分别是 SearchViewControllerInterestingnessViewController。后面的示例主要以这两个类为主。

由于接口是基于 Frickr 的,所以如果想运行起来,需要去 Frickr 上注册一个应用,以获取 API KEY 和密钥,并填充到 AppDelegate.m 的下面一行代码中。(另外还请自备梯子)

[[FlickrKit sharedFlickrKit] initializeWithAPIKey:@"This is your API key" sharedSecret:@"This is the secret"];
复制代码

Scene

在进入主题之前,先来了解一下 Scene 对象。我们将一个页面描述为一个 Scene 对象,其代码如下:

@interface MIScene : NSObject

@property (nonatomic, copy, nonnull) NSString *viewName;             //!< view name
@property (nonatomic, copy, nonnull) NSString *controllerName;       //!< controller name
@property (nonatomic, copy, nonnull) NSString *storeName;            //!< store name

+ (instancetype _Nullable)sceneWithView:(NSString * _Nonnull)viewName controller:(NSString * _Nonnull)controllerName store:(NSString * _Nonnull)storeName;

@end
复制代码

这个类很简单,只是用来组合一个页面的三层对象。这个类并不是必须的,只是为了表达清晰。注意这里三个属性的类型都是 NSString,意味着我们将通过反射机制来创建一个 ViewController 对象及其关联的 StoreView(同时也意味了更多的硬编码,我们会在后面说明)。

具体的创建过程在 MIMediator 中,代码如下所示:

- (UIViewController *)viewControllerWithScene:(MIScene *)scene context:(NSDictionary<NSString *,id> *)context callback:(MICallback)callback {
    
    Class controllerClass = NSClassFromString(scene.controllerName);
    Class storeClass = NSClassFromString(scene.storeName);
    Class viewClass = NSClassFromString(scene.viewName);
    
    id<MIStore> store = [[storeClass alloc] initWithContext:context];
    
    return [[controllerClass alloc] initWithStore:store viewClass:viewClass callback:callback];
}
复制代码

MIMediator 是页面跳转的一个中介者,主要是负责横向数据流操作,在这不多解释。

因此,创建及使用一个 Scene 看起来是下面这样:

MIScene *scene = [MIScene sceneWithView:@"SearchView" controller:@"SearchViewController" store:@"SearchStore"];
    UIViewController *viewController = [[MIMediator sharedMediator] viewControllerWithScene:scene context:nil];
    UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:viewController];
复制代码

正如上一篇后面所说,ViewControllerView 层和 Store 层的了解仅限于一两个接口,而三层同时依赖于同一个 pipeline,这就意味着如果两个 View 都依赖于同一个 pipeline,那么这两个 View 可以相互替换,同理 ViewControllerStore 也一样。这样,我们就可以根据 pipeline 来拼装三层对象。即如果 View 调整了,但整体展示的数据还是那些,那我们的 ViewControllerStore 都不需要变动,在创建 Scene 时,我们换一个 View 名就行了。(当然这是一种理想状态)。

构造 pipeline

由于 ViewViewControllerStore 都是依赖于 pipeline,所以 pipeline 可以说是整个框架的核心。如何去构造 pipeline,将决定整个结构的灵活性。可以从两个角度来考虑这个问题:

  1. 根据 View 层来构造 pipeline

对于一个 App 来说,View 层是和用户直接交互的,它即是用户输入数据的来源,也是业务数据的表述。采集和显示哪些数据,都需要根据 View 来确定。另外,一个复杂的 View 层可能由多个甚至多层子 View 构成,每个子 View 有不同的数据需求,所以它能更精细地去表述数据,包括页面点击这种 flag 数据。因此,我们可以根据视图的树形结构,构造一棵类似的 pipeline 树形结构。如下图所示:

这种方案还有一个好处,每一个子视图只需要依赖于其对应的 pipeline 即可,而不需要依赖于整个 pipeline

不过这种方案的问题在于,一旦 View 变更,将直接影响到 pipeline 的构造,进而可能影响 ViewControllerStorepipeline 属性的监听。

  1. 根据业务来构造 pipeline

这种方案是把业务相关的一组数据整合在一个 pipeline 对象里面(一个业务可能对应多个 View),再把一个页面里面的多个 pipeline 组织成一棵 pipeline 树。

这种方式的优点是 pipeline 相对独立于 View层,除了一些 View 相关的数据外,pipeline 不会受 View 过多的影响。缺点是这种 pipeline 对数据的表述比较粗旷,View 层可以监听到一些与其无关的数据。

在实际开发过程中,可以根据实际情况来确定使用哪种方案构造 pipeline。构造 pipeline 的主要任务在 store 层中完成,因为这里是数据的处理中心。以 InterestingnessStore 为例,我们将 InterestingnessStore 的业务逻辑拆分到两个 store 中,每个 store 维护其自身的 pipeline,然后在 InterestingnessStore 中构建起 pipeline 的层级结构。

InterestingnessPipeline.h

@interface InterestingnessPipeline : MIPipeline

@property (nonatomic, strong) TopImagePipeline *imagePipeline;
@property (nonatomic, strong) PhotoListPipeline *photoListPipeline;

- (void)setShowImageAtIndex:(NSUInteger)index;

@end
复制代码

InterestingnessStore.m

@interface InterestingnessStore ()

@property (nonatomic, strong) InterestingnessPipeline *interestPipeline;    // Pipeline

@property (nonatomic, strong) TopImageStore *imageStore;                    // Top image store
@property (nonatomic, strong) PhotoListStore *photoStore;                   // Photo List store

@end

@implementation InterestingnessStore

// ...

- (InterestingnessPipeline *)interestPipeline {
    if (!_interestPipeline) {
        _interestPipeline = [[InterestingnessPipeline alloc] init];
        
        _interestPipeline.imagePipeline = self.imageStore.imagePipeline;
        _interestPipeline.photoListPipeline = self.photoStore.photoListPipeline;
    }
    return _interestPipeline;
}

@end
复制代码

以上 pipeline 层级结构与 View 的层级结构对应,即我们采用方案一来构造 pipeline

建立依赖

store 层中根据需求构造好 pipeline 后,就需要建立 ViewControllerView 层对 pipeline 的依赖了,这个过程并不复杂,但是比较繁琐。这一操作主要是通过 ViewController 向上分发 pipeline 来完成的。我们再来看看 MIViewController 的实现。


@implementation MIViewController

// ...

- (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

复制代码

- viewDidLoad 方法里面调用了两个 - setupPipeline

  • 第一个是 ViewController 自身的方法,这可以构建 ViewControllerpipeline 的依赖,当然如果 ViewControllerpipeline 没有数据需求,则子类可以不实现这个方法;
  • 第二个是 ViewController 的根视图的设置方法,这个方法将 pipeline 传递给 View 层,View 层可以根据实际需要再去设置自身的 pipeline,以及将子 pipeline 分发各子 View,例如 InterestingnessView 的实现:
@interface InterestingnessView ()

@property (nonatomic, strong) InterestingnessPipeline *pipeline;

@property (nonatomic, strong) TopImageView *topImageView;
@property (nonatomic, strong) PhotoListView *photoListView;

@end

#pragma mark - InterestingnessView implementation

@implementation InterestingnessView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self addSubview:self.topImageView];
        [self addSubview:self.photoListView];
        
        // ...
    }
    return self;
}

- (void)setupPipeline:(__kindof MIPipeline *)pipeline {
    self.pipeline = pipeline;
    
    // 分发二级 pipeline
    [self.topImageView setupPipeline:self.pipeline.imagePipeline];
    [self.photoListView setupPipeline:self.pipeline.photoListPipeline];
}

// ...

@end
复制代码

至此,ViewControllerViewpipeline 的依赖关系构建完成。构建完成后,各层就只需要与 pipeline 打交道了,从 pipeline 中读取数据,或者把数据写入 pipeline 中。

数据传输

最后来看看最主要的部分:数据传输。回到最上面的 Demo,我们在第二个页面中点击列表中的一个单元格,顶部的图片信息就跟着变化。我们来看看这种变化时如何产生的。我们先从目标视图即 TopImageView 说起,这个 View 监听了 TopImagePipelineurl 属性:

// Observe `url` property of the pipeline
    [MIObserve(self.pipeline, url) changed:^(id  _Nonnull newValue) {
        @strongify(self)
        [self mi_updateView];
    }];
复制代码

也就是说,只要这个 url 发生了改变,TopImageView 就能监听到这种改变并相应地去更新视图,而至于是谁触发了这种变更,TopImageView 并不关心,同时 TopImageView 并没有提供额外的接口让父视图或者 ViewController 来调用。

这里有一点需要行说明一下,Flickrflickr.photos.getRecent 接口(获取列表)并没有返回图片的 url,所以我们需要通过图片的 photoID,再去调用一次 flickr.photos.getSizes 接口,以获取图片 url,于是便有了这边繁琐的流程。正好也根据这个示例来说明问题。

我们再来看看事件的发起源 photoListView,在 PhotoListView 中,选中 table view 的操作只有一行代码:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    self.pipeline.inputSelectedPhotoIndex = indexPath.row;
}
复制代码

然后,这里 PhotoListView 只是简单的设置了它的 pipelineinputSelectedPhotoIndex 属性,然后就没有任何操作了。也就是说 PhotoListView 仅仅重新设置了所选的行索引,后面如何处理便和 PhotoListView 没有关系了。

额,然后好像就断线了。两者是怎么连接起来的呢?我们来看看谁监听了 inputSelectedPhotoIndex 属性。全局搜索,我们可以看到 InterestingnessStore 对象监听了 inputSelectedPhotoIndex 属性:

[MIObserve(self.interestPipeline.photoListPipeline, inputSelectedPhotoIndex) changed:^(NSNumber * _Nonnull newValue) {
        
    @strongify(self)
    [self.interestPipeline setShowImageAtIndex:[newValue integerValue]];
}];
复制代码

InterestingnessPipelinesetShowImageAtIndex 方法(这个方法只做简单的处理)中,重新设置了其子 imagePipelinephotoID 属性。

然后 TopImageStore 监听了 imagePipelinephotoID 属性,

[MIObserve(self.imagePipeline, photoID) changed:^(NSString * _Nonnull newValue) {
    @strongify(self)
    [self fetchData];
}];
复制代码

并依此发起网络请求,最终获得图片的 url

- (void)fetchData {
    
    // ...
    
    @weakify(self)
    [self.getSizesService requestWithParameters:@{@"photo_id": self.imagePipeline.photoID} success:^(NSString * _Nullable data) {
        @strongify(self)
        self.imagePipeline.url = data;
    } fail:^(id  _Nullable data, NSError * _Nullable error) {
        // You can do something if the request failed.
    }];
}
复制代码

在此更新 imagePipelineurl 后,TopImageView 监听到这一变更,并从 imagePipeline 中获取到 url 后,去加载图片。至此整个流程打通。我们画册个简单的图来梳理一下流程。

整个过程中,每个对象只关心自己需要监听 pipeline 中哪些属性并做出相应的操作,以及执行某些操作后去修改 pipeline 对应的属性,而不必去理会其它事情,专心做好自己的事实就好。不过这种特性也带来一些不好的后果,你可能已经发现,在后一篇中也会讲到这个问题。

小结

在这一篇中,我们结合 Demo 实践了如何去构造 pipeline,建立三层对象对 pipeline 的依赖,以及建立依赖后,如何实现各层之间的数据通信。实际上整个流程并不复杂,只是第一次看可能有些地方不太好理解。

由于这套框架并没有太多的实战,所以更多的是一个实验性的不成熟框架。其中还有一些细节,由于篇幅限制,在此不多做介绍,有兴趣可以看看源码,一起探讨。另外我自己后来总结了一下,还存在很多问题,这些问题我会在下一篇中说明。

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

知识小集公众号

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值