共计 7682 个字符,预计需要花费 20 分钟才能阅读完成。
前言
古代的 Android 我的项目都是 Gradle 工程,所以大家都习惯于用 Gradle Module 来划分和组织代码,Module 的大量应用也带来一个问题,一个大我的项目往往几十上百的 Module,然而当数量泛滥的 Module 之间的依赖关系不合理时,依然会重大拖慢工程的编译速度,如何更迷信地组织 Gradle Module 是 Android 开发畛域的广泛需要。
从事 Android 开发的同学可能都据说过 Clean Architecture,即所谓整洁架构。Google 举荐大家应用它对 MVVM 进行更正当的分层。整洁架构的概念出自以下这本书(国内译本:代码整洁之道),对于这本书以及作者 Bob 大叔的小名这里就不多介绍了,说这是软件架构方面的圣经也不为过。
除了优化 MVVM 这样的业务架构,这本书在组件设计方面也产出了不少最佳实际和方法论,可用来优化 Gradle 这样的工程架构。本文就来探讨如何基于整洁架构中的各种设计准则来设计咱们的 Gradle Module。
文章目录如下:
-
Module 粒度划分
- 复用公布等价准则(REP)
- 独特关闭准则(CCP)
- 独特复用准则(CRP)
- 三准则的衡量
-
Module 依赖关系
- 无环依赖准则(ADP)
-
稳固依赖准则(SDP)
- 稳定度公式
-
稳固形象准则(SAP)
- 形象度公式
-
不稳定度与形象度的关系
- 苦楚区与无用区
- 总结
Module 粒度划分
参考 Clean Architecture 中对组件的定义:
组件是软件的部署单元,是整个软件系统在部署过程中能够独立实现部署的最小实体。例如,对于 Java 来说,它的组件是 jar 文件。而在 Ruby 中,它们是 gem 文件。在 .Net 中,它们则是 DLL 文件。
Android 中 Gradle Module 是公布 JAR 或者 AAR 的根本单元,因而 Module 能够看作是一个组件,在 Module 粒度划分上,咱们套用书中对于组件划分的三个准则:
- 复用公布等价准则(Release Reuse Equivalency Principle)
- 独特关闭准则(The Common Closure Principle)
- 独特复用准则(The Common Reuse Principle)
复用公布等价准则(REP)
软件复用的最小粒度应等同于其公布的最小粒度
REP 通知咱们 Module 划分的一个根本准则就是代码的可复用性,当一些代码有被复用的价值时,它们就应该被思考拆分为 Module,可用来独立公布。此外 REP 还要求咱们留神 Module 不能拆分适度。当咱们公布 AAR 时都须要为其设定公布版本号,其中一个重要起因是如果不设定版本号就无奈保障被组件之间可能彼此兼容。这在 androidx 系列组件中尤为突出,咱们常常遇到因为版本不统一造成的运行时问题,产生这种不统一的一个重要起因就是,组件的拆分适度。
如果两个能够独立公布的组件,它们总是作为一个整体被复用,就会呈现可复用的粒度大于可公布粒度的问题,也就增大了版本抵触的概率,此时能够思考将它们合二为一,同时公布、防止版本不统一。
在小团队中这个问题不突出,因为通过人为约定能够保障所以组件同时公布,然而在跨团队的大型项目中,如果一个性能的降级总是要多个团队一起配合,那沟通老本是难以忍受的。
独特关闭准则(CCP)
组件中的所有类对于同一种性质的变动应该是独特关闭的,即一个变动的影响应该尽量局限在单个组件外部,而防止同时影响多个组件,咱们应该将那些会因为雷同目标而同时批改的类放到同一个组件中,而将不会为了雷同目标同时批改的那些类放到不同的组件中。
绝对于 REP 关注的可复用性,CCP 强调的是可维护性,很多场景下可维护性绝对于可复用性更加重要。CCP 要求咱们将所有可能被一起批改的类集中在一起,两个总是被一起批改的类应该放入同一组件。这和大家熟知的 SOLID 设计准则中的 SRP(繁多职责)很相似,SRP 要求总是一起批改的函数应该放在同一个类,CCP 能够看作是组件版本的 SRP。
顺便一提:SOLID 设计准则也是出自 Clean Architecture 一书,SOLID 针对的是 OOP 的类和接口,而本文探讨的是更大粒度的组件。
有的人在 Android 我的项目中喜爱依照代码的功能属性划分 Module,比方一个 MVVM 架构的工程,目录可能如下划分:
+ UI
+ Logic
+ Repository
+ API
+ DB
但理论开发中,很少只批改 UI 或者只批改 Logic,大多是围绕某个 feature 进行垂直批改,这种跨 Module 批改显然违反了 CCP 准则,因而以业务属性为单位来进行 Module 划分可能更加正当,比方一个短视频利用的目录构造应该是
+ VideoPlay
+ ui
+ data
+ ...
+ VideoCreation
+ Account
+ ...
这样,咱们的批改能够在单个组件中闭环实现,在 Gradle 编译中,缩小受影响的模块,晋升编译速度。
独特复用准则(CRP)
组件中的类应该同时被复用,即组件中不要依赖不参加复用的类
REP 要求咱们将严密相干的类放在一个组件中一起公布,而 CRP 要求咱们强调的是不要把不相干的类放进来,要用大家就一起用。要留神所谓“独特”复用并不意味着所有的类都能被内部拜访,有些类可能是服务于外部其余类的,但也是必不可少的。咱们尽管不心愿组件适度拆分,然而同时要求组件的类不能适度冗余,不应该呈现他人只须要依赖它的某几个类而不须要其余类的状况。
+ VideoPlay
+ VideoCreation
+ Account
+ ...
比方 VideoCreation
承载了短视频创作相干的性能,短视频创作链路分为拍摄和编辑两局部,某些场景下用户通过相册选取素材后间接进入编辑,此时可能不须要拍摄模块,所以拍摄和编辑两个模块共存一个组件不合乎 CRP 准则,当拍摄局部代码产生变动时,会牵累那些只依赖编辑模块的组件一起参加编译。此时应该思考拆分 VideoCreation
为 VideoRecord
和 VideoEdit
两个 Module
CRP 与 SOLID 的 ISP(接口隔离准则)有点像,ISP 指的是对外不裸露不须要的接口,从这点来看,CRP 能够称为组件版的 ISP。
三准则的衡量
上述三个准则有着互斥关系,REP 和 CCP 是粘合性准则,通知咱们哪些类要放在一起,这会让组件变得更大。CRP 是排除性准则,不须要的类要从组件中移除进来,这会使组件变小。组件设计的重要工作就是在这三个准则之间做出平衡
REP,CCP,CRP 的中心思想都是谋求组件外部正当的内聚性,然而它们的侧重点不同,三者很难同时兼顾,如果考虑不周会落入按下葫芦浮起瓢的困境。如果只恪守 REP、CCP 而疏忽 CRP,就会依赖了太多没有用到的组件和类,而这些组件或类的变动会导致你本人的组件进行太多不必要的公布;恪守 REP、CRP 而疏忽 CCP,因为组件拆分的太细了,一个需要变更可能要改 n 个组件,带来的老本也是微小的,如果只恪守 CCP 和 CRP 而疏忽 REP 可能因为组件的能力太过于垂直而就义了底层能力的可复用性
Gradle Module 粒度如何划分很难找到一个普适的论断,应该综合我的项目类型、我的项目阶段等各种因素,在三准则中做出取舍和衡量。例如在我的项目晚期咱们更加关注业务开发和保护效率,所以 CCP 比 REP 更重要,但随着我的项目的倒退,可能就要思考底层能力的可复用性,REP 变得重要起来,随着我的项目的继续迭代,组件能力越发臃肿,此时须要借助 CRP 对组件进行正当的拆分和重构。
Module 依赖关系
在粒度划分上咱们谋求的是组件如何放弃正当的内聚性,组件间依赖关系的梳理有助于更好地维持内部的耦合。Clean Architecture 中对于组件耦合设计也有三个准则:
- 无环依赖准则(The Acyclic Dependencies Principle)
- 稳固依赖准则(The Stable Dependencies Principle)
- 稳固形象准则(The Stable Abstractions Principle)
无环依赖准则(ADP)
组件依赖关系图中不应该呈现环,关系图应该必须是一个有向无环图(DAG)Module 之间呈现环形依赖会扩充组件变更带来的影响范畴,减少整体编译老本。
比方 A -> B -> C -> A 这样的环形依赖中,因为 C 依赖了 A,B 又依赖了 C,A 的变动对 B,C 都会带来影响,依赖环中的任何一点产生变更都会影响环上的其余节点。构想一下如果没有 C -> A 的依赖,C 的变动只会影响 B,B 只会影响 A,A 的变动将不再影响任何人。
所幸咱们不用放心 Gradle 中呈现环形依赖的 Module。Gradle 须要依据 Module 依赖关系决策编译程序,如果 Module 之间有环存在,Gradle 在编译期会报错揭示,因而咱们须要关怀的是发现环形依赖后该如何解决,打消环形依赖个别有两种办法:
- 依赖倒置
借助 SOLID 中的 依赖倒置准则(DIP),把 C > A 的依赖内容,形象为 C 中的接口,C 面向接口编程,而后让 A 实现这些接口,依赖关系产生反转
- 减少组件
新增 D 组件,C > A 的依赖局部下沉到 D,让 C 和 A 独特依赖 D,相似于中介者设计模式。
当然,这种形式如果滥用会导致工程的组件收缩,所以是否真的要从 A 中下沉一个 D 组件,还要联合前文介绍的 REP 和 CCP 准则综合思考。
稳固依赖准则(SDP)
依赖关系要趋于稳定的方向,例如 A 依赖 B,则被依赖方 B 应该比依赖方 A 更稳固。
SDP 准则很好了解,如果 A 是一个公共组件须要放弃较高稳定度,而它如果依赖一个常常变更的组件 B,则会因为 B 的变更变得不稳固,若要保障 A 的稳固 B 的批改就会变得畏手畏脚,难以进行。一个预期会常常变更的组件是一个不稳固的组件,这个定义过于太主观,如何主观掂量一个组件的稳定性呢?
稳定度公式
稳定度的掂量形式能够看一个组件依赖了多少组件(入向依赖度)和被多少组件所依赖(出向依赖度)这两个指标:
- 入向(Fan-in):依赖这个组件的反向依赖的数量,这个值越大,阐明这个组件的职责越大。
- 出向(Fan-out):这个组件正向依赖的其余组件的数量,这个值越大,阐明这个组件越不独立,天然越不稳固。
- 不稳定度:I(Instability) = Fan-out / (Fan-in+Fan-out)
这个值越小,阐明这个组件越稳固:
- 当 Fan-out == 0 时,这个组件不依赖其余任何组件,然而有其余组件依赖它。此时它的 I = 0,是最稳固的组件,咱们不心愿轻易地改变它,因为它一旦改变了,那么依赖它的其余组件也会受到影响。
- 当 Fan-in == 0 时,这个组件不被其余任何组件依赖,然而会依赖其余组件。此时它的 I = 1,是最不稳固的组件,它所依赖的组件的改变都可能影响到本身,然而它本身能够自在地改变,不对其余组件造成影响
留神:入向、出向有时也被称为反向依赖度(Ca)和正向依赖度(Ce),实质是一回事: en.wikipedia.org/wiki/Softwa…
依然以短视频利用的工程构造为例:
+ app #宿主
+ play #视频播放
+ ui
+ data
+ creation #视频创作
+ ui
+ data
+ common #公共能力
+ db
+ net
+ camera
+ cache
+ infra #根底框架等
infra
的不稳定度
- Fan-in = 5
- Fan-out = 0
- I = 0/(5+0) = 0
不稳定度 0,infra
是一个极为稳固的 Module,它不能擅自改变
app
的不稳定度
- Fan-in = 0
- Fan-out = 3
- I = 3 / (0 + 3) = 1
不稳定度 1,app
是一个极度不稳固的 Module,任何一个 Module 的变动都会影响它。
一个绝对衰弱的工程构造,箭头的走势肯定是合乎 SDP 的准则,从不稳固的组件流向稳固的组件,app 和 infra
在整体构造中合乎这一准则。从 Module 外部来看 :ui
的不稳定度高于 :data
也合乎 UI 侧需要更容易变更的主观显示。
再剖析一下 VideoPlay
和 VideoCreation
这两个 Module 的依赖关系,假如此利用为了激励用户创作在视频播放时减少了创作入口,所以 VideoPlay
对 VideoCreation
产生依赖
- VideoPlay 不稳定度
- Fan-in = 1
- Fan-out = 2
- I = 2 / (1+2) = 0.66
- VideoCreation 不稳定度
- Fan-in = 2
- Fan-out = 5
- I = 5 / (2+5) = 0.7
VideoCreatioin
的不稳定度反而略高于 VideoPlay
,这是与 SDP 准则相违反的。在产品层面为了投合需要咱们让 VideoPlay
间接依赖了 VideoCreation
,然而在工程层面这并非一个好设计,对于解决方案能够参考后文内容。
稳固形象准则(SAP)
一个组件的抽象化水平应该与其稳定性保持一致,越稳固的组件应该越形象。
SOLID 中最外围的当属开闭准则(OCP):代码应该对扩大凋谢,对批改敞开。咱们经常通过面向抽象类 / 接口编程的办法去实际 OCP,即用形象构建框架,用实现扩大细节。形象层蕴含程序的根底协定和顶层设计等,这些代码不应该常常变更,所以应该放在稳固组件(I=0)中,而不稳固组件(I=1)中适宜放那些可能疾速和不便批改的实现局部。
SAP 为组件的稳定性和抽象化水平建设了一种关联,稳固组件须要变更时应该防止批改本人,而是通过其派生类的扩大来实现变更,这就要求稳固组件具备良好的形象能力。而至于抽象类的实现局部,应该从稳固组件中剥离,放到不稳固组件中,这样能够无压力的对其代码进行批改而不用放心影响别人。
形象度公式
- Nc:组件中类的数量
- Na:组件中抽象类和接口的数量
- A:形象水平,A = Na / Nc
A 的取值范畴从 0 到 1,值越大示意组件内的抽象化水平越高,0 代表组件中没有任何抽象类,1 代表组件只有形象没有任何实现。
如上图中,因为 infra
处于极度稳固状态,它应该有与之匹配的抽象化水平。以数据层的能力为例,咱们将 infra
中所有的对于数据层的实现抽离到 Common
,只留下形象接口,其余 Module 的 :data
只依赖依赖稳固的 infra
,而 app
负责全局注入 db
、net
、cache
等具体实现。
此外,因为 VideoCreation
中没有剥离形象和实现,对 VideoCreation 实现的批改可能会毁坏其应有的稳定性。
基于 SAP 准则,咱们新增一个高度抽象化的 creation:api
,它具备高稳定性和高形象度,而 VideoCreation
的稳定性升高,负责同时为 api
提供具体实现。
不稳定度与形象度的关系
组件的不稳定度(I)和形象度(A)关系可见下图:
纵轴为 A 值(数值越大越形象),横轴为 I 值(数值越大越不稳固)。基于 SAP 准则,一个衰弱的组件应该尽量凑近主序列(Main Sequence),咱们用 Distance from the Main Sequence (D)
来评估组件的形象度与稳定性之间的均衡关系:D = abs((A+I) - 1)
。这个值越小,阐明这个组件的形象度与稳定性是越均衡的。位于(A = 1, I = 0)的组件是极度稳固并齐全形象,位于(A = 0,I = 1)的组件是齐全具象且极度不稳固的组件。
苦楚区与无用区
位于坐标左下角的组件因为其稳定性要求高不能被随便批改,然而因为其代码形象度很低又无奈通过扩大进行批改,一旦有降级要求只能批改本身。这种既须要批改又不能批改的矛盾使得这个区域被称为苦楚区(Zone Of Pain)。
比方之前例子中的 Common
局部,如果作为公共模块被间接依赖、须要具备极高的稳定性,然而因为其外部充斥具体实现,当咱们要降级 db
或者 net
等公共库时因为影响范畴太大往往须要对程序进行全面回归测试。所以咱们不不容许 Common
被过多地依赖,升高其稳定性,也升高了其产生变更时的累赘,当产生变更时,只有针对其依赖的 infra
接口实现单元测试即可,防止了回归测试老本。
位于坐标右上角的组件,不稳定度很高意味着没有被其余组件依赖,所以在这部分做任何形象都是无意义的,因而也被称为无用区(Zone Of Useless)。个别这种代码都是历史起因造成的,例如咱们常常看到某个角落里遗留了一些没有被实现的抽象类,像这样的无用代码应该被移除。
一个衰弱的组件应该尽量远离苦楚区和无用区,并尽量凑近主序列。
总结
最终总结之前,再看一下咱们这个短视频利用通过整洁架构优化之后的成果
除了前文叙述过的通过新增 creation:api
,让 VideoPlay
的稳定性和形象度趋于统一以外,咱们还对 camera
的地位做了调整,调整前的 camera
处于 Common
中,但它的批改比拟独立且仅仅被 VideoCreation
所依赖,首先这不合乎 CRP 准则,其次 camera
常常随同 VideoCreation
的需要而降级,也不合乎 CCP 的要求,因而咱们把 camera
从 Common
抽出并挪动到 VideoCreation
最初咱们还新增了一个 creation:common
。因为 VideoCreation
中有多处对 infra
的依赖,尽管 infra
是极度稳固的组件,然而作为内部组件依然不可信赖,一旦 infra
产生变动对 VideoCreation
的稳定性造成影响,咱们新增 creation:common
收敛对 infra
的依赖,晋升 VideoCreation
的稳定性。优化之后各组件的不稳定度如下表:
app
- Fan-in = 0
- Fan-out = 7
- I = 7 / (0 + 7) = 1
VideoCreation
- Fan-in = 1
- Fan-out = 2
- I = 2 / (1 + 2)= 0.66
VideoPaly
- Fan-in = 1
- Fan -out = 1
- I = 1 / (1 + 1) = 0.5
Common
- Fan-in = 3
- Fan-out = 3
- I = 3 / (3 + 3) = 0.5
infra
- Fan-in = 6
- Fan-out = 0
- I = 0 / (0 + 6) = 0
不稳定度(I)逐层递加,形象度也随之逐步递增。文章中的例子非常简略,必定有人会感觉这种水平的优化仅凭直觉就可实现,没必要套用公式。然而理论我的项目往往要简单得多,理解这些公式可能在简单场景中施展疏导作用,防止咱们迷失方向。
最初做一个总结,Gradle Module 作为 Android 工程的组件单元,咱们能够基于整洁架构中对于组件设计的准则对其进行治理:
- 所有且仅有严密相干的类或模块应该放入同一组件
- 因为同样目标须要同时批改的组件应该尽量放到一起
- 组件粒度应该如何划分,须要依据理论状况进行衡量
- 组件之间不应该存在循环依赖,能够通过依赖倒置或者减少组件的形式解决
- 被依赖的组件总是比依赖它的组件更稳固
-
组件的稳定性和形象度应该保持一致,越稳固的组织形象度越高
#### 相干视频举荐:
【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)含音视频_哔哩哔哩_bilibili
【Android 进阶教程】——Framework 面试必问的 Handler 源码解析_哔哩哔哩_bilibili
Android 进阶零碎学习——Gradle 入门与我的项目实战_哔哩哔哩_bilibili
Android 开源架构原理剖析与手写实战——热修复自动化脚本!_哔哩哔哩_bilibili