iOSiOS架构方案

03--(三)MVVM项目结构解读

2021-09-27  本文已影响0人  修_远

一、概述

每个人的开发风格都不相同,于是在交接其他人的代码的时候,就要去熟悉别人的开发习惯,阅读别人的思维模式,这个过程的体验想必都深有体会。如果有一种固定的开发模板,每个人都只需要按照这个模板填写代码,尽管开发风格迥异,但仍能最快找到约定的代码。ProtocolChain刚好可以做到这一点,它只解决各个节点之间消息传递的问题,所以要使用它就必须要先构造出一个树形结构的架构,而树形结构的构造就需要参照特定的模板。

  1. 标准的MVVM项目依赖树,如下图;
  2. 固定的结构可以使用模板代码;
  3. 固定的模板便于代码的审查;
  4. 单一的职责更容易维护、组合、扩展;
模块结构.png

整体原则

  1. 下层对上层提供依赖;
  2. 右侧对左侧提供依赖;
项目结构

职责说明

Dependency(依赖注入层)

1、作用:返回模块的入口ViewController,内部完成ViewModel、DataStore的依赖注入;
2、说明:除了View的创建,Controller、ViewModel、DataStore、Model等职责的创建和装配。譬如外部传进来的初始数据,可能需要DataStore或Model来接受,外部传入的Block,可能需要ViewModel来接受。依赖注入的方式可以隔离业务逻辑与视图层,对复用、替换、扩展都会带来非常友好的支持。
3、实现案例:

+ (UIViewController *)createMVVMProViewController:(NSString *)testName
                                            model:(nonnull id<XYMVVMProDataModel>)testModel
                                            block:(nonnull void (^)(NSString * _Nonnull))testBlock {
    XYMVVMProDataStore *dataStore = [[XYMVVMProDataStore alloc] initWithModel1:testModel];
    XYMVVMProViewModel *viewModel = [[XYMVVMProViewModel alloc] initWithDataStore:dataStore block:testBlock];
    XYMVVMProViewController *vc = [[XYMVVMProViewController alloc] initWithViewModel:viewModel];
    return vc;
}

4、调用案例
一般来说,模块外的接入者并不需要知道模块控制器叫啥名字,也不需要知道DataStore、ViewModel分别需要什么样的参数,只需要关注依赖注入层提供的参数列表,而拿到的也仅仅是一个纯粹的UIViewController,可以打开这个模块即可。

UIViewController *vc = [XYMVVMProDependencyFactory createMVVMProViewController:@"张三"
                                                                             model:model
                                                                             block:^(NSString * _Nonnull desc) {
        NSLog(@"%@", desc);
}];
[self.navigationController pushViewController:vc animated:YES];

Controller(核心控制层)

1、系统提供的视图生命周期、导航、状态栏相关权限;

- (instancetype)init;
- (void)dealloc;

- (void)loadView;
- (void)viewDidLoad;

- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated; 
- (void)viewWillDisappear:(BOOL)animated;
- (void)viewDidDisappear:(BOOL)animated; 

2、作为树的根节点,关联View与ViewModel
这里命名actionResponder事件响应者,表示指定View的事件响应者为ViewModel,数据流方向是从View->ViewModel,从模板化

self.proView.actionResponder = self.proViewModel;
self.proView.proView1.actionResponder = self.proViewModel.proViewModel1;
self.proView.proView2.actionResponder = self.proViewModel.proViewModel2;

3、构造协议响应链,分发消息

  1. 按照下面的协议响应链的格式,可以将消息发送到树的任何一个节点;
  2. 可以将所有点击消息都发送给traceServertraceServer在初始化的时候可以指定dataStore,如此便可将埋点的代码都隔离到traceServer中;

协议响应链一

  1. 指定@protocol(XYMVVMProActionRespondable)的第一响应者为self.proViewModel,需要实现协议中的所有方法,并都是第一响应者;
  2. 为协议中的方法@selector(mvvmViewActionCallback)构造响应链,后续响应链为self
  3. 为协议中的方法@selector(mvvmViewActionCallback)构造响应链,后续响应链为self. traceServer
self.bind(@protocol(XYMVVMProActionRespondable), self.proViewModel)
    .link(self, @selector(mvvmViewActionCallback))
    .link(self.traceServer, @selector(mvvmViewActionCallback))
    .close();

从日志中可以看出方法mvvmViewActionCallback的响应链为:

XYMVVMProViewModel -> XYMVVMProViewController -> XYMVVMProTraceServer

[ProtocolChain] <XYMVVMProActionRespondable>    【XYMVVMProViewModel】
    [0][ResponderChain][mvvmViewActionCallback] 
        -> XYMVVMProViewController
        -> XYMVVMProTraceServer

协议响应链二

  1. 指定@protocol(XYMVVMProActionRespondable1)的第一响应者为self.proViewModel.proViewModel1,需要实现协议中的所有方法,并都是第一响应者;
  2. 为协议中的方法@selector(mvvmView1ActionCallback)构造响应链,后续响应链为self
  3. 为协议中的方法@selector(mvvmView1ActionCallback)构造响应链,后续响应链为self. traceServer
self.bind(@protocol(XYMVVMProActionRespondable1), self.proViewModel.proViewModel1)
    .link(self, @selector(mvvmView1ActionCallback))
    .link(self.traceServer, @selector(mvvmView1ActionCallback))
    .close();

从日志中可以看出方法mvvmView1ActionCallback的响应链为:

XYMVVMProViewModel -> XYMVVMProViewController -> XYMVVMProTraceServer

[ProtocolChain] <XYMVVMProActionRespondable1>   【XYMVVMProViewModel1】
    [0][ResponderChain][mvvmView1ActionCallback]    
        -> XYMVVMProViewController
        -> XYMVVMProTraceServer

协议响应链三

  1. 指定@protocol(XYMVVMProActionRespondable2)的第一响应者为self.proViewModel.proViewModel2,需要实现协议中的所有方法,并都是第一响应者;
  2. 为协议中的方法@selector(mvvmView2ActionCallback)构造响应链,后续响应链为self
  3. 为协议中的方法@selector(mvvmView2ActionCallback)构造响应链,后续响应链为self. traceServer
self.bind(@protocol(XYMVVMProActionRespondable2), self.proViewModel.proViewModel2)
    .link(self, @selector(mvvmView2ActionCallback))
    .link(self.traceServer, @selector(mvvmView2ActionCallback))
    .close();

从日志中可以看出方法mvvmView2ActionCallback的响应链为:

XYMVVMProViewModel -> XYMVVMProViewController -> XYMVVMProTraceServer

[ProtocolChain] <XYMVVMProActionRespondable2>   【XYMVVMProViewModel2】
    [0][ResponderChain][mvvmView2ActionCallback]    
        -> XYMVVMProViewController
        -> XYMVVMProTraceServer

思考,ViewModel如何将消息发送给View?

View(视图显示层)

1、头文件结构

1、非叶子节点

@property (nonatomic, weak) id<XYMVVMProActionRespondable> actionResponder;
@property (nonatomic, strong, readonly) XYMVVMProView1 *proView1;
@property (nonatomic, strong, readonly) XYMVVMProView2 *proView2;

2、叶子节点

@property (nonatomic, weak) id<XYMVVMProActionRespondable1> actionResponder;

只需要提供actionResponder:事件响应者,为视图数据的输出以及输入提供通道即可。

2、实现文件结构

1、视图初始化

这三部分是最基础的视图显示需要的步骤,而如果视图本身需要修改,例如文本、颜色、位置大小等属性的变化,view便需要一个异步操作的注入,这里通过Block来实现。

- (void)setActionResponder:(id<XYMVVMProActionRespondable1>)actionResponder {
    _actionResponder = actionResponder;
    
    __weak typeof(self) weakSelf = self;
    _actionResponder.updateColorBlock = ^(UIColor * _Nonnull color) {
        weakSelf.backgroundColor = color;
    };
    
    _actionResponder.getColorBlock = ^UIColor * _Nonnull{
        return self.backgroundColor;
    };
}

ViewModel(逻辑处理层)

主要作用
  1. 业务逻辑的处理,数据获取的发起、数据流转、视图刷新、视图跳转等业务逻辑;
  2. ViewModel同样也是树的结构,对于非叶子节点的ViewModel下,会挂多个子ViewModel,可以集中处理多个子ViewModel的共同逻辑,可以处理数据需要在子ViewModel中多次来回流转的逻辑(隔离子ViewModel之间的直接沟通);
  3. 子ViewModel,不是所有的业务逻辑都需要提升到父ViewModel中去处理,往往在子ViewModel中即可处理完成,形成闭环;
关联职责
  1. View:作为View的事件响应者,需要处理View传出来的事件,在合适的地方调用Block,触发View的异步刷新;
  2. DataStore:作为ViewModel的初始化属性,一般设置为只读属性,在各个ViewModel之间传递,数据的存储以及部分数据的组装;
  3. Server:服务层,往往提供单一的服务。例如网络请求Server提供数据的获取和部分数据组装的能力,埋点Server提供埋点的能力,图片处理Server提供图片的合成、裁剪、渲染等能力;
文件结构
  1. 一般不直接存储数据,只管理数据;
  2. 所以注入的属性都需要设置成只读属性,例如从Dependency层注入的block、dataStore,所有的子ViewModel;
  3. 实现文件中往往会有多个协议的方法,只需要按需添加方法,在Controller中link即可,并不需要增加属性;
@interface XYMVVMProViewModel : NSObject<XYMVVMProActionRespondable>
@property (nonatomic, strong, readonly) XYMVVMProViewModel1 *proViewModel1;
@property (nonatomic, strong, readonly) XYMVVMProViewModel2 *proViewModel2;
@property (nonatomic, strong, readonly) XYMVVMProDataStore *dataStore;
@property (nonatomic, copy, readonly)   void (^testBlock)(NSString * _Nonnull);
- (instancetype)initWithDataStore:(XYMVVMProDataStore *)dataStore block:(nonnull void (^)(NSString * _Nonnull))testBlock;
@end
与MVC的对比

传统的MVC(胖Model)往往会把数据的获取、组装、存储都放在Model中,这样会增加对Model的理解困难,Model更重要的职责是其所描述的特征。尽管如此,Controller仍然避免不了异常的庞大,所有View的事件处理,View的刷新、数据的流转等职责仍然很繁重。不合理的MVC往往会直接把逻辑处理放在View里面,最后的最后,你中有我我中有你,难舍难分!

上面介绍的结构,不是从某种特定架构出发,而是从单一职责出发

在满足上述前提的条件下,只需要解决几个问题:

  1. ActionOutput:View如何将事件传递出去;
  2. DataInput:Data如何渲染到View;
  3. DataStream:Data是如何流转(DataStore、NetServer、ViewModel);

ActionOutput和DataInput比较简单,都是一对一的逻辑。DataStream数据流往往涉及多个职责之间的共同协作,数据的获取、组装、存储,因此需要有一个通道来流转这些数据。如此便可以通过ProtocolChain来进行分发。

DataStore(数据存储层)

1、头文件结构

@property (nonatomic, strong, readonly) id<XYMVVMProDataModel> model1;
@property (nonatomic, strong, readonly) XYMVVMProModel2 *model2;
- (instancetype)initWithModel1:(id<XYMVVMProDataModel>)model1;

XYMVVMProViewModel:根ViewModel

@interface XYMVVMProViewModel : NSObject<XYMVVMProActionRespondable>
@property (nonatomic, strong, readonly) XYMVVMProViewModel1 *proViewModel1;
@property (nonatomic, strong, readonly) XYMVVMProViewModel2 *proViewModel2;
@property (nonatomic, strong, readonly) XYMVVMProDataStore *dataStore;
@property (nonatomic, copy, readonly)   void (^testBlock)(NSString * _Nonnull);
- (instancetype)initWithDataStore:(XYMVVMProDataStore *)dataStore block:(nonnull void (^)(NSString * _Nonnull))testBlock;
@end

XYMVVMProViewModel1:子ViewModel

@interface XYMVVMProViewModel1 : NSObject<XYMVVMProActionRespondable1>
@property (nonatomic, strong, readonly) XYMVVMProDataStore *dataStore;
- (instancetype)initWithDataStore:(XYMVVMProDataStore *)dataStore;
@end

XYMVVMProViewModel2:子ViewModel

@interface XYMVVMProViewModel2 : NSObject<XYMVVMProActionRespondable2>
@property (nonatomic, strong, readonly) XYMVVMProDataStore *dataStore;
- (instancetype)initWithDataStore:(XYMVVMProDataStore *)dataStore;
@end

2、实现文件结构
初始化相关的方法往往可以做成固定的模板

1、私有可读可写属性的定义;
2、指定初始化方法的实现;
3、私有属性的懒加载;

除了对数据的存储,DataStore也会分担部分数据的组装。用好getter方法,可以对数据的变化更进一步的抽象。例如,可以对ViewModel增加只读属性:
@property (nonatomic, assign, readonly) BOOL isGetUserInfo;
如此便可以将业务的条件封装在DataStore中,调用方并不需要关心内部是根据什么判断的,判断的那些条件。这里仅仅是作为一个简单的🌰,如果有更加复杂的条件,不妨考虑策略模式,可能会提供更多的选择。

- (BOOL)isGetUserInfo {
    return (self.model1 && self.model1.name && self.model1.address);
}

Model(数据描述层)

Model最主要作用是对数据的描述,换成代码的语言,指类的公有属性,指示数据的含义。

Model一般也会作为数据传输的单元在多个模块之间传递。通常情况下,定义在A模块中的ModelA不希望被B模块直接引用(也可能是因为不同库封装的问题)、或者B模块可以引用到ModelA,但是希望对ModelA进行一些修改。这种case是非常场景的,一旦ModelA同时兼容了A模块和B模块的某种特性,那么ModelA便被污染了。

万能的解决这种问题的方法是中间层,“所有的计算机问题都可以用中间层解决”,这是计算机大佬提出来的观点,这里就不做过多的评价了。

常规的解决思路是数据转换,在A模块中奖ModelA转成字典DictionaryA,B模块接受了DictionaryA,并转成成自己的数据类型。在C/S模式下,A模块作为Server端,B模块作为Client端,类似于客户-服务端的通信模式,传输的都是json数据。这种方式自然能解决数据交互的问题,但同样的数据需要进行两次转换才能被使用,而且是面向字符串约定,任何一端的修改都会造成数据解析失败的问题,且问题还不会立马被发现,需要在运行时才能确定问题所在。

优雅的解决思路是面向协议,A模块并不直接传递ModelA,而是传递一个抽象类型id<ModelA>,而B模块接收到id<ModelA>,如果不需要修改,可直接使用,若要修改,完全可以用自己模块内的数据接收:ModelB<ModelA>。这个时候也仅仅是A模块和B模块同时对抽象<ModelA>的依赖,并不会导致某个实例对象被污染。

抽象类型id<ModelA>往往也是存在依赖的,使用场景也存在一定局限性,适用于同一个项目、组件内部的模块之间的调用。如果是跨组件的调用,这种方式仍然要求调用方依赖提供方的某种抽象类型id<ModelA>,对组件解耦来说是不太友好的一种方式。要解决这个问题,其实也非常简单,在Target-Action体系中,我们只需要在Target实现Action方法中来调用Dependency层的方法即可,或者是在分类中提供接口支持,仍然可以保证模块内部的完整性。

Server(服务提供层)

举个🌰,基础框架提供的图片处理是:切圆角重设图片大小图片拼接三个基础的操作,而业务要去是,不同大小的图片要拼接成一张图片,且要切圆角。所以可以在ImageServer中提供一个新的API,兼容这三个操作。

低业务关联,而不是完全不关联业务。只有在当前模块中才会需要有这种图片操作,因此是业务关联的。而这个API又可以脱离业务由其他任何地方调用,因此是低业务关联性的。

独立可替换性,这个图片操作是完全独立,不依赖MVVM架构中的任何职责,内部实现也是完全不透明的,可能是CoreImage、也可能是BitMap、或者是某个图片处理框架。随着系统变更,也许会有更先进、高效的手段出现,到时候只需要更新内部的实现即可,丝毫不影响业务逻辑。

操作复杂性,上面的操作如果放在ViewModel中,势必会造成ViewModel体积的变大,增加了一大堆非业务逻辑的代码,让本就复杂的代码更加雪上加霜。

上面的一个例子是ImageServer,常见的Server还有网络相关的。

NetServerMockServer,NetServer提供访问数据库的能力(也可以在这里进行部分数据的组装,返回加工好的数据)。然而,一个新的迭代开始阶段,后台仅仅提供了一份接口文档,当你需要的时候可能接口都没有通,程序员自然是有非常多的解决办法。本地搭建一个Mock服务,然后对每个接口都Mock数据并返回,这个要求稍微有点高,要能搭建其Mock服务且非常熟练Mock的API,除此之外,可能仍需耗费比较多的时间。另外一种使用在线的MockAPI,但前提是公司的网关没有限制,可以随意访问MockAPI,而对网络安全要求比较高的公司,可能还是需要手写数据来完成Mock,而NetServer中写好的数据解析的逻辑肯定是不会有人想要去打乱了,因此MockServer便站出来了,让NetServerMockServer同时遵循协议<xxxNetInterface>,在调用的地方并不直接使用NetServer类来创建对象,而是创建一个抽象类型id<xxxNetInterface>,因此可以在需要Mock数据的时候,将NetServer改成MockServer,在MockServer中返回手写的数据即可。

TraceServer,业务代码中出现上百行的埋点代码,这无疑是一种灾难。而埋点的存在仅仅是在用户触发某个操作的时候记录一下而已,完全是应该要脱离业务的,埋点仅仅需要时机+数据即可

上面介绍过了DataStore的职责,所有的数据都存储在这个对象中,所以我们可以在TraceServer的初始化中,将DataStore传递进去即可:

- (XYMVVMProTraceServer *)traceServer {
    if (!_traceServer) {
        _traceServer = [[XYMVVMProTraceServer alloc] initWithDataStore:self.proViewModel.dataStore];
    }
    return _traceServer;
}

在介绍Controller时,已经将事件传递给了TraceServer,当TraceServer同时获取到时机+数据后,便可以从业务代码中隔离出来。

self.bind(@protocol(XYMVVMProActionRespondable), self.proViewModel)
    .link(self, @selector(mvvmViewActionCallback))
    .link(self.traceServer, @selector(mvvmViewActionCallback))
    .close();

self.bind(@protocol(XYMVVMProActionRespondable1), self.proViewModel.proViewModel1)
    .link(self, @selector(mvvmView1ActionCallback))
    .link(self.traceServer, @selector(mvvmView1ActionCallback))
    .close();

self.bind(@protocol(XYMVVMProActionRespondable2), self.proViewModel.proViewModel2)
    .link(self, @selector(mvvmView2ActionCallback))
    .link(self.traceServer, @selector(mvvmView2ActionCallback))
    .close();

还有更多的Server,满足上面三个特性,我们便可以将其抽离出来,以减少ViewModel的工作量,以封装某种变化,以提供某种扩展。

Protocol(协议提供层)

对协议比较好的分类应该是三种协议,分别对应上面介绍的:

  1. ActionOutput:事件输出协议
  2. DataInput:数据输入协议
  3. DataStream:数据流转协议

因为block的特性,这里便定义了两种:事件响应协议数据模型协议

xxxActionRespondable

常规的命名规则后缀是:xxxDelegate,而Delegate这个词汇本身并无法表示任何含义,所以不太合适出现在当前架构中,我们用xxxActionRespondable代替,表示可响应的事件。而在View中定义的属性也不再使用delegate这里没有具体含义的命名,用actionResponder代替,表示事件响应者。例如:@property (nonatomic, weak) id<XYMVVMProActionRespondable1> actionResponder;。下面是一些事件响应协议定义的举例:

@protocol XYMVVMProActionRespondable <NSObject>
- (void)mvvmViewActionCallback;
@property (nonatomic, copy) void (^hideView2Block)(BOOL isHidden);
@end


@protocol XYMVVMProActionRespondable1 <NSObject>
- (void)mvvmView1ActionCallback;
@property (nonatomic, copy) void (^updateColorBlock)(UIColor *color);
@property (nonatomic, copy) UIColor *(^getColorBlock)();
@end


@protocol XYMVVMProActionRespondable2 <NSObject>
- (void)mvvmView2ActionCallback;
@end

【同名冲突】众所周知,oc中没有命名空间这一概念,所以的差异都需要通过命名来直接区分,因此不同协议中定义相同的方法名便有可能造成歧义。

@protocol XYMVVMProActionRespondable <NSObject>
- (void)mvvmViewActionCallback;
@end


@protocol XYMVVMProActionRespondable1 <NSObject>
- (void)mvvmViewActionCallback;
@end

上面的两个协议中都有mvvmViewActionCallback方法,当一个类同时遵循这两个协议的时候,便无法区分这个事件是从哪个对象发出来的,所以需要通过命名的方式区分开来。

xxxModel

在将Model层的时候,介绍过面向抽象所解决的问题,这里就不再累述,可以看下定义:

@protocol XYMVVMProDataModel <NSObject>
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *address;
@end

多个事件协议可能会放到一个文件中(具体执行需要按需斟酌),但数据协议必须要与事件协议分开,很多情况下,数据协议都需要随着Dependency层一起对外公开,例如:

#import <Foundation/Foundation.h>
#import "XYMVVMProDataModel.h"

NS_ASSUME_NONNULL_BEGIN

@interface XYMVVMProDependencyFactory : NSObject
+ (UIViewController *)createMVVMProViewController:(NSString *)testName
                                            model:(nonnull id<XYMVVMProDataModel>)testModel
                                            block:(nonnull void (^)(NSString * _Nonnull))testBlock;
@end

NS_ASSUME_NONNULL_END

block

09-异步回调(block).png
- (void)setActionResponder:(id<XYMVVMProActionRespondable1>)actionResponder {
    _actionResponder = actionResponder;
    
    __weak typeof(self) weakSelf = self;
    _actionResponder.updateColorBlock = ^(UIColor * _Nonnull color) {
        weakSelf.backgroundColor = color;
    };
    
    _actionResponder.getColorBlock = ^UIColor * _Nonnull{
        return self.backgroundColor;
    };
}
@implementation XYMVVMProViewModel1
@synthesize getColorBlock=_getColorBlock;
@synthesize updateColorBlock=_updateColorBlock;

因为block的调用是invoke(func),并非msg_send(),所以不需要依赖任何对象即可完成数据的异步刷新。


源码地址

【后记】一直在想着怎么不依赖响应式框架RAC来完成MVVM的数据绑定,也模仿RAC写过简单的MVVM框架,但相关类仍然需要依赖自定义的信号,才可以完成订阅。一旦框架出现某种不可修改的缺陷或没人维护的时候,在替换的时候需要修改的地方太多,存在的风险过大;基于消息转发实现了ProtocolHook能力;参照响应链的方式构造了协议响应链;结合RN中Redux的思想,将需要转发的事件都挂载在根节点controller上,需要消费的对象,在controller中接受事件即可;面向协议依赖是一种依赖抽象的思想,所以可以很好的拆分、替换、复用;因为是借助runtime的能力动态去转发,所以不会破坏任何类的原始结构,都是objc中最原始的用法。假如框架出现不可修改的缺陷或者没人维护时,也可以重新建立代理关系,即可完成消息链路的构造,并不会影响到业务逻辑的修改。

上一篇下一篇

猜你喜欢

热点阅读