Service Oriented 的 iOS 应用架构

Intro

前不久我们上线了一款新的 App - Glow Baby,App 针对 0 - 12 个月大的新生宝宝,提供爸爸妈妈全面、健康、科学的育儿知识,帮助记录宝宝成长的点点滴滴。在 Glow Baby 的开发中,我们也做了一些新的尝试 - 使用 Swift 开发,并基于 Swift 的语言特点设计了新的 iOS App 架构。

除了 Community 这部分的代码是作为一个私有的 Repo 引入,Glow Baby 基本是 100% Swift 代码。Glow Baby iOS 团队都是第一次接触 Swift,过程中我们踩过很多坑,遇到过很多抓狂的问题。但总体上,写 Swift 更加有趣,所有的努力最终也证明是值得的:App 运行更加流畅,代码更整洁可读性更高,我们开发效率也大大提高。

Baby App 跟 Glow 的其他几个 App 都是较为复杂的 App,因为像日记记录、本地存储、网络请求,服务器端数据的同步这些技术难点都有涉及。这些问题也都要求多线程编程。特别是数据同步,如何增量记录数据的增删改,什么时机跟服务器端进行同步。解决这些技术难题是非常有意思的工作,也是架构设计的创造性和乐趣所在。

接下来的一系列文章我们来看看 Baby App iOS 的应用架构。有些设计是基于 Swift 的语言特点的考虑,但并不妨碍整体的架构思路被应用在 Objective-C,甚至 Android 的 App 上。

MV(X)

在介绍 Glow Baby 的应用架构之前,先来看看目前 iOS 上最基础的架构 MVC,以及为解决 MVC 的毛病而诞生的其他几个架构,如 MVVM、VIPER 等。

Cocoa 的很多技术跟架构都是基于 MVC。而且无论是文档、示例代码,还是创建一个项目时提供的模板代码,Apple 都鼓励开发者去使用 MVC。MVC 定义了 App 里对象的角色(Model-View-Controller),以及他们之间的交互方式:

  • Model: 表示业务数据对象
  • View:展现数据的 UI
  • Controller:Model 跟 View 之间的粘合剂。一方面对 View 上的行为作出反应,通常会涉及到 Model 的更改;另一方面将 Model 的改动反映到 View 上

由于 Controller 作为粘合剂的存在,View 和 Model 只需要跟 Controller 交互,而不知道另一方的存在。这样,View 和 Model 作为独立可复用的组件,Controller 里处理业务逻辑。听起来这样的架构很清晰直观,实际应用中,MVC 对于不是很复杂的 App 也是非常高效的。但对稍复杂些的 App,MVC 使用起来就会非常吃力。

你可能听过 MVC 也被简称为 Massive View Controller,这就是原因所在 - View Controller 承担的职责太多:

  • 网络请求
  • 数据访问和存储
  • UI 的调整和组合
  • 业务逻辑
  • View 的 delegate、data source
  • 状态的维护

与单一责任准则(Single Responsibility Principle)背道而驰。过于臃肿的 View Controller 使 App 的维护成本非常高。我们的第一个 App - Glow 其实就是这个样子,尽管我们已经把网络请求以及数据访问和存储放到了 Model 里,但由于对象边界的定义不够清晰,大部分 View Controller 依然很臃肿,上千行的 View Controller 很常见。关于 View Controller 有个准则:如果一个 View Controller 超过了 300 行代码,那它一定做了责任范围以外的事。更不幸的是由于一些职责移交给 Model,导致 Model 也变得臃肿起来。原来唯一可以做 Unit Test 的 Model 现在测试也很困难。

为解决 Massive View Controller 的问题,MVVM、VIPER 等架构应运而生。这里不再详细介绍这些架构,有兴趣的读者可以自行去 Google。

Baby App 没有使用 MVVM 和 VIPER。因为:

  • 不够直观,提高了整体代码的复杂度,对于新入职的员工有一定学习成本
  • 要发挥 MVVM 的优势,需要有 Reactive。Reactive 增加学习成本的同时,也让调试变得更困难。
  • VIPER 虽然能平衡责任的分配,但由于引入过多对象,维护成本高。一个简单的页面也要求新增多个类和大量傻瓜代码

所以我们结合自己的需求和 Swift 的语言特点设计了面向服务的架构(Service Oriented Architecture)。

Service Oriented Architecture

面向服务的架构在服务器端的开发中很常见,它把业务分成了多个逻辑独立的组件。一个组件相当于一个 Service,封装了与其业务相关的功能,如 UserService 负责用户的注册、登入等,而 BabyService 有 Baby 的增加、移除、以及数据的记录等。Glow 服务器端的架构实际就是面向服务的。在 Baby App iOS 架构中引入 Service 的概念,是 App 开发过程中迭代的结果,灵感也是来自我们服务器端的架构。

可以看到,Service 是对整个架构纵向逻辑切分的结果。抛开业务逻辑谈 Service 意义不大,Service 通常与数据库表的设计紧密相关。

横向的逻辑切分将 Baby App iOS 的架构自上而下切分成三个层(Layer):

  • 应用层(Application Layer)
  • 服务层(Service Layer)
  • 数据层(Data Access Layer)

服务层和数据层把复杂的逻辑封装起来,作为 Framework 提供接口给上层调用。应用层只能调用服务层暴露出来的接口,而不能直接调用数据层。层次结构加强了可重用性和可测试性。应用层调用服务层提供的简单接口获得数据或者实行用户操作。服务层也不需要知道数据层中网络请求,服务器同步,以及数据持久化的具体实现。服务层,数据层,以及应用层都能很容易实现各自的单元测试(Unit Test)。

Framework 是很棒的工具。把服务层和数据层打包成 Framework,不仅帮助构建解耦可重用的代码,同时 App 的结构和业务逻辑也更加清晰。

应用层(Application Layer)

应用层也可以叫展示层(Presentation Layer),负责 UI展示逻辑。从 Code 角度说,就是 UIView 跟 UIViewController 的集合。复杂的逻辑都封装到了下层,UIViewController 就变得十分轻量。在 Glow Baby 中,一个 View Controller 通常 200 至 300 行代码之间,主要负责三件事:

  1. 从 Service 获得数据(ViewModel)并展示
  2. 响应用户操作,调用相应的 Service 接口
  3. 监听 Service 层发出的消息,并执行相应操作,如更新 UI

从 Service 获取的 ViewModel 实例并不是 NSManagedObject 或者其他持久化的 model 实例,跟 MVVM 中的 ViewModel 也不一样。在 Baby App 中,它只是简单的 Swift Struct,提供应用层需要的数据值。使用 Struct 的好处主要是:

  • 值类型(Value Type): 简单、容易理解,线程安全
  • 松耦合的 View Controller,减少 View Controller 之间可能的交互
  • 减少了 Statefulness 和 Mutability
  • 更高效、占用更少内存

使用 Struct 也就意味着想要底层持久化 Model 的更改放映到 UI 上,你必须通过 Service 再抓一次数据。也许有人认为这是使用 Struct 的一个缺点。其实不是,这应该是优点。因为 Immutable 的 ViewModel ,让 View Controller 变得更加简单,你不用担心其他地方的代码会更改你的 ViewModel 实例。调试起来也会更加方便,代码更容易理解、可读更高。WWDC 中有好几个视频都对 Struct 的使用和优势进行了详解。

Baby App 支持 Theme,因此 Baby App 的 View Controller 还会调用 Theme 对象来设定 UI 的样式。但对 View 样式的设定都封装在了 Theme 里,所以并没有增加太多代码量及 View Controller 的复杂度。

服务层(Service Layer)

服务层定义了一系列 Service 和供给应用层使用的 ViewModel。Service 封装了 App 主要的业务逻辑,负责把底层持久化的 Model 和网络请求返回的 JSON 转换为 ViewModel,再提供给应用层使用。这样的分离即加强了 Immutablility 和 Statelessness,也让应用层中的 ViewController 更轻量,只需几行 Service calls。Service 虽然承担大部分业务逻辑,但一个 Service 通常也就 300 行左右的代码量,这得益于数据层的封装和抽象。

数据层(Data Access Layer)

数据层的作用是提供简化的数据访问接口,主要有 3 个模块:

  • 数据存储(Persistence)
  • 网络请求(Network)
  • 数据同步(Data Synchronization)

数据存储我们使用的是 Core Data,也可以用 Realm 或者其他数据库代替。网络请求我们使用了 Moya 进行抽象,使 API 的设计和调用更简洁,并支持我们 Server 自定义的错误。数据同步模块,会自动同步本地和服务器端的用户数据。

Conclusion

在 iOS 上,MVC 因 Controller 的臃肿而遭到众人诟病。但其实 MVC 作为最基础的设计模式,展现了一个架构的精髓 - 抽象分离。这是我们应该学习思考的,而不是盲目从其他架构模式中选择一个来代替 MVC。Glow Baby iOS 的架构从可以看作是一种 MVC。从整体看,数据层是 Model,服务层是 Controller,应用层是 View。而如果看细节的地方,应用层跟服务层提供的 ViewModel 也可以看做一个 MVC:ViewModel - UIViewController - UIView.

设计架构也没有「最好」或者「最正确」的方式,设计本身就是一项极具创造力的工作。但架构是有好坏区别,一个好的架构应该是对团队成员最为直观,同时扩展性良好的架构。

开篇先简单介绍下架构的整体,后续文章会具体分析各个层次的实现细节。