软件架构是指,设计软件的人为软件赋予的形状,这个形状是指系统如何被划分为组件(Components),各个组件如何排列(Arrangement),组件之间如何沟通(Communication)。
这是Uncle Bob 的 《Clean Architecture》中对架构的定义。
文中Demo
架构的意义:为了能够将创建和维护软件的成本最小化。
编写代码中至关重要的是,需要使每一部分容易被识别,赋有一个特定而明显的目的,并与其他部分在逻辑关系中完美契合。这就是我们所说的软件架构。好的架构不仅让一个产品成功投入使用,还可以让产品具有可维护性,并让人不断头脑清醒的对它进行维护!而不能研发一时爽,维护火葬场!
架构设计模式简介
目前主流的几种架构模式:
- MVC
- MVP
- MVVM
- VIPER
MVP、MVVM、VIPER都是从MVC演变而来,都是为了解决开发过程中的实际问题而提出来的,各有优缺点和适用的场景,本质目的都是不断地从ViewController中把逻辑拆分出去。
从功能来区分的话,可以从三个层面划分上面的4种架构设计模式:
-
Model层: 负责数据访问,又可以分为以下两类:
- 业务处理:日常开发中DAO、Service都可以算作是Model层衍生出来的业务请求模块,负责用于处理用户提交的请求。
- 数据承载:用于专门承载业务数据的实体类,比如开发中定义的Student、User等各种Entity.
- View层: 负责视图的展示。
- Controller/Presenter/ViewModel:Model和View之间的中介,一般负责在用户操作View时更新Model,以及当Model变化时更新View。
一个好的架构或者架构模式应该满足以下三点:
- 清晰的职责划分
- 可测试
- 易用性
MVVM
view都是以容器的方式封装起来的,作为viewController的成员变量,view所产生的交互事件以代理的方式回调给viewController,viewController持有viewModel ,而viewModel以block或代理的方式把业务逻辑的处理结果交给viewController。
职责划分:
- View:负责控件初始化,设置UI数据(不负责网络数据和业务数据),交互事件代理给viewController。
- viewController:负责视图创建、组合,协调逻辑,view事件的回调处理,数据绑定。
- viewModel:负责业务逻辑处理,其中又涉及到网络数据或业务数据转化成UI数据的预排版;负责数据增删改查封装者,注意这里只是封装,具体的实现逻辑不在这一层。
MVVM的优点:
- 开发人员可以专注于业务逻辑(viewModel)。
- 从而更容易针对viewModel单元测试。
- 可重用性。
MVVM缺点:
- 数据绑定不便调试bug
- 没有统一的路由管理,界面间的链接逻辑分散在各控制器中,导致控制器间存在一定的耦合和依赖。
- 没有统一的状态管理
后两点在MVC,MVP,VIPER中都存在。所以下文中重点探讨以下两点以及相应的实现方案:
- 路由管理
- 状态管理
使用App Coordinator 统一管理应用路由
App Coordinator 是 Soroush Khanlou 在 2015 年的NSSpain演讲上提出的一个模式,其本质上是 Martin Fowler 在《 Patterns of Enterprise Application Architecture 》中描述的 Application Controller 模式在 iOS 开发上的应用。其核心理念如下:
- 抽象出一个 Coordinator 对象概念
- 由该 Coordinator 对象负责 ViewController 的创建和viewModel等配置
- 由该 Coordinator 对象来管理所有的 ViewController 跳转
- Coordinator 可以派生子 Coordinator 来管理不同的 Feature Flow。
传统的开发模式下,页面间的跳转是通过 navigationController 的 push() 方法,这种方法固然便捷,但是实现跳转存在页面间耦合。Coordinator的诞生就是为了解决这一问题。
引入 Coordinator后跳转逻辑对页面不可见,由 Coordinator 管理,其提供了 navigationController 的接口并持有 Controller,跳转逻辑隐藏在了 Coordinator 中。Coordinator 独立与 MVVM 之外,是一个附加层。可以理解为 Coordinator 是每个组件对外暴露的接口,页面间的交互,只能通过 Coordinator,它同样依赖于 RxSwift。
经过这层抽象之后,一个复杂 App 的路由对应关系就会如下:
从图中可以看出,UI 和业务逻辑被拆分开,各自有了自己清晰的职责。ViewController 的初始化及配置,ViewController 之间的深层链接逻辑全部都转移到 App Coordinator 的体系中去了,ViewController 则彻底变成了一个个独立的个体,ViewController间没了依赖关系,其只负责:
- 装配子视图
- 数据绑定
- 把界面上的 user action 转换为业务上的 user intents(用户意图),然后转入 App Coordinator 中进行业务处理。
还可以在 App Coordinator 的具体实现和 ViewController 之间抽象一层 Protocols,把 UI 和业务逻辑的实现彻底抽离开。经过这层抽象之后,路由关系变化如下:
经过 App Coordinator 统一处理路由之后,App 可以得到如下好处:
- ViewController 变得非常简单,成为了一个概念清晰的,独立的 UI 组件。这极大的增加了其可复用性。
- UI 和业务逻辑的抽离也增加了业务代码的可复用性,在多屏时代,当你需要为当前应用增加一个 iPad 版本时,只需要重新做一套 iPad UI 对接到当前 iPhone 版的 App Coordinator 中就完成了。
具体实现可参看Demo。
基于状态管理的单向数据流架构
为什么需要统一的状态管理?
一个 iOS 应用本质上就是一个状态机,从一个状态的UI由 User Action 或者 API异步调用返回的 Data Action 触发达到下一个状态的 UI。为了准确的控制应用功能,开发者需要能够清楚的知道:
应用的当前 UI 是由哪些状态决定的?
User Action 会影响哪些应用状态?如何影响的?
Data Action 会影响哪些应用状态?如何影响的?
各应用的状态通常分散在 Model中,甚至有些状态直接保存在 View Controller 中,在跟踪状态时经常需要跨越多个 Model,很难获取到一个全貌的应用状态。有时用户操作产生的Action可能导致多个 Model的状态发生改变,这时跟踪状态就变得比较困难。
通常我们需要在ViewModel中通过大量的变量来维护一个比较复杂的页面在运行期间的各种状态和逻辑,通过RxSwift将ViewModel中的状态变量直接绑定给视图就隐藏了通知视图的过程,这样我们只需要专注于数据本身,不用再去管UI层的逻辑,但是滥用这个特性也会带来麻烦,大量的可观察变量和绑定操作会让逻辑变得含糊不清,修改一个变量的时候可能会导致一系列难以预料的连锁反应,这样代码反而会变得更加难以维护,这时统一管理状态显得很有必要了。
那什么是单向数据流,多向数据流呢?
MVC,MVVM,MVP、VIPER这些架构设计方案,都是基于MVC演变而来,本质目的都是从ViewController中把逻辑拆分出去,对控制器进行瘦身和更细粒度的职责划分.
他们都有各自的特点,但是都有同一个核心: 通过多向数据流将代码按照单一职责原则来划分代码。在多向数据流中,数据在各个模块中传递。
但多向数据流的代码在阅读和debug上都可能变成一场灾难,一个改变可能会带来一系列的连锁反应,跟踪状态改变也比较困难。而单向数据流就能让程序的运行更加具有可预测性,也能够减少阅读这些代码的痛苦。
多向数据流并不一定是你想要的,相反单向数据流才是我们更喜欢的数据传递方式。
所谓的单向绑定和双向绑定所描述的都是视图(View)和数据(Model)之间的关系:
比方说有一个展示消息的页面,首先需要从网络加载最新的消息,在MVC中我们可以这样写:
class NormalMessageViewController: UIViewController {
var msgList: [MsgItem] = [] // 数据源
// 网络请求
func request() {
// 1. 开始请求前播放loading动画
self.startLoading()
MessageProvider.request(.news) { (result) in
switch result {
case .success(let response):
if let list = try? response.map([MsgItem].self) {
// 2. 请求结束后更新model
self.msgList = list
}
case .failure(_):
break
}
// 3. model更新后同步更新UI
self.stopLoading()
self.tableView.reloadData()
}
}
// ...
}
还可以将不需要的消息从列表中删除:
extension NormalMessageViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// 1. 更新model
self.msgList.remove(at: indexPath.row)
// 2. 刷新UI
self.tableView.reloadData()
}
}
// ...
}
在request方法中我们通过网络请求修改了数据msgList,一旦msgList发生改变必须刷新UI;在tableView上删除消息时,视图层直接对数据进行操作然后刷新UI。视图层即会响应数据改变的事件,又会直接访问和修改数据,这就是一个双向绑定的关系:
虽然在这个例子中看起来非常简单,但是当页面比较复杂的时候UI操作和数据操作混杂在一起会让逻辑变得混乱。看到这里单向绑定的含义就很明显了,它去掉了View -> Model的这一层关系,视图层不能直接对数据进行修改,它只能通过某种机制向数据层传递事件,并在数据改变的时候刷新UI。
Redux是一种基于状态管理的单向数据流架构。
为了构造单向数据流,Redux引入了一系列概念,这是Redux中所描述的数据流:
核心概念:
View
顾名思义,View就是视图,用户在视图上的操作事件不会直接修改模型,而是会被映射成一个个Action。Action
Action表示一个对数据操作的请求,Action会被发送到Store中,这是对模型数据进行修改的唯一办法。
Demo中用到ReSwift,它是一个轻量级的Redux框架。
在ReSwift中有一个名为Action的协议(仅作标记用的空协议),对于Model中数据的每个操作,比如说设置一个值,都需要有一个对应的Action:
/// 设置数据的Action
struct ActionSetMessage: Action {
var news: [MsgItem] = []
}
/// 移除某项数据的Action
struct ActionRemoveMessage: Action {
var index: Int
}
用struct类型来表示一个Action,Action所携带的数据保存在其成员变量中。
- Store和State
就像上面所提到的,State表示了应用中的Model数据,而Store则是存放State的地方;在Redux中Store是一个全局的容器,所有组件的状态都被保存在里面;Store接受一个Action,然后修改数据并通知视图层更新UI。
定义 State 的方式可以从业务上建模,也可以根据 UI 需求来建模,建议根据 UI 需求来建模,这样的 State 更容易和 UI 进行绑定。
如下所示,每一个页面和组件都有各自的状态以及用来储存状态的Store:
// State
struct ReduxMessageState: StateType {
var newsList: [MsgItem] = []
}
// Store,直接使用ReSwift的Store类型来初始化即可,初始化时要指定reducer和状态的初始值
let newsStore = Store<ReduxMessageState>(reducer: reduxMessageReducer, state: nil)
Store通过一个dispatch方法来接收Action,视图调用这个方法来向Store传递Action:
messageStore.dispatch(ActionRemoveMessage(index: 0))
- Reducer
Reducer是一个比较特殊的函数,Redux强调了数据的不可变性(Immutable),简单来说就是一个数据模型在创建之后就不可被修改,那当我们要修改Model某个属性时要怎么办呢?答案就是创建一个新的Model,Reducer的作用就体现在这里:
签名如下:
(_ action: Action, _ state: StateType?) -> StateType
接受一个表示动作的action和一个表示当前状态的state,然后计算并返回一个新的State,随后这个新的State会被更新到Store中:
// Store.swift中的实现
open func _defaultDispatch(action: Action) {
guard !isDispatching else {
raiseFatalError("...")
}
isDispatching = true
let newState = reducer(action, state) // 1. 通过reducer计算出新的state
isDispatching = false
state = newState // 2. 直接将新的state赋值到当前的state上
}
应用中所有数据模型的更新操作最终都通过Reducer来完成,为了保证这一套流程可以正常的完成,Reducer必须是一个纯函数:它的输出只取决于输入的参数,不依赖任何外部变量,同样也不能包含任何异步的操作。
Demo中的Reducer是这样写的:
func reduxMessageReducer(action: Action, state: ReduxMessageState?) -> ReduxMessageState {
var state = state ?? ReduxMessageState()
// 根据不同的Action对数据进行相应的修改
switch action {
case let setMessage as ActionSetMessage: // 设置列表数据
state.newsList = setMessage.news
case let remove as ActionRemoveMessage: // 移除某一项
state.newsList.remove(at: remove.index)
default:
break
}
// 最后直接返回修改后的整个State结构体
return state
}
最后在视图中实现StoreSubscriber协议newState接收State改变的通知并更新UI即可。
Redux将View -> Model
这一层关系分解成了View -> Action -> Store -> Model
,每一个模块只负责一件事情,数据始终沿着这条链路单向传递。
在这个机制下, 一个 App 的状态转换如下:
从业务操作 -> 产生 Action -> Reducer 接收 Action 和当前 App State 产生新的 AppState -> 更新当前 State -> 通知 UI AppState 有更新 -> UI 显示新的状态 -> 下一个业务操作…
在这个状态转换的过程中,需要注意,业务操作会有两类:
- 无异步调用的操作,如点击界面把界面数据存储到 App State 上;这类操作处理起来非常简单,按照上面提到的状态转换流程走一圈即可。
- 有异步调用的操作。如点击查询,调用 API,数据返回之后再存储到 App State 上。这类操作就需要通过 Action Creators 来处理异步调用并分发新的 Action。
经过 ReSwift 统一管理应用状态之后,App 开发可以得到如下好处:
- 统一管理应用状态,包括统一的机制和唯一的状态容器,这让应用状态的改变更容易预测,也更容易调试。
- 清晰的逻辑拆分,清晰的代码组织方式,让团队的协作更加容易。
- 函数式的编程方式,每个组件都只做一件小事并且是独立的小函数,这增加了应用的可测试性。
- 单向数据流,数据驱动 UI 的编程方式。
Vuex - ReactorKit
ReactorKit是另一个在Redux思想的实现,它的一些设计理念与Vuex十分相似,Vuex则是其专门为Vue提出的状态管理模式,其在Redux之上进行了一些优化。
与ReSwift不同的是ReactorKit的实现本身便于基于RxSwift,所以不必再考虑如何与Rx结合,下面是ReactorKit中数据的流程图:
大体流程与Redux类似,不同的是Store变成了Reactor,这是ReactorKit引入的一个新概念,它不要求在全局范围统一管理状态,而是每个组件管理各自的状态,所以每个视图组件都有各自所对应的Reactor,而ReSwift随着开发的深入,会拥有日益增大的状态树。
具体的代码请看Demo中的ReactorKit文件夹,各个部分的含义如下:
- Reactor:
现在用ReactorKit来重写上面的那个例子,首先需要为这个页面创建一个实现了Reactor协议的类型MessageReactor,一个Reactor需要定义State、Action、Mutation这三个部分,首先比起Redux这里多了一个Mutation的概念,在Redux中由于Action直接与Reducer中的操作对应,所以Action只能用来表示同步的操作。ReactorKit将这个概念更加细化,拆分成了两个部分:Action和Mutation:
Action:视图层触发的动作,可以表示同步和异步(比如网络请求),它最终会被转换成Mutation再被传递到Reducer中;
Mutation:只能表示同步操作,相当于Redux模式中的Action,最终被传入Reducer中参与新状态的计算;
- mutate():
mutate()是Reactor中的一个方法,用来将用户触发的Action转换成Mutation,mutate()的存在使得Action可以表示异步操作,因为无论是异步还是同步的Action最后都会被转换成同步的Mutation:
func mutate(action: MessageReactor.Action) -> Observable<MessageReactor.Mutation> {
switch action {
case .request:
// 1. 异步:网络请求结束后将得到的数据转换成Mutation
return service.request().map { Mutation.setMessageList($0) }
case .removeItem(let index):
// 2. 同步:直接用just包装一个Mutation
return .just(Mutation.removeItem(index))
}
}
值得一提的是,这里的mutate()方法返回的是一个Observable<Mutation>类型的实例,得益于Rx强大的描述能力,我们可以用一致的方式来处理同步和异步代码。
- reduce():
reduce()方法与Redux中的Reducer一样,唯一不同的是这里接受的是一个Mutation类型,但本质是一样的:
func reduce(state: MessageReactor.State, mutation: MessageReactor.Mutation) -> MessageReactor.State {
var state = state
switch mutation {
case .setMessageList(let news):
state.newsList = news
case .removeItem(let index):
state.newsList.remove(at: index)
}
return state
}
- Service
图中还有一个与mutate()产生交互的Service对象,Service指的是实现具体业务逻辑的地方,Reactor会通过各个Service对象来执行具体的业务逻辑,比如说网络请求:
protocol MessageServiceType {
/// 网络请求
func request() -> Observable<[MsgItem]>
}
final class MessageService: MessageServiceType {
func request() -> Observable<[MsgItem]> {
return MessageProvider
.rx
.request(.news)
.mapModel([MsgItem].self)
.asObservable()
}
}
看到这里Reactor的本质基本上已经明了:Reactor实际上是一个中间层,它负责管理视图的状态,并作为视图和具体业务逻辑之间通讯的桥梁。
此外ReactorKit希望我们的所有代码都通过函数响应式(FRP)的风格来编写,这从它的API设计上可以看出:Reactor类型中没有提供如dispatch这样的方法,而是只提供了一个Subject类型的变量action:
var action: ActionSubject<Action> { get }
在Rx中Subject既是观察者又是可观察对象,常常扮演一个中间桥梁的角色。视图上所有的Action都通过Rx绑定到action变量上,而不是通过手动触发的方式:比方说我们想在viewDidLoad的时候发起一个网络请求,常规的写法是这样的:
override func viewDidLoad() {
super.viewDidLoad()
service.request() // 手动触发一个网络请求动作
}
而ReactorKit所推崇的函数式风格是这样的:
// bind是统一进行事件绑定的地方
func bind(reactor: MessageReactor) {
self.rx.viewDidLoad // 1. 将viewDidLoad作为一个可观察的事件
.map { Reactor.Action.request } // 2. 将viewDidLoad事件转成Action
.bind(to: reactor.action) // 3. 绑定到action变量上
.disposed(by: self.disposeBag)
// ...
}
bind方法是视图层进行事件绑定的地方,我们将VC的viewDidLoad作为一个事件源,将其转换成网络请求的Action之后绑定到reactor.action上,这样当VC的viewDidLoad被调用时该事件源就会发出一个事件并触发Reactor中网络请求的操作。
这样的写法是更加FRP,一切都是事件流,但是实际用起来并不是那么完美。首先我们需要为用到的所有UI组件提供Rx扩展(上面的例子使用了RxViewController这个库);其次这对reactor实例初始化的时机有更加严格的要求,因为bind方法是在reactor实例初始化的时候自动调用的,所以不能在viewDidLoad中初始化,否则会错过viewDidLoad事件。
- 优点:
相比ReSwift简化了一些流程,并且以组件为单位来管理各自的状态,相比起来更容易在现有工程中引入;
与RxSwfit很好的结合在了一起,能提供较为完善的函数响应式(FRP)开发体验; - 缺点:
因为核心思想还是Redux模式,所以模板代码过多的问题还是无法避免。
三层架构
三层架构是一个分层式的软件体系架构设计理念。
把软件架构分为三层;
1:UI层 (user interface layer) 界面层
2:BLL层 (business logic layer) 业务逻辑层
3:DAL层 (data access layer) 数据访问层
UI层:就是展现给客户的界面,用于展示用户输入以及服务端返回的数据;交互式操作界面中,用户输入的数据和想要的数据展示。
业务逻辑层: 桥梁层,用户输入的数据通过业务逻辑层的处理发给数据层;数据层返回的数据通过业务逻辑层发送给界面展示。常做的操作是验证、计算、业务规则等。
数据访问层:主要管理数据,实现对数据的增删改查等操作。把业务逻辑层提交的用户输入的数据保存,把业务逻辑层请求的数据返回给业务逻辑层。
三层架构的重要指导原则就是:高内聚、低耦合。其最大目的就是:解耦。
如何解耦?定义协议。
MVC,MVP,MVVM都架构可以看作是对UI层的一种细分。
依赖注入DI
把有依赖关系的类放到外部容器中,解析出这些类的实例。
原则:外部创建对象的依赖,而不是自身主动创建。
参考文章:
Coordinators Redux
RXSwift+MVVM+Coordinator
Taming Great Complexity: MVVM, Coordinators And RxSwift
MVVM-C with Swift
MVVMC – ADAPTING THE MVVM DESIGN PATTERN AT RUNTASTIC
MVVMC – ADAPTING THE MVVM DESIGN PATTERN AT RUNTASTIC中文版
用 ReSwift 实现 Redux 架构
基于 ReSwift 和 App Coordinator 的 iOS 架构
谈谈RxSwift和状态管理
Redux
iOS应用架构谈 开篇
iOS应用架构谈 view层的组织和调用方案
iOS应用架构谈 网络层设计方案
深入分析MVC、MVP、MVVM、VIPER
三层架构
依赖注入
Swinject