常用架构模式

一.架构模式

1.引言

架构模式,用于描述软件系统里的基本的结构组织或纲要。架构模式提供了一些定义好的子系统,指定它们的责任,并给出把它们组织在一起的法则和指南。常见的架构模式有:分层模式、微核模式、管道与过滤器、MVC模式、REST模式、SOA模式。

2.起源

软件开发中需求变动往往导致代码的修改,而一些代码尤其是大型系统的码理解起来并不容易,只有当时的开发者比较清楚,其他人很难接手。修改这种代码时往往会碰到“触一发而动全身”的问题,因为有“代码耦合”问题。代码耦合让整个系统变得难以理解、修改、分工、集成。针对耦合问题软件界进行了大量的理论研究和实践,最后发现:系统的架构设计,是改善耦合的最好方式~

3.特点

优秀架构模式的共同点:

  • 任务均衡分摊给具有清晰角色的实体;
  • 高内聚、低耦合;
  • 高重用性、低维护成本;
  • 可单独测试。

4.区分

区分两个名词:架构模式设计模式

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式一共有23种,常见的有工厂模式、单例模式、代理模式、观察者模式、策略模式等。

一个构架往往用到多个设计模式,如mvc架构是观察者模式、策略模式和组合模式的演变。

传统MVC

上图是传统的MVC架构,项目中的代码被整合为视图模型控制器三个角色。

  • 当用户操作一个视图时,会产生一个事件并交给控制器;
  • 控制器收到事件后会采用一种策略:要求模型更新数据的状态或者要求视图更新样式;
  • 模型中数据变化后则会通知视图更新界面。

二.MVC

1.Apple版MVC

上图是传统的MVC模式,虽然也区分了三种角色,但是它们互相之间都有耦合,尤其是模型和视图之间。而视图和模型往往是最需要复用的,所以,最合理的设计是保持二者的独立性。因此 Apple 设计了自己的MVC版本,通过控制器来实现视图与模型的解耦。

2.角色划分

cocoaMVC

模型

持有数据、封装处理数据的业务,为控制器提供数据接口。模型不应与视图层有任何关联。

视图

展示数据和信息并允许用户编辑数据。

视图应像UIButton一样可复用、可配置并保持统一性。

视图不能绑定模型,所以视图需要一种机制来知晓模型中数据的变化。

控制器

控制器作为中间人连接模型视图

视图提供需要显示的数据;

响应用户的操作并调用模型中的业务更新数据;将数据的变化通知给视图。

控制器也用来管理对象的生命周期,定义相关业务以实现一些设置和指定的任务。

3.现实中的MVC

理想中视图与控制器应相互独立,但实际开发过程中视图层往往紧密耦合在控制层里:控制器要负责维护视图的生命周期,响应用户在视图层的操作,实现诸如Tableview等视图的代理等,MVC事实上变成了M-VC

理想中视图与模型应相互独立,但实际中可能会遇到这总情形:初始化Cell时经常会直接传入Model对象,形成视图与模型的耦合。

1
2
3
4
5
6
7
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Goods"];
cell.goods = self.dataSource[indexPath.row]; //绑定Model
return cell;
}

理想中模型应持有数据、封装处理数据的业务、提供数据接口,但实践中模型被简单的设计为只有一些属性的类,数据相关的业务都被放在了控制器中。当需要修改数据的业务逻辑时,如更换网络通讯库、更新json解析时的某个字段,就需要在控制器中进行修改。

理想中上面的Cell不应引用模型对象,那么Cell的设置就要放到控制器中,这会增加控制器中的代码量。另外像网络请求、工具方法等不应放在视图和模型层里,而放在控制器里也会造成控制器的臃肿。这些就是Massive View Controller的由来,形象表示如下:
mass

当业务逻辑和展示逻辑都在控制器中时,很难进行单元测试。所以控制器中只应存放一些不能复用的代码,其他代码尽量分离出去。

不能复用的代码包括:

  • 初始化View 和 Model的语句;
  • 根据View 层用户操作调用 Model 层处理数据的语句;
  • 用来接收 Model 层数据的回调、代理,和通知 View 层更新视图的逻辑;

可分离的代码包括:

  • 在自定义类中搭建视图结构,通过代理将用户操作回调给控制器;
  • 将诸如TableView的代理抽离到单独的类中(参考AFN中请求回调的处理方式);
  • 将转换 Model 层回调数据的业务抽离到单独的类中;

三.MVP

1.角色划分

iOS架构模式-MVP

模型

与 MVC 中的模型类似。

视图

由”视图+控制器“组成,作用:

界面元素布局和执行动画;

将用户操作事件交给P层处理并展示处理结果;

P层中的业务逻辑处理完毕后通过代理告知视图刷新界面;

控制器负责生成视图,实现视图的代理和数据源以及界面的跳转等。

P层

作为视图模型的中间人,实现视图与模型的解耦:

定义响应视图中事件的接口,实现事件对应的处理逻辑;

调用模型的接口以获取数据,加工数据并将其封装成视图适用的数据和状态。

MVP 由 MVC 演化而来,它对 MVC 中的控制器进行了优化而生成PresenterP层与 MVC 中的控制器一样负责核心逻辑。不一样的是P层通过接口和协议传递数据,从而使视图和模型解耦,更加专注于自身业务逻辑。因为主要的逻辑在P层,所以可以方便的对其进行单元测试。

2.示例:

一个简单场景:请求加载一页数据;进度指示器根据加载状态显示/隐藏。

V层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#import "ViewController.h"
#import "Presenter.h"
#import "UserViewProtocol.h"

@interface ViewController ()
<UserViewProtocol,
UITableViewDataSource>

@property (nonatomic,strong) NSArray *friendlyUIData;
@property (nonatomic,strong) Presenter *presenter;
@property (weak, nonatomic) IBOutlet UITableView *tableview;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *indicator;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
self.presenter = [Presenter new]; //持有P层对象
[self.presenter confDelegate:self]; //设置代理
[self.presenter fetchData]; //向P层发送命令
}

#pragma mark -Tableview datasource
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return self.friendlyUIData.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.textLabel.text = [self.friendlyUIData[indexPath.row]
valueForKey:@"name"];

return cell;
}

#pragma mark -UserView Protocol

//P层的代理方法
-(void)userViewDataSource:(NSArray*)data
{
self.friendlyUIData = data; //通过代理回调,收到P层返回的数据
[self.tableview reloadData]; //更新UI
}

-(void) showIndicator
{
self.indicator.hidden = NO;
}

- (void) hideIndicator
{
self.indicator.hidden = YES;
}

ViewControllertableview构成了 MVP 中的视图层,负责界面的显示和更新。它实现了tableview的协议及用户交互的协议UserViewProtocol,等待 Presenter的命令被动更新UI。

P层
1
2
3
4
5
6
7
8
//Presenter.h
#import <Foundation/Foundation.h>
#import "UserViewProtocol.h"

@interface Presenter : NSObject
-(void)confDelegate:(id <UserViewProtocol>)view;
-(void)fetchData;//模拟获取网络数据
@end

Presenter中定义外部接口,供View层调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//Presenter.m
#import "Presenter.h"
#import "UserService.h"

@interface Presenter()
@property (nonatomic,strong) UserService *userService;
@property (nonatomic,weak) id <UserViewProtocol> mDelegate;
@end

@implementation Presenter

- (void)confDelegate:(id<UserViewProtocol>)view
{
self.mDelegate = view;
self.userService = [UserService new];
}

-(void)fetchData{
[self getUserDatas];
}

-(void)getUserDatas{
[self.mDelegate showIndicator];
//调用M层,模拟网络请求
[_userService getUserInfosSuccess:^(NSDictionary *dic)
{
//获取到数据 通知View层更新UI
[self.mDelegate hideIndicator];
[self.mDelegate userViewDataSource:[self processOriginDataToUIFriendlyData:userArr]];
} andFail:^(NSDictionary *dic) {
}];
}

//处理数据,输出成UI需要的数据
-(NSArray *)processOriginDataToUIFriendlyData:(NSArray *) originData
{
//略..
return friendlyUIData;
}
@end

其中的-confDelegate方法中有两个作用:

  • 将实现了UserViewProtocol协议的对象绑定到 Presenter 上。
  • 持有一个M层对象(UserService)以发起网络请求。
1
2
3
4
5
6
7
8
9
10
//UserViewProtocol.h
#import <Foundation/Foundation.h>

@protocol UserViewProtocol <NSObject>

-(void) userViewDataSource:(NSArray*)data;
-(void) showIndicator;
-(void) hideIndicator;

@end

这个协议是P层的一部分,其中定义的方法就是P层视图层发送的命令。View层实现了此协议,在Model层拿到数据并通过block返回给P层后,P层会通过代理,通知View层更新UI。

M层
1
2
3
4
5
6
7
8
9
//UserService.h
#import <Foundation/Foundation.h>
typedef void(^SuccessHandler)(NSDictionary *dic);
typedef void(^FailHandler)(NSDictionary *dic);

//Model层,用来发起网络请求并返回数据给Presenter。
@interface UserService : NSObject
-(void)getUserInfosSuccess:(SuccessHandler )success andFail:(FailHandler) fail;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//UserService.m
#import "UserService.h"

@implementation UserService

-(void)getUserInfosSuccess:(SuccessHandler )success
andFail:(FailHandler) fail
{
NSArray *result =@[@{@"name":@"Tom",@"age":@25}];
dispatch_after(dispatch_time(
DISPATCH_TIME_NOW,
(int64_t)(2 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
success(@{@"data":result});
});
}
@end

UserService属于M层,用来请求数据给P层。当M层数据发生变化时(如网络请求返回数据),通过block回调给P层P层再将处理后的数据通过代理反馈给View层并由后者更新界面。

使用 MVP 会增加代码量,但是总体上职责清晰,各层通过定义的API、代理或block回调进行通信,减少了代码的复杂性。

三.MVVM

1.角色划分

image

模型

与 MVP 中的模型类似。

视图

View+Controller组成,负责显示UI。

绑定ViewModel中的属性,触发ViewModel中的命令。

ViewModel

负责用户输入内容的验证逻辑(如用户名、密码的合法性);

视图显示逻辑;暴露公开的属性和命令供视图层进行绑定;

Model层获取数据,转换成视图层可展示的数据(如TableView 的 DataSource)。

Binder

在 MVVM 中,声明式的数据和命令绑定是一个隐含的约定,它可以让开发者非常方便地实现视图层和ViewModel的同步,避免编写大量繁杂的胶水代码。著名的RAC库就提供了这种绑定能力。

2.角色细解

数据绑定

MVVM是微软提出的,是在 MVP 的基础上发展起来的。因此 MVVM 各层的职责基本上与 MVP 的类似,其中VM对应P层。MVVM 相对于 MVP 改良了什么呢?答案就是 数据绑定

在 MVP 中,从用户点击开始,一个完整的响应流程是:视图调用P层处理业务逻辑,P层调用模型处理数据,处理完成后 P层再通过代理回调视图。这里要反复同步P层的状态,当事件多起来时这样写就有点麻烦。

而在 MVVM 中, ViewVM层之间多了数据绑定的操作,这意味着当VM层的数据变化时,你只需要更新VM层的某个属性,那么绑定了该属性的视图层会相应的更新UI,自动实现状态的同步。

ViewModel

MVVM 模式中,ViewModel存在的目的在于抽离Controller中的展示业务逻辑,而不是代替Controller本身。所以它不负责视图的操作,也就不需要持有任何的视图对象,更不能包含视图的push/present等跳转逻辑。这里,Controller 要做的仅是将视图ViewModel进行绑定,同时管理各个视图。所以实际上我们的 MVVM 也可以看成M-VM-C-V

3.示例1:OC

场景:点击刷新按钮,请求数据;进度指示器根据请求状态显示/隐藏;

View层与绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@interface ViewControllerII ()
<UITableViewDataSource,
UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *mTableview;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *mIndicator;
@property (nonatomic, strong) RACViewModel *mViewModel; //VM对象

@end

@implementation ViewControllerII

- (void)viewDidLoad {
[super viewDidLoad];
[self setUps]; //设置UI
[self bindViewModel]; //创建VM并绑定其属性
}

#pragma mark -BUsiness

- (void)setUps{

_mIndicator.hidden = YES;

UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setTitle:@"reload" forState:UIControlStateNormal];
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[btn sizeToFit];

//监听刷新按钮点击事件
[[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self)

self.mIndicator.hidden = NO;
[self.mViewModel.fetchCommand execute:nil];//执行ViewModel中的RACCommand
}];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:btn];

self.mTableview.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
}

- (void)bindViewModel{

//监听VM的canReload字段
@weakify(self)
[RACObserve(self.mViewModel, canReload) subscribeNext:^(id x) {
@strongify(self);
[self.mTableview canReload];
self.mIndicator.hidden = YES;
}];

//监听进度指示器的hidden属性 同步动画状态
[RACObserve(self.mIndicator, hidden) subscribeNext:^(NSNumber *hiddenNum) {
@strongify(self);
BOOL hidden = hiddenNum.boolValue;
if (hidden) {
[self.mIndicator stopAnimating];
}else{
[self.mIndicator startAnimating];
}
}];
}

- (RACViewModel *)mViewModel{
if (!_mViewModel) {
_mViewModel = [[RACViewModel alloc] init];
}
return _mViewModel;
}

#pragma mark -Tableview Data&Delegate
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.mViewModel.cellNums; //计算业务交给ViewModel处理
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
UILabel *label = [cell viewWithTag:1];
//由VM处理数据转换业务
label.text = [self.mViewModel convertedTextAtIndexPath:indexPath];

return cell;
}
@end

以上是View层,由ViewController与其中的tableView、按钮、进度小菊花共同组成;

这里创建并持有VM层对象mViewModel,同时绑定了VM层的属性(canReload);

点击按钮即会向VM层的fetchCommand发送指令请求数据;

VM层拿到数据后,View层不再需要代理或block,而是直接在订阅的VM层信号回调中更新UI;

更新UI所需的数据及其转换等业务逻辑均交由VM层处理。

ViewModel

头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
#import "RACCommand.h"
#import "RACSignal.h"

@interface RACViewModel : NSObject

//属性对外只读 供View层绑定监听
@property (nonatomic, readonly) NSUInteger cellNums;
@property (nonatomic, readonly) BOOL canReload;
@property (nonatomic, readonly, strong) RACCommand *fetchCommand; //对外提供的指令

- (NSString*)convertedTextAtIndexPath:(NSIndexPath*)indexPath;

@end

m文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#import "ReactiveCocoa.h"
#import "RACHttpRequestManager.h"
#import "RACEXTScope.h"
#import "RACModel.h"

@interface RACViewModel()
//属性对内可读可写
@property (nonatomic, readwrite) NSUInteger cellNums;
@property (nonatomic, readwrite) BOOL canReload;
@property (nonatomic, readwrite, strong) RACCommand *fetchCommand;
@property (nonatomic, strong) NSArray *mItemsArr;
@end

@implementation RACViewModel

- (instancetype)init
{
self = [super init];
if (self) {
//创建指令
self.fetchCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
//返回M层请求数据的信号
return [[RACHttpRequestManager shareManager] fetchRequest];
}];

//订阅M层返回的信号
@weakify(self)
[[self.fetchCommand.executionSignals switchToLatest] subscribeNext:^(NSArray *dataArr) {
@strongify(self)
if (dataArr) {
self.mItemsArr = dataArr;
}
}];

//监听数据源变化
[RACObserve(self, mItemsArr) subscribeNext:^(NSArray *x) {
@strongify(self)
self.cellNums = x.count;
self.canReload = YES;//修改V层绑定的属性,触发V层更新TableView
}];
}
return self;
}

//处理V层的数据转换业务
- (NSString *)convertedTextAtIndexPath:(NSIndexPath *)indexPath
{
if (!self.mItemsArr.count) {
return @"0";
}
RACModel *model = self.mItemsArr[indexPath.row];
NSString *str = model.title;
return [str copy];
}
@end

这是ViewModel层,定义了一些属性供View层绑定和监听;

同时定义了处理View层数据转换的业务逻辑;

当收到View层交互指令后,本层会调用Model层去请求数据,并在信号回调中处理数据;

通过修改对外暴露的属性字段(self.canReload),通知View层更新UI;

Model

Model实体类:

1
2
3
4
5
6
7
@interface RACModel : NSObject
@property (nonatomic, copy) NSString *title;
@end

@implementation RACModel

@end

数据请求类.h:

1
2
3
4
5
6
7
8
9
10
#import "RACModel.h"
#import "ReactiveCocoa.h"
#import "AFHTTPRequestOperationManager+RACSupport.h"

@interface RACHttpRequestManager : AFHTTPRequestOperationManager

+ (instancetype)shareManager;
- (RACSignal*)fetchRequest;

@end

数据请求.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "RACHttpRequestManager.h"
#import "RACModel.h"

static RACHttpRequestManager *mManager;

@implementation RACHttpRequestManager

+ (instancetype)shareManager
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mManager = [[self alloc] init];
});
return mManager;
}

- (RACSignal*)fetchRequest
{
RACSignal *s = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"++on RACSignal in fetch~");
//模拟网络请求 5秒后返回数据
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"++++Http responsed!");
NSMutableArray *mutArr = [NSMutableArray arrayWithCapacity:15];
for (int i = 0; i < 15; i++) {
RACModel *model = [[RACModel alloc] init];
model.title = [NSString stringWithFormat:@"%d",i];
[mutArr addObject:model];
}
[subscriber sendNext:mutArr];
});
return [RACDisposable disposableWithBlock:^{
}];
}];

return s;
}
@end

这是一个数据请求的工具类,是Model层的一部分,对外提供了数据请求的信号fetchRequest以供订阅。数据返回后被解析成RACModel实体数组;VM通过其内部的订阅回调收到这个数组,修改View层绑定的对应属性,通知View层更新UI。

  • 以上实践中,View层持有ViewModel并绑定其暴露的属性;
  • ViewModel持有Model层实例,并订阅了其请求数据的信号;
  • 当用户点击reload按钮后,ViewModel层收到指令并通过Model层请求数据;
  • Model层在请求的数据返回后通过信号告知ViewModel
  • ViewModel处理数据并通过修改View层绑定的属性告知其更新UI;

可以看到,各层的职责也是非常清晰,通过绑定属性和订阅信号,各层处理相关业务的代码都内聚在自己的层里,这也很好的体现了“高内聚,低耦合”的架构设计要求。

4.示例2:Swift

场景:本地校验输入的登录文本格式,合规时发送请求;

要求:

  • 用户名与密码均须大于6个字符;
  • 用户名或密码小于6个字符时显示提示文案;
  • 用户名与密码小于6字符时登录按钮显示灰色不可点击,均大于6字符时变蓝可点击;
  • 点击登录按钮,发起请求并根据结果显示提示;

RxSwift登录

View与绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import RxSwift
import RxCocoa

class DKRxGetInController: UIViewController {

@IBOutlet weak var DKNameTf: UITextField!
@IBOutlet weak var DKPswTf: UITextField!
@IBOutlet weak var DKNameTips: UILabel!
@IBOutlet weak var DKPswTips: UILabel!
@IBOutlet weak var DKGetInBtn: UIButton!

let disposeBag = DisposeBag()

//ViewModel
lazy var aGetinVM:DKRxGetInViewModel = {
return DKRxGetInViewModel(name: DKNameTf.rx.text.orEmpty.asObservable(), psw: DKPswTf.rx.text.orEmpty.asObservable())
}()

override func viewDidLoad() {
super.viewDidLoad()

//将昵称的输入结果与提示文本的“隐藏”属性进行绑定
aGetinVM.nameObs.bind(to: DKNameTips.rx.isHidden).disposed(by: disposeBag)
//将昵称的输入结果与密码输入框的“是否可用”属性进行绑定,昵称输入合规时才能输入密码
aGetinVM.nameObs.bind(to: DKPswTf.rx.isEnabled).disposed(by: disposeBag)

//将密码的输入结果与提示文本的“隐藏”属性进行绑定
aGetinVM.pswObs.bind(to: DKPswTips.rx.isHidden).disposed(by: disposeBag)

//组合昵称与密码信号,昵称与密码同时满足条件时,登录按钮方可用
let _ = aGetinVM.allObs.subscribe{ [weak self] in
self?.DKGetInBtn.backgroundColor = ($0 ? UIColor.blue : UIColor.lightGray)
}
aGetinVM.allObs.bind(to: DKGetInBtn.rx.isEnabled).disposed(by: disposeBag)

//给登录按钮添加点击事件
DKGetInBtn.rx.tap.subscribe{ [weak self] _ in
//发起登录请求
let _ = self?.aGetinVM.fetchUserinfo(name: (self?.DKNameTf.text)!, psw: (self?.DKPswTf.text)!).subscribe { (ob) in
self?.showAlert()
} onError: { (error) in
print(error.localizedDescription)
}
}.disposed(by: disposeBag)
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}

func showAlert() {
let alert = UIAlertController.init(title: "提示", message: "登录成功!", preferredStyle: .alert)
let action = UIAlertAction.init(title: "Ok", style: .default, handler: nil)
alert.addAction(action)
present(alert, animated: true, completion: nil)
}

deinit {
print(#file, #function)
}
}

这是View层,它持有一个VM实例并将输入框的文本与VM中的属性进行双向绑定。提示文本是否显示、输入框是否允许输入、登录按钮的状态都是根据输入内容是否合规来决定的。这些校验的逻辑均在VM中处理;点击登录按钮时通过VM发起请求,结果返回后显示对应的提示。

ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import RxSwift

class DKRxGetInViewModel {

//声明可观察属性 供View层绑定
let nameObs: Observable<Bool>
let pswObs: Observable<Bool>
let allObs: Observable<Bool>

//Model层
private var aModel = DKModel()

//初始化并定义业务逻辑
init(name: Observable<String>, psw: Observable<String>) {

//创建信号1:“昵称”输入框
nameObs = name.map { text in
return text.count >= 6 //昵称须>6个字符
}

//创建信号2:“密码”输入框
pswObs = psw.map { $0.count >= 6 } //密码须>6个字符

//组合昵称与密码两个信号
allObs = Observable.combineLatest(nameObs, pswObs) { $0 && $1 } //&&:需要昵称与密码同时满足条件
}

//发起登录请求
func fetchUserinfo(name:String, psw: String)->Observable<[String: Any]> {

return Observable.create { [weak self] (ob) -> Disposable in

let _ = self?.aModel.fetchUserinfo(name: name, psw: psw).subscribe { (dic) in
ob.onNext(dic)
} onError: { (error) in
print(error)
}
return Disposables.create()
}
}
}

这是一个比较简单的VM,里面定义了校验用户名与密码的逻辑;

它持有一个Model层实例,在给View层的接口中通过Model发起登录请求并回调结果。

Model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import RxSwift
import RxAlamofire

class DKModel {
let disposeBag = DisposeBag()

func fetchUserinfo(name:String, psw: String)->Observable<[String: Any]> {

return Observable.create { [weak self] (ob) -> Disposable in
//模拟URL
let urlString = "https://www.douban.com/j/app/radio/channels"
let url = URL(string:urlString)!

//创建并发起请求
RxAlamofire.request(.get, url)
.responseJSON()
.subscribe(onNext: {
response in
let json = response.value as! [String: Any]
ob.onNext(json) //回调数据
}, onError: { (error) in
ob.onError(error)
}).disposed(by: self!.disposeBag)

return Disposables.create()
}
}
}

这是Model层,其中定义了给VM使用的登录接口,接口内发起登录请求并回调登录结果。

在Swift版示例中,各层也比较清晰,每层内部的代码高度内聚。比较示例1,它更好的实现了ViewVM的双向绑定,即输入内容与校验结果绑定,校验结果与提示文本和登录按钮的状态绑定。

5.使用规范

  • MMVM 中V层绑定了ViewModel,反过来不行,ViewModel中不能包含View
  • MMVM 中ViewModel绑定了Model层,反过来也不行;
  • Controller尽量不涉及业务逻辑,让ViewModel去做;
  • Controller是中间人,接收View的事件、调用ViewModel的方法、响应VM的变化;
  • ViewModel 避免过于臃肿,否则重蹈 Controller 的覆辙变得难以维护,可拆分成子VM;
  • MVVM 配合某种绑定机制时效果最好(如RAC)。

四.后记

架构一直在演进过程中,考虑到项目大小、所用语言、学习成本等因素,没有最好的架构,只有适合自己的架构。我们要根据实际需求,不断学习和调整。


相关参考:

#AppleDoc-MVC

#©sunnyxx-RAC


常用架构模式
https://davidlii.cn/2020/09/01/patterns.html
作者
Davidli
发布于
2020年9月1日
许可协议