浅谈客户端业务架构

Author Avatar
mrriddler 9月 09, 2018

这一篇文章中,笔者带着自己有限的经验,会从一些经典架构出发,结合客户端业务复杂度特点,针对一些常见问题,提出一些建议和看法。文中有很多个人观点,鉴于笔者才疏学浅,难免有失偏颇,望各位看官指点一二。架构这个话题很广,又没有明确定义,本文就不以体系的角度,仅以业务开发上的架构为题探讨。

经典架构

MVC

MVC历史悠久,SmallTalk时代就已经有概念并被实现出来。不同场景或者说不同的应用对于MVC的实现,有很多区别,这些区别在于职责的划分,层与层之间的通信流方向。所以,以职责的划分或者以某种特定的通信流方向去定义MVC是有问题的,并且也没法定义,毕竟职责是观点,如何划分因人而异。MVC历史可参考这篇文章(https://draveness.me/mvx)。

MVC的主旨在于,将展示层和模型层分层,展示层触发模型层改变,而模型层改变,无论主动通知展示层还是被动同步给展示层,结果为展示层基于模型层做改变,而展示层又可分为视图层和交互控制层。MVC虽然没有特定的通信流方向,不同的实现可以不一致,但是每种实现要有自己固定的通信流。

MVC其实已经不单单是一个具体的架构,而是一种架构指导思路,MVC的分层和分层后的固定的通信流,就是架构本身的含义

MVP

仔细端详MVP的概念,实际上MVP完全符合MVC的。MVP可以看做就是一个规定好职责和通信流的MVC蓝图,也就是MVP是MVC的一个实例。

MVVM

MVVM相较于MVC和MVP,很明显,多抽象一层展现模型层ViewModel。同样地,职责和通信流并不是MVVM的主旨,不过,因为MVVM分层和通信流很明确,不会产生歧义,导致业界使用的MVVM,其职责和通信流基本一致。

VIPER

VIPER相比于前几个架构,自然多了几个抽象层,也规定好了职责和通信流。不过,这都不是重点,其中最闪光的就是Router。Router不仅只负责页面跳转这样的职责,Router本质可以做到不同模块的通信。这就代表,VIPER从根本上适合解决业务单元化的问题,或者换句话说,没有到业务单元化的复杂度时候,VIPER显不出自己的长处。

Router是一个很好的架构思路,从VIPER拓展出来,可以将View撇开来,单以IPER作为一个业务单位。而这点,非常有意思,以这点作为基础,就可以做出领域驱动设计,将复杂的业务以IPER组织好,形成领域。但客户端业务复杂度远比不上服务端,这一点在客户端是否值得、能解决一些实际问题仍需验证。

客户端复杂度

此章节以不同于以上章节的角度,从客户端业务复杂度的角度来看客户端。

客户端和前端,说到底都是GUI系统。而GUI系统的核心:应用状态如何流动的。这个核心是前端、客户端都相通的地方,能把这个核心的复杂度降下来,整个App的棘手问题能减少大半。

从这一点,延伸出客户端业务经常面对的三大问题:

  • 数据流向
  • 多场景数据一致性
  • 单元化

这三点我们来逐个击破。

数据流向

数据流向这点是从前端学习过来的,Facebook也提出了解决办法Flux来确保单向数据流,易于人脑理解,也让数据流更可预测。

  • view:类似于controller-views,将数据变动隔离出去。view只能引起数据变动和等待数据变动的通知。view这一部分更适合用React(Immutable化的视图)。
  • store:明确抽象数据状态变动部件,只有store中的数据是可变的,其他部件中的数据都是不可变的,状态变动限制在局部。
  • dispatcher:明确抽象调度者部件,在调度者里处理有联带效应或者既定顺序的数据变动。
  • action:由于其他部件不能更改数据,若想更改数据必须提出申请,抽象更改数据单位,交给dispatcher处理。

相对于Flux,还有其变种Redux,Redux更像是声明式的Flux,整体架构通过添加reducer,去掉dispatcher,更倾向于声明而不是命令式。

客户端使用的语言不像JavaScript,抽象action的代价有点大,完全照搬硬套只会水土不服。不过,只要抓住关键,数据流能单向下来,也就达到了解决问题的目的了,具体案例移步(http://wereadteam.github.io/2017/09/30/reflow/)。

多场景数据一致性

这个指的是等同数据在不同场景下的同步问题。比如说个经典问题,购物车列表页和商品详情页能对同一商品进行一系列操作,这两个页面如何数据同步。在这个例子中,同一商品就是等同数据。这个例子比较好理解,这个问题一般在同一个业务模块下,也有可能会跨多个业务模块。

解决这个问题的本质就是需要一个订阅通知系统,将能改变的实例看做系统中的分布点,分布点与分布点之间需要同步。

最常见的思路就是,在业务层构造一个订阅通知系统,业务使用数据要订阅通知,如果有改变数据的行为,要推送出去。这个思路的订阅通知系统是数据驱动的push系统。

而从这个思路出发,还有两条延伸选择:

这个思路有两个明显的问题。其一,数据更新的粒度。数据和视图实际上是一个对应的树状结构,数据更新后,对应的视图要重绘。而这个有两个策略,要么整颗树更新一遍,要么具体到子树更新。

  • 整颗树更新:性能较差,补救措施就是将数据做好diff,尽量减少数据引起的重绘。
  • 子树更新:将数据和视图子树匹配大量的工作量,粒度越小工作量越大。

其二,线程安全问题。多场景下可能从不同的线程访问数据,其中多线程读写就会造成多次执行结果不一致的情况,这也就引起了线程不安全。而解决这个问题比较好的思路便是能够对数据源支持读读并发,而读写、写写串行的方案。

响应式编程也是解决多场景数据一致性的一个方案,不过不同于上文,上文尝试从架构角度解决这个问题,响应式编程尝试从范式的角度解决这个问题。

单元化

此章节关注的是复杂页面业务的单元化,与组件化不同。当页面业务越来越复杂,无论从哪种M**架构折腾都力不从心,页面承载太多的业务,总有一部分代码多到无法维护。

解决这种问题的思路就是将页面划分为多个独立的部分,作为单元,单元与单元之间不直接通信,单元与单元之间感知度到最小。

从架构上来讲,这种划分使得整个页面业务划分出多个业务单元,代码也就随着业务单元的划分抽离了。这种解决思路也不光解决了这类问题,还提供了动态界面的能力和相似业务的复用能力。具体案例移步

这个思路也有很多的点可以深入挖掘,性能、后台以及整套开发流程。

RIBs

RIBs是uber代言的移动端架构框架,详情移步(https://github.com/uber/RIBs/wiki)。RIBs的排场足够占一个章节,里面有很多思路和工程细节值得学习,RIBs的定位相较于客户端,类似于React相较于前端。

核心思路如下:

  • 彻底组件化,并将逻辑业务和界面业务划分为等同组件。不同于国内的模块化方案,只将业务粗粒度划分,RIBs将无论是逻辑业务或界面业务组件划分到细粒度层次。
  • 规范组件架构为VIPER改进版VIPRBCD。统一后的组件都是VIPRBCD架构,其中Router是组件互相解耦的核心,Builder、Component、Dependency是组件互相定义依赖的核心。单个组件内,数据流单向,Interactor充当Store。
  • 业务场景的流动以组件树模型化,切换业务场景,形式化为树状结构添加子树或移除子树。
  • 用依赖注入的方式管理依赖,动态依赖通过响应式编程中的流管理。

RIBs彻底组件化后,再将组件规范为VIPRBCD,RIBs将抽象粒度降到尽可能的小。组件树形式化状态流动,RIBS很自然的处理组件的装卸。这都源于uber的工程特点:

  • 快速迭代,在原功能代码基础上快速、多次修改。
  • 注重测试。
  • 代码分为core和optional,core代表主流程功能,optional代表实验性功能。core低容错,optional可随时上和下。
  • 跨平台开发和混合code review。

彻底组件化和规范VIPRBCD,使组件更加独立,在快速迭代过程中,提高组件拓展性和提高系统的稳定性。并且便于单元测试。组件树形式化状态流动,使装卸实验性功能非常方便。

带来的缺点就是胶水代码更多,学习成本加大,编写代码门槛提高。虽然RIBs提供了全套工具支持开发,但还是很繁琐。uber原文移步(https://eng.uber.com/new-rider-app/)。

RIBs的设计来自于uber的工程特点,架构师在组件的抽象粒度上放细并注重测试来提升整体系统的稳定性

共享声明:本作品采用 知识共享署名 4.0 国际许可协议 转载时请注明作者和原文链接
本文链接:http://blog.mrriddler.com/浅谈客户端业务架构/