概览图
我画了一张草图,从下向上,分为三层,分别是 Service,Router,View-VM。
Service
服务层,由多个服务模块组成,每个服务模块对应一个业务实体,比如 User服务主要提供用户信息相关的操作、状态、通知,Diary服务主要提供日记信息相关的操作、状态、通知。 操作也就是增、删、改、查方法,实现形式包括 网络请求,本地文件访问,本地数据库访问等,状态就是当前最新的状态数据,比如User服务可能包括当前用户的 Token,昵称,头像,VIP状态,是否已同意某协议,是否看过某引导海报 等等,通知就是当数据发生改变时,发生通知,以让其他组件收到通知后做相应处理,不同的数据对应不同的通知,实现形式主要是 Notification
和 Observable
。
上图中,大圆形 表示一个服务模块,上面的带线小圆点 表示状态数据,类似WIFI信号的表示 Observable,小喇叭表示Notification。
不同的人对 Service 和 Model 有不同的理解,容易产生疑惑。 这里我讲一下自己的理解,Service和Model都有狭义与广义之分,狭义的Service只包括IO接口(如网络请求、文件访问),广义的Service除了接口,还包括模块相关的状态、通知、业务过程封装 等很多方面。狭义的Model只是数据结构定义,仅用于反序列化,广义的Model 也称大模型,除了数据结构定义,还包括模型相关的业务,其在实现形式上,Model往往拥有丰富的成员方法、类方法。 由于广义Service和广义Model有很多重叠,实际使用中,要么是广义Service搭配狭义Model,要么是广义Model搭配狭义Service。我选择的是广义Service搭配狭义Model。
service正如其名字:服务,专门为上层组件提供服务支持。落实服务的是 ServiceProvider,服务提供者,是个全局单例,聚合了每个服务模块的实例,大多数时候,它都够用。(只有特殊时候,才需要单独创建Service实例)
Router
路由层,为视图组件关联路由,目的是 屏蔽视图组件的差异,使用相同的方式展示视图组件。
视图组件是对 可视页面的统称,可以是传统的ViewController,可以是全屏弹层(抽屉式浮层),可以是H5页面,可以是原子性的一组ViewController...... 在为它们关联路由后,一个视图组件就可以用路由 打开另一个视图组件,而无需关心其是用什么技术实现的。
路由话事人:navigator,导航器,是个全局单例,负责具体路由操作。
视图组件之间,彼此“不认识”,"不知道"对方是用什么技术实现的,但它们都“认识” navigator,navigator扮演 中间人,为视图们“牵线搭桥”。一个视图要打开另一个视图,只要把另一个视图的路由“告诉” navigator,然后静静地等待navigator处理就好。
View-VM
视图组件层中的 视图组件 可以看作一个黑盒,黑盒内部的实现思路有很多种,可以用传统的MVC,可以用MVP,可以用MVVM,还可以用上图中的 Reactor,如果是全屏弹层,或H5页面,能用的模式就更多。
视图组件除了“认识” navigator,还“认识” ServiceProvider,虽然二者做为全局单例 在任何时间,任何位置都可以访问,但还是存在一些推荐的使用位置,比如 如果使用了 Presenter、ViewModel、Reactor(后文 将用 VM 指代三者),应该只在VM中使用 navigator 和 ServiceProvider,因为VM承载着页面的主要逻辑,在这里对接外部,可以使功能更集中,提高内聚性。
上图中,Reactor上的小耳朵表示在监听Notification,订阅图标 表示在订阅Service的Observable。图标都放在Reactor中,而不是Controller。
在使用VM之前,传统的写法是在Controller中处理绝大多数逻辑,包括界面绑定和业务逻辑。在使用了VM后,VM用来处理业务逻辑,Controller应该变成一个“空壳儿”,仅负责把 VM 与 View 联系起来,也就是如何把VM的数据显示到View上,如何把 View 产生的用户操作转接到VM上,还包括一些纯界面相关的逻辑。
单看MVVM,Model-View-ViewModel,三者中没有iOS的ViewController, 如果硬要套到MVVM中,我认为iOS的View和ViewController都应归类为VIEW,因为有了VM,ViewController就专注于界面相关处理,而View本身也能做界面处理,这就使ViewController变得非必需,然而实际使用中,受iOS API的制约,有ViewController 会比没有 更方便一些,所以VC可以视为View的增强型外挂。这里引入一个常见问题:iOS的View可不可以直接搭配VM,还是应该经由VC搭配VM? 我的回答是 View可以直接搭配VM(在不需要VC或没有VC的场景中)。
对于可复用子View,应不应该在其中处理业务逻辑?有两种态度,一种是支持,理由是视图和业务逻辑写在一起,功能内聚,在多场景中复用时,简单方便,无需重复编写业务逻辑。另一种是反对,理由是业务逻辑应该集中在VM或Controller中,可复用子View只负责展示,并把事件回传,子组件保持纯净、无副作用,有利于总体可预测性、可维护性。我认为后者没错,前者得看应用场景,只要场景合适,就没错。如果认定某可复用子View是一个高内聚的、相对独立的业务模块,或者类似一个功能黑盒,比如分享弹窗,内部包含微信、朋友圏、微博、H5链接等相关判断与处理,它功能很独立,与外层场景无关,很明显 适合把逻辑写在内部。还有一些场景不明显,我的经验是,可以先写在内部,如果后续需要在内部添加条件判断,且这个条件和外层场景有关,那么 这个可复用子View 就不再是独立的功能黑盒了,这时应该把它改为无副作用的、仅用于展示的纯净子View,把逻辑挪到外层场景中, 如果使用场景多,重复代码多,可以考虑把逻辑处理流程封装进Service。
以上几个VM模式,我个人(至少目前是)推荐 Reactor。文档可参考 https://github.com/ReactorKit/ReactorKit
推荐理由:与“自由发挥”的ViewModel相比,Reactor有写法上的限制,需要遵守它的规则,初期可能会有所不适,然而所谓「无规矩不成方圆」,Reactor的"限制性约束" 可以让团队中每个人的思路和风格 趋于一致,从而提高代码的可维护性。(之前见过不同小伙伴写的ViewModel,写法过于自由,有的VM是对接口的简单封装,有的VM是用delegate引用VC,完全没有Observable,有的VM是公用/可复用代码的提取地......风格各异,不易维护 )
Reactor的模板:
import ReactorKit
final class MyReactor: Reactor {
typealias Model = Int // 改成自己的Model类型
// 初始状态
var initialState: State
init() { // 可添加初始化参数
self.initialState = State()
}
// 用户动作
enum Action {
case requestItems(Int) // pageNum
case requestMore
}
// 状态数据
struct State {
var isLoading: Bool = false
var items: [Model]?
var noMore: Bool?
var isMoreLoading: Bool = false
@Pulse var alertMessage: String?
}
// 修改(对状态做何修改)
enum Mutation {
case setLoading(Bool)
case pageItems([Model], Int)
case setNoMore(Bool)
case setMoreLoading(Bool)
case setAlertMessage(String)
}
// 用户动作 -> 修改
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .requestItems(pageNum):
return .of(
.pageItems([1, 2, 3, 4, 5], pageNum),
.setNoMore(false)
)
case .requestMore:
return .of(
.pageItems([6, 7, 8, 9, 10], 2),
.setNoMore(true)
)
}
}
// 监听、订阅外部事件,将外部事件转换为(对状态的)修改
// func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
// return Observable.merge(mutation, ......)
// }
// 目前状态 + 修改 -> 新状态
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let .setLoading(loading):
state.isLoading = loading
case let .setAlertMessage(msg):
state.alertMessage = msg
case let .pageItems(items, pageNum):
if pageNum == 1 {
state.items = items
} else {
state.items = (state.items ?? []) + items
}
case let .setNoMore(noMore):
state.noMore = noMore
case let .setMoreLoading(loading):
state.isMoreLoading = loading
}
return state
}
}
有的小伙伴会问,Service和VM都有State,也都可以用于驱动UI,我应该把状态定义在VM中还是Service中呢? 嗯,好问题。 VM一般是和页面一起产生,一起销毁,如果页面销毁后再次打开,状态也会复位,即从头再来,这种状态适合定义在VM中。如果状态要持续,比如再次进入页面时要依赖上次的状态,这种状态适合定义在Service中,还有一种状态 它影响大,超出了页面本身,即 除了本页面,其他页面也要根据这个状态做同步展示,这种也适合定义在Service中,某个状态在未来大概率对其他模块产生影响,也适合定义在Service中。定义位置不是一成不变的,随着业务的改变,状态很可能会在VM与Service之间移动。
编码建议
- 弹框在多处被用到时,使用独立路由。
- 使用通知:避免在其他页面组件中添加本页面组件专用的通知。
- 多用订阅,少用callback和delegate。
- 多考虑Service,服务模块(方法、状态、通知)能解决很多问题。
分工合作
为对方提供:
- 路由 (用于 展示)
- 事件源 (Notification或Observable,用于 订阅)
维护阶段,主要是 添加事件源。自己为对方提供事件源,或者对方为自己提供事件源。
最后
以上种种,本质是在尝试提供一个准确的预期,即 每个模块的组织结构(静态),每个流程的执行阶段(动态),都遵守相同的约定,熟悉这种约定的人,即使不看代码,也能准确地预判到模块的组织结构,代码执行流程的先后位置。这样能明显降低维护成本,提高开发效率。
架构思路可以有很多种,只要能从总体结构到流程细节,都能给出详细的约定,有助于提高团队协作效率,那它就是好架构,值得学习和借鉴。
如果你也有一套好用的思路,欢迎留言讨论。
写的很好,第一次听说 Reactor 模式,相比传统的 ViewModel 它有更明确的代码规范,受益匪浅!!
大牛写的极好,期待新的佳作!!!