稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Android应用的架构演进

本文译自「Architectural Evolution of and Android app」,原文链接https://herrbert74.github.io/posts/architectural-evolution-of-an-app/,由Zsolt Bertalan发布于20258月19日。

本文将解释 Android 应用在发展过程中可能经历的各个阶段。不同的架构可能看起来截然不同(更不用说对细节的不同看法),但其背后的理念是相同的。我将通过整洁架构 (Clean Architecture) 和我的 FlickSlate 代码库(链接:https://github.com/herrbert74/FlickSlate%EF%BC%89%E6%9D%A5%E8%A7%A3%E9%87%8A%E8%BF%99%E4%B8%80%E7%82%B9%E3%80%82

我还想介绍一下大型应用中的常见模式:超级层功能组。每当我开发的应用达到一定规模时,都会用到这些模式,但我从未制定过相关的基本规则。我希望把它们写下来,能让我和其他人更容易理解。

超级层

你可能还记得我之前的文章,我的应用有一个独立的领域层数据层和展现层(从现在开始是UI层)都依赖于它。这些是基本的水平层。包含它们的垂直层也称为功能层。

我创造了 “超级层” 这个术语,是为了在基本水平层之上引入一个更广泛的层次结构。我们需要它们,因为水平层的范围可能有所不同,这意味着它们不仅可以涵盖功能层,还可以涵盖更广泛的范围。我将定义功能层功能组应用级(共享)多应用级(基础) 超级层。

请参阅下图。

在我上面解释的基本情况下,我们有功能层,其中整洁架构层仅在功能层内部可见。

接下来,功能组层是介于应用范围层和功能范围层之间的中间层。由于它是对超级层的最后且最不明显的补充,我将在稍后详细讨论。

共享、通用或应用范围层通常包含与业务相关或整个应用独有的类。这是可以添加领域、数据和 UI 模块的最低层。

基础层、基础设施层或基础结构层可以跨多个应用使用。它不包含领域、数据或 UI 层,但包含这些层之间通用的类。我通常有一个Kotlin 模块和一个 Android 基础模块。我创建了 BaBeStudios-Base 库项目(https://bitbucket.org/babestudios/babestudiosbase/src/master/%EF%BC%89%EF%BC%8C%E4%BB%A5%E4%BE%BF%E5%9C%A8%E5%A4%9A%E4%B8%AA%E5%BA%94%E7%94%A8%E4%B8%AD%E5%A4%8D%E7%94%A8%E8%BF%99%E4%BA%9B%E6%A8%A1%E5%9D%97%EF%BC%8C%E4%BD%86%E6%88%91%E7%9A%84%E5%BA%94%E7%94%A8%E4%B8%AD%E4%B9%9F%E6%9C%89%E4%B8%80%E4%BA%9B%E5%9F%BA%E7%A1%80%E6%A8%A1%E5%9D%97%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%B0%9A%E6%9C%AA%E5%8C%85%E5%90%AB%E5%9C%A8%E8%AF%A5%E5%BA%93%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84%E4%BB%A3%E7%A0%81%E3%80%82 BaBeStudios-Base 也可从 MavenCentral 获取,但目前尚无相关文档。

随着应用程序规模的扩大和模块的引入,你需要引入上述各层。

接下来,让我们看看随着应用程序规模的扩大,我通常采取的模块化步骤。

步骤 1:单模块

当你确定应用程序规模不会超过最大规模,或者时间紧迫时,你可以选择单模块应用程序或单体应用。我不确定单模块应用程序的最大规模是多少。这可能因人而异。

此最大规模也不同于阈值规模,你应该从该阈值开始模块化应用程序,或者更通俗地说,你应该从该阈值切换到流程的下一步。我认为阈值大小远小于单模块应用程序的最大大小,这意味着你应该在达到该限制之前就开始模块化。

对于大多数应用程序,我甚至建议从模块化开始,而不是从单个模块开始,因为与接近限制(比如 10 或 20 kLOC)时才开始模块化相比,这样做的开销和干扰程度要小得多。

这是一个单模块应用程序的包树示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
├── data
│   ├── database
│   ├── local
│   ├── repository
│   └── remote
├── domain
│   ├── api
│   ├── model
│   └── usecase
├── presentation
│   ├── master
│   └── detail
└── shared

上面图中的叶子节点代表包,它们可以包含此处未显示的其他包。下面我将展示一个类似的模块结构,其中以冒号开头的名称代表模块,双冒号代表根模块。

步骤 2:简单模块化

对于小型应用程序,我使用简单模块化,而不是下一章的完全模块化。这里我们只介绍每个功能的模块。每个功能将包含一个领域模块,以及依赖于领域模块的数据和呈现模块(在我的例子中)。

我们还引入了一个共享模块,其中包含所有功能模块的通用代码。

应用程序的超层由一条水平线分隔。每个层都依赖于其下方的所有层。数据和呈现依赖于领域模块,但这并未在图中显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
::feature:Feature A

├── :feature/:featurea/:data
├── :feature/:featurea/:domain
└── :feature/:featurea/:presentation

::feature:Feature B

├── :feature/:featureb/:data
├── :feature/:featureb/:domain
└── :feature/:featureb/:presentation

 ::feature:Feature C

├── :feature/:featurec/:data
├── :feature/:featurec/:domain
└── :feature/:featurec/:presentation


::shared:data

├── database
├── local
├── repository
└── remote

::shared:domain

├── api
├── model
└── usecase

::shared:presentation

├── compose
├── design
└── util


::base:kotlin

步骤 4:功能组

当你的应用完全模块化后,共享层会逐渐成为单体应用,并成为构建过程中的瓶颈

我经常在超过一定规模的应用(大约 50 到 80 kLOC)时遇到这种情况。你需要共享太多内容,因此你的共享领域模块和展示模块变得过大。你开始注意到,在所有垂直模块之间共享代码也是一种浪费,因为你只想在两个或最多三个或四个模块之间共享代码。你会发现越来越多的新代码被添加到这些模块中,而增量缓存的效率越来越低,因为你频繁地修改代码,导致它们失效。

你可能会说,可以通过适当地重构和构建应用程序,或者复制一些代码来避免这种情况,但这可能比你想象的要难。我发现这些建议并没有起到什么帮助作用。或者你可能手头有一个遗留应用,没有时间完美地重构所有内容。

我目前解决这个问题的方法是识别功能组,即一组具有大量公共依赖项的功能。这样,你就可以通过创建更具凝聚力的模块,水平拆分共享层中的部分代码。

我最近开发的所有大型应用中都有两个不同的功能组。

第一个功能组与核心业务功能相关:例如,一个模块围绕产品列表,另一个模块围绕产品详情,第三个模块围绕收藏产品。因此,我们可以将这个功能组命名为产品。另一个功能组围绕支付、广告或订阅,或者有时是这些功能的组合。这关系到应用的盈利方式。因此,我们可以将功能组命名为“支付”

大型应用中可能会有五个或更多功能组。每个功能组将包含领域模块、数据模块和演示模块(如果需要)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
feature/Feature A

feature/Feature B

feature/Feature C

feature/Feature D

feature/Feature E

feature/Feature F


:data

:domain

:presentation


:kotlin-base

何时使用哪一步?

你肯定希望将单体应用用于永远不会投入生产或一次性使用的应用程序。这些应用程序可以是概念验证 (PoC, Proof of Concept) 应用程序、用于第三方错误的最小可复现示例 (MRE, Minimum Reproducible Example) 应用程序,或用于求职申请的测试挑战应用程序。

对于所有其他情况,我建议先从部分或完全模块化开始,然后根据需要升级到下一步,或者尽可能提前升级,以减少以后的麻烦。

你可以在我的 FlickSlate 代码库(链接:https://github.com/herrbert74/FlickSlate%EF%BC%89%E4%B8%AD%E6%89%BE%E5%88%B0%E4%B8%8A%E8%BF%B0%EF%BC%88%E5%A4%A7%E9%83%A8%E5%88%86%EF%BC%89%E5%86%85%E5%AE%B9%E7%9A%84%E6%9C%89%E6%95%88%E7%A4%BA%E4%BE%8B%EF%BC%8C%E6%88%91%E6%9C%80%E8%BF%91%E5%B0%86%E5%85%B6%E6%9B%B4%E6%96%B0%E5%88%B0%E4%BA%86%E5%AE%8C%E5%85%A8%E6%A8%A1%E5%9D%97%E5%8C%96%E9%98%B6%E6%AE%B5%E3%80%82%E8%99%BD%E7%84%B6%E4%B8%BA%E6%97%B6%E8%BF%87%E6%97%A9%EF%BC%8C%E4%BD%86%E6%9C%89%E5%8A%A9%E4%BA%8E%E6%BC%94%E7%A4%BA%E8%BF%99%E4%BA%9B%E5%8E%9F%E5%88%99%E3%80%82%E5%BD%93%E7%84%B6%EF%BC%8C%E4%B9%9F%E5%8F%AF%E4%BB%A5%E9%81%BF%E5%85%8D%E4%BB%A5%E5%90%8E%E7%9A%84%E9%BA%BB%E7%83%A6%E3%80%82

FlickSlate 尚未升级到功能组。这毫无意义,因为共享层还无法拆分为功能组。该应用没有盈利功能,所以只能对节目进行分组,不过目前共享层已经完美地实现了这一点。

这篇文章主要源于我对互联网上关于何时以及如何模块化的建议一概而论的不满:要么建议不加区分地进行模块化,要么有人对任何进行模块化的人大喊“过度工程”。我希望这篇文章能帮助你在何时以及如何进行模块化方面做出更明智的决定。

Comments