业务架构的救世主是 MVI(一)

复杂度

Android 架构演进系列是围绕着复杂度向前推进的。

软件的首要技术使命是“管理复杂度” —— 《代码大全》

因为低复杂度才能降低理解成本和沟通难度,提升应对变更的灵活性,减少重复劳动,最终提高代码质量。

架构的目的在于“将复杂度分层”

复杂度为什么要被分层?

若不分层,复杂度会在同一层次展开,这样就太 ... 复杂了。

举一个复杂度不分层的例子:

小李:“你会做什么菜?”

小明:“我会做用土鸡生的土鸡蛋配上切片的番茄,放点油盐,开火翻炒的番茄炒蛋。”

听了小明的回答,你还会和他做朋友吗?

小明把不同层次的复杂度以不恰当的方式揉搓在一起,让人感觉是一种由“没有必要的具体”导致的“难以理解的复杂”。

小李其实并不关心土鸡蛋的来源、番茄的切法、添加的佐料、以及烹饪方式。

这样的回答除了难以理解之外,局限性也很大。因为它太具体了!只要把土鸡蛋换成洋鸡蛋、或是番茄片换成块、或是加点糖、或是换成电磁炉,其中任一因素发生变化,小明就不会做番茄炒蛋了。

再举个正面的例子,TCP/IP 协议分层模型自下到上定义了五层:

  1. 物理层
  2. 数据链路成
  3. 网络层
  4. 传输层
  5. 应用层

其中每一层的功能都独立且明确,这样设计的好处是缩小影响面,即单层的变动不会影响其他层。

这样设计的另一个好处是当专注于一层协议时,其余层的技术细节可以不予关注,同一时间只需要关注有限的复杂度,比如传输层不需要知道自己传输的是 HTTP 还是 FTP,传输层只需要专注于端到端的传输方式,是建立连接,还是无连接。

有限复杂度的另一面是“下层的可重用性”。当应用层的协议从 HTTP 换成 FTP 时,其下层的内容不需要做任何更改。

引子

该系列的前三篇结合“搜索”这个业务场景,讲述了不使用架构写业务代码会产生的痛点:

  1. 低内聚高耦合的绘制:控件的绘制逻辑散落在各处,散落在各种 Activity 的子程序中(子程序间相互耦合),分散在现在和将来的逻辑中。这样的设计增加了界面刷新的复杂度,导致代码难以理解、容易改出 Bug、难排查问题、无法复用。
  2. 耦合的非粘性通信:Activity 和 Fragment 通过获取对方引用并互调方法的方式完成通信。这种通信方式使得 Fragment 和 Activity 耦合,从而降低了界面的复用度。并且没有一种内建的机制来轻松的实现粘性通信。
  3. 上帝类:所有细节都在界面被铺开。比如数据存取,网络访问这些和界面无关的细节都在 Activity 被铺开。导致 Activity 代码不单纯、高耦合、代码量大、复杂度高、变化源不单一、改动影响范围大。
  4. 界面 & 业务:界面展示和业务逻辑耦合在一起。“界面该长什么样?”和“哪些事件会触发界面重绘?”这两个独立的变化源没有做到关注点分离。导致 Activity 代码不单纯、高耦合、代码量大、复杂度高、变化源不单一、改动影响范围大、易改出 Bug、界面和业务无法单独被复用。

详细分析过程可以点击下面的链接:

  1. 写业务不用架构会怎么样?(一)
  2. 写业务不用架构会怎么样?(二)
  3. 写业务不用架构会怎么样?(三)

紧接着又用了三篇讲述了如何使用 MVP 架构对该业务场景的重构过程。MVP 的确解决了一些问题,但也引入了新问题:

  1. 分层:MVP 最大的贡献在于将界面绘制与业务逻辑分层,前者是 MVP 中的 V(View),后者是 MVP 中的 P(Presenter)。分层实现了业务逻辑和界面绘制的解耦,让各自更加单纯,降低了代码复杂度。
  2. 面向接口通信:MVP 将业务和界面分层之后,各层之间就需要通信。通信通过接口实现,接口把做什么和怎么做分离,使得关注点分离成为可能:接口的持有者只关心做什么,而怎么做留给接口的实现者关心。界面通过业务接口向 Presenter 发出请求以触发业务逻辑,这使得它不需要关心业务逻辑的实现细节。Presenter 通过 view 层接口返回响应以指导界面刷新,这使得它不需要关心界面绘制的细节。
  3. 有限的解耦:因为 View 层接口的存在,迫使 Presenter 得了解该把哪个数据塞给哪个 View 层接口。这是一种耦合,Presenter 和这个具体的 View 层接口耦合,较难复用于其他业务。
  4. 有限内聚的界面绘制:MVP 并未向界面提供唯一 Model,而是将描述一个完整界面的 Model 分散在若干 View 层接口回调中。这使得界面的绘制无法内聚到一点,增加了界面绘制逻辑维护的复杂度。
  5. 困难重重的复用:理论上,界面和业务分层之后,各自都更加单纯,为复用提供了可能性。但不管是业务接口的复用,还是View层接口的复用都相当别扭。
  6. Presenter 与界面共存亡:这个特性使得 MVP 无法应对横竖屏切换的场景。
  7. 无内建跨界面(粘性)通信机制:MVP 无法优雅地实现跨界面通信,也未内建粘性通信机制,得借助第三方库实现。
  8. 生命周期不友好:MVP 并未内建生命周期管理机制,易造成内存泄漏、crash、资源浪费。

详细分析过程可以点击下面的链接:

  1. MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?(一)
  2. MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?(二)
  3. MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?(三)

再然后用了两篇讲述了 MVVM 架构是如何解决 MVP 的痛点:

  1. ViewModel 的引入使得“有免死金牌的业务层”成为可能,也使得跨界面之间的业务逻辑共享以及通信变得轻松。
  2. LiveData 的引入使得业务层成为数据持有者以数据驱动刷新界面,还避免了生命周期问题以及内存泄漏风险。
  3. 因为数据持有者,MVVM 也引入了新的复杂度,首先是不好处理的粘性数据问题,更棘手是更新数据的方法是带有副作用的,由此会引发界面状态不一致问题。

关于 MVVM 架构的详细分析可以点击下面的链接:

  1. “无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(一)
  2. “无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(二)

从这一篇开始,试着引入 MVI 架构的思想进行搜索业务场景的重构,看看是否能解决上述痛点。

在重构之前,再介绍下搜索的业务场景,业务流程如下:在搜索条中输入关键词并同步展示联想词,点联想词跳转搜索结果页,若无匹配结果则展示推荐流,返回时搜索历史以标签形式横向铺开。点击历史可直接发起搜索跳转到结果页。

将搜索业务场景的界面做了如下设计:

搜索页用Activity来承载,它被分成两个部分,头部是常驻在 Activity 的搜索条。下面的“搜索体”用Fragment承载,它可能出现三种状态 1.搜索历史页 2.搜索联想页 3.搜索结果页。

Fragment 之间的切换采用 Jetpack 的Navigation。关于 Navigation 详细的介绍可以点击Navigation 组件使用入门 | Android 开发者 | Android Developers

MVP & MVVM & MVI 架构图比对

MVI 和 MVP/MVVM 不是非此即彼的关系,它们是不同维度的。

MVP 和 MVVM 关心的是业务层的形态,MVP 中业务层用 Presenter 表达,如下图所示:

而 MVVM 中业务层用 ViewModel 表达,如下图所示:

MVI 不关心业务层形态,而是关心业务数据变换及流动的形态。

将名词解释放在一边,先来看看架构图表达形式上的差异。

MVI 的图和其他两个相比有一个显著的区别,之前使用的是{},比如View{Presenter{}}表示 View 持有一个 Presenter。但 MVI 的架构图中使用的是(),表示一个函数,即 Model 是 Intent 的函数,View 是 Model 的函数。

函数 & 函数式编程

函数即两个集合之间的一种对应关系。若对集合 x 中的值施加法则 f 后都能唯一对应集合 y 中的一个值,则说 y 是 x 的函数,记为y = f(x)

这里的关键是唯一自变量 x 对应唯一应变量 y。在数学中这是简单的一元函数,在编程中这是一种低复杂度,低复杂度意味着不会出错。

把一元函数进一步具象化到界面刷新这个 case 上,可以表达为 “一个 Model 唯一对应一个界面状态”,记为view = f(model)

按照这个思想回看一下 MVVM 架构中的函数关系(同样的问题也存在于 MVP 中),界面的状态有若干个LiveData<Model>表达,即 viewState = f(model1, model2, model3, ...),其中任何一个 model 都可以独立发生变化,而任一 model 变化后,都会引起 viewState 的变化,即所有 model 的任一排列组合与一个 viewState 对应。这个复杂度就很高,出错的概率就很大,这样的错误称为界面状态不一致,即界面状态和你预想的不一样,因为有一种排列组合没有考虑到。关于实战中界面状态不一致的实例分析可以点击“无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(一)

MVI 把函数对应关系做到了极致,它把界面刷新的整个流程都表达成了一元函数。首先界面发起的动作被抽象为数据 Intent,Intent 的函数是 Model,即model = f1(intent),表示任一业务动作会产生唯一对应的 model,紧接着任一 Model 对应唯一界面状态,即view = f2(model)

f1 和 f2 这两个一元函数描述了业务意图、数据、界面状态之间一一对应的关系,在需求文档确定下来的同时,这一一对应的关系就已固定下来。

将函数的思想应用到编程,就产生了函数式编程

函数式编程是一种编程范式,即关于如何编写程序的方法论。它的主要思想是把运算过程尽量写成一系列嵌套的函数调用。

这种编程范式最大的好处是没有副作用。副作用是指函数内部与外部互动,产生运算以外的其他结果。最典型的情况是修改全局变量。关于 MVVM 中的副作用详解可以点击“无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(二)

而函数式编程中只有输入参数和返回值,不修改全局变量。从耦合的角度来看,函数式编程中的函数只包含运算且不与任何东西耦合,这使得它复杂度低、运行结果可预测、易于单元测试、调试。函数式编程是 MVI 架构相较于其他架构的一个显著不同点。

综上,使用 MVI 架构的开发过程即是:

运用函数式编程思想将需求翻译成业务意图(I)、数据(M)、界面状态(V)间的函数关系,再用响应式编程的方式将其串联成数据流的过程。

最后将函数串联的方式是响应式编程。它是一种面向数据流的编程范式。关于响应式编程的详细介绍可以点击Android 架构之 MVI 雏形 | 响应式编程 + 单向数据流 + 唯一可信数据源

除了编程范式上之外,MVI 架构还有一些其他的不同点,引用之前文章的总结:

MVI = 响应式编程 + 单向数据流 + 唯一可信数据源

关于这三条规范的剖析可以点击该系列文章:

  1. Android 架构之 MVI 雏形 | 响应式编程 + 单向数据流 + 唯一可信数据源
  2. Android 架构之 MVI 初级体 | Flow 替换 LiveData 重构数据链路
  3. Android 架构之 MVI 完全体 | 重新审视 MVVM 之殇,PartialChange & Reducer 来拯救
  4. Android 架构之 MVI 究极体 | 状态和事件分道扬镳,粘性不再是问题

其中的响应式编程会运用 Kotlin Flow,关于它的详细介绍可以点击:

  1. Kotlin 异步 | Flow 应用场景及原理
  2. Kotlin 异步 | Flow 限流的应用场景及原理

总结

在具体分析 MVI 的实现细节之前,对其做一个概念性总结:

MVI 用数据流来理解界面刷新:界面是数据流的起点(生产者)也是终点(消费者),界面发出的数据叫意图,意图会用函数式编程的方式被变换为状态,最终状态通过响应式编程的方式流向界面,界面消费状态完成刷新。在这个流动的过程中,若保证了唯一可信数据源,就能实现单向数据流。

下一篇会基于搜索这个业务场景,详细展开 MVI 的实现细节。

作者:唐子玄
链接:https://juejin.cn/post/7178526966155313208

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容