大家好!
不久前,我们意识到移动应用程序不仅是瘦客户端,而且是大量需要简化的非常不同的逻辑。 这就是为什么我们从“干净”架构的思想中得到启发,感觉到DI是什么,学会了如何使用Dagger 2的原因,并且现在我们闭上眼睛就能将任何功能分解为多层。
但是,世界并没有停滞不前,随着老问题的解决,新的问题就出现了。 这个新问题的名称是单模块化。 当装配时间飞入太空时,您通常会发现有关此问题的信息。 恰好有多少关于向多模数转换(
一 ,
二 )的报告开始了。
但是由于某种原因,每个人都在某种程度上忘记了单模量不仅会极大地影响组装时间,而且会严重影响您的体系结构。 在这里回答问题。 您的AppComponent有多大? 您是否在代码中定期看到功能A由于某种原因而拉动了功能B的存储库,尽管看起来似乎不应该如此,还是应该以更高的级别出现? 功能是否有任何合约? 以及您如何组织功能之间的通信? 有什么规定吗?
您认为我们已经通过层解决了问题,也就是说,垂直方向上的一切似乎都很好,但是水平方向上出现了问题? 仅破坏数据包并控制审阅并不能解决问题。
而对于安全性问题的经验更丰富。 当您转向多模块性时,您是否不必铲掉一半的应用程序,总是将代码从一个模块拖到另一个模块中,并在未组装的项目中度过一段不错的时间?
在我的文章中,我想告诉您我是如何从架构的角度精确地实现多模数的。 哪些问题困扰着我,以及我如何逐步解决这些问题。 最后,您将找到一种算法,可以从单模块化切换到多模块化,而不会产生泪水和痛苦。
回答第一个问题,我可以承认AppComponent有多大-非常大。 它不断折磨着我。 怎么发生的? 首先,这是由于这样的DI组织所致。 我们将开始使用DI。
就像我之前做过DI
我认为许多人已经在他们的脑海中形成了类似于组件依赖关系和相应范围的图表:

我们这里有什么
AppComponent ,绝对吸收了
Singleton范围的所有依赖项。 我认为几乎每个人都有这个组成部分。
FeatureComponents 。 每个功能都有其自己的范围,并且是
AppComponent的子组件或高级功能。
让我们稍微介绍一下功能。 首先,功能是什么? 我会用自己的语言尝试。
功能是逻辑上完整的,最大程度独立的程序模块,具有明确定义的外部依存关系,可以解决特定的用户问题,并且相对容易在另一个程序中再次使用。 功能可以大可小。 功能可能包含其他功能。 他们还可以通过明确定义的外部依赖项来使用或运行其他功能。 如果我们使用我们的应用程序(适用于Android的Kaspersky Internet Security),则可以将其视为防病毒,防盗等功能。
ScreenComponents 。 特定屏幕的组件,也具有自己的范围,并且也是相应功能组件的子组件。
现在列出“为什么这样”
为什么使用子组件?在组件依赖关系中,我不喜欢一个组件可以同时依赖多个组件这一事实,在我看来,这最终可能导致组件及其依赖关系的混乱。 如果您具有严格的一对多关系(一个组件及其子组件),那么它会更安全,更明显。 另外,默认情况下,父组件的所有依赖项都可用于子组件,这也更加方便。
为什么每个功能都有范围?因为从那时起,我开始考虑每个功能都是其自己的生命周期,与其他功能并不相同,因此创建自己的作用域是合乎逻辑的。 还有许多要点,我将在下面提到。
由于我们是在Clean的背景下谈论Dagger 2的,所以我还将提到依赖项交付的那一刻。 演示者,交互器,存储库和其他辅助依赖项类是通过构造函数提供的。 在测试中,然后通过构造函数替换存根或moki并静默测试我们的类。
依赖关系图的关闭通常发生在活动,片段,有时甚至是接收者和服务中,通常发生在android可以从其开始的根位置。 典型的情况是,为某个功能创建活动时,该功能组件启动并存在于该活动中,并且在功能本身中,存在三个屏幕,这些屏幕分为三个片段。
因此,一切似乎都是合乎逻辑的。 但是,一如既往,生活会做出自己的调整。
生活问题
示例任务
让我们看一下应用程序中的一个简单示例。 我们具有扫描仪功能和防盗功能。 这两个功能都有一个珍贵的购买按钮。 此外,“购买”不仅是发送请求,而且还包含与购买过程相关的许多不同逻辑。 这纯粹是业务逻辑,带有一些立即购买的对话框。 也就是说,它本身具有一个单独的功能-购买。 因此,在两个功能中,我们需要使用第三个功能。
从ui和导航的角度来看,我们有以下图片。 主屏幕开始,其上有两个按钮:

通过单击这些按钮,我们可以使用扫描仪或防盗功能。
考虑扫描仪的功能:

通过单击“开始防病毒扫描”,完成了某种扫描工作,通过单击我们只想购买的“购买我”,即,我们具有“购买”功能,但是在“帮助”上,我们会显示一个简单的带有帮助的屏幕。
防盗功能看起来几乎相同。
潜在的解决方案
我们如何用DI来实现此示例? 有几种选择。
第一选择
选择购买功能作为仅依赖于
AppComponent的
独立组件 。

但是,然后我们面临一个问题:如何将来自两个不同图形(组件)的依赖项一次注入到一个类中? 只有通过肮脏的拐杖,这是当然的事情。
第二选择
我们在子组件中选择购买功能,具体取决于AppComponent。 扫描仪和防盗组件可以从购买组件中成为子组件。

但是,正如您所了解的,应用程序中可能有很多类似的情况。 这意味着组件依赖关系的深度确实是巨大而复杂的。 这样的图形比使您的应用程序更连贯和易于理解更令人困惑。
第三选择
我们
不是在单独的组件中而是在单独的Dagger模块中选择购买功能。 还有两种方法是可能的。
第一种方式让我们将
Singleton范围功能添加到所有依赖项并连接到
AppComponent 。

该选项很流行,但会导致
AppComponent膨胀。 结果,它的大小膨胀了,包含了所有应用程序类,并且使用Dagger的全部目的在于更方便地将依赖项传递给类-通过字段或构造函数,而不是通过单调。 原则上,这是DI,但是我们错过了体系结构要点,事实证明每个人都知道每个人。
通常,在路径的开头,如果您不知道将类归于哪个功能的位置,那么将其全局设置会更容易。 当您与Legacy合作并尝试引入至少某种架构时,这很常见,而且您还不了解所有代码。 的确,那里睁大了眼睛,这些行动是合理的。 错误是,当一切或多或少迫在眉睫时,没有人愿意
解决这个
AppComponent 。
第二种方式这是将所有功能简化为单个范围的功能,例如
PerFeature 。

然后,我们可以轻松简单地将Shopping的Dagger模块连接到必要的组件。
好像很方便 但是从结构上看,结果并不是孤立的。 扫描程序和防盗功能绝对了解有关购买功能的所有信息,包括其所有杂碎。 无意间,可能会涉及到某些事情。 也就是说,“购买”功能没有清晰的API,功能之间的边界模糊,没有清晰的合同。 不好 好了,在多模数中,稍后将很难实现。
建筑上的痛苦
老实说,很长一段时间我一直使用
第三个方法 。 当我们开始逐步将我们的遗产转移到正常轨道时,这是一项必要措施。 但是,正如我提到的那样,通过这种方法,您的功能开始有些混乱。 每个人都可以了解每个细节,以及实施细节以及每个人的细节。
AppComponent的膨胀明显表明需要做些事情。
顺便说一句,
第三个选项将有助于
AppComponent的卸载。 但是,有关实现和混合功能的知识将无处可寻。 好吧,当然,在应用程序之间重用功能将非常困难。
中间结论
那么,我们到底想要什么? 我们要解决什么问题? 让我们直接讲到这一点,从DI开始,然后转到架构:
- 一种便捷的DI机制,使您可以使用其他功能中的功能(在我们的示例中,我们希望使用Scanner和Anti-Theft中的购物功能)而不会感到烦恼和痛苦。
- 最薄的AppComponent。
- 功能部件不应了解其他功能部件的实现。
- 默认情况下,任何人都不应访问功能,我想拥有某种严格的控制机制。
- 可以用最少的手势将功能提供给另一个应用程序。
- 向多模块性的逻辑过渡,以及此过渡的最佳实践。
我仅在最后专门谈到了多模块性。 我们会到达她的身边,我们不会超越自己。
“以新的方式生活”
现在,我们将尝试逐步实现上述愿望清单。
走吧
DI增强
让我们从相同的DI开始。
拒绝大量范围
正如我上面所写,在我采用这种方法之前,是:对于每个功能,都有自己的范围。 实际上,没有任何特别的收益。 仅获得大量的范围和一定程度的头痛。
这条链就足够了:
Singleton -
PerFeature -
PerScreen 。
放弃子组件以支持组件依赖性
已经很有趣了。 有了
子组件,您的看似更加严格的层次结构,但与此同时,您却束手无策,至少没有办法进行调整。 此外,
AppComponent知道所有功能,并且您还会获得一个庞大的
DaggerAppComponent类。
有了
组件依赖性,您将获得一项超酷的优势。 在组件依赖关系中,您可以指定的
不是接口组件,而是干净的接口 (由于Denis和Volodya)。 因此,您可以替换任何您喜欢的接口实现,Dagger会吃掉所有东西。 即使具有相同范围的组件是此实现:
@Component( dependencies = FeatureDependencies.class, modules = FeatureModule.class ) @PerFeature public abstract class FeatureComponent {
从DI增强到体系结构增强
让我们重复功能的定义。
功能是逻辑上完整的,最大程度独立的程序模块,具有明确定义的外部依存关系,可以解决特定的用户问题,并且相对容易在另一个程序中重用。 功能定义中的关键表达之一是“具有明确定义的外部依存关系”。 因此,让我们描述一下外界想要的所有功能,我们将在一个特殊的界面中进行描述。
假设购物功能的外部依赖项界面如下:
public interface PurchaseFeatureDependencies { HttpClientApi httpClient(); }
或“扫描程序”功能的外部依赖项接口:
public interface ScannerFeatureDependencies { DbClientApi dbClient(); HttpClientApi httpClient(); SomeUtils someUtils();
正如在DI部分中已经提到的那样,依赖关系可以由任何人实现,并且您可以随意将它们作为纯接口,而我们的功能也不再需要这些额外的知识。
“纯”功能的另一个重要组成部分是存在一个清晰的api,外界可以通过它访问该功能。
以下是购物的api功能:
public interface PurchaseFeatureApi { PurchaseInteractor purchaseInteractor(); }
也就是说,外界可以获取
PurchaseInteractor并尝试通过它进行购买。 实际上,在上面我们已经看到扫描仪需要
PurchaseInteractor来完成购买。
这是扫描仪的api功能:
public interface ScannerFeatureApi { ScannerStarter scannerStarter(); }
然后我立即带来
ScannerStarter的界面和实现:
public interface ScannerStarter { void start(Context context); } @PerFeature public class ScannerStarterImpl implements ScannerStarter { @Inject public ScannerStarterImpl() { } @Override public void start(Context context) { Class<?> cls = ScannerActivity.class; Intent intent = new Intent(context, cls); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } }
这里更有趣。 事实是,扫描仪和防盗器是相当封闭和孤立的功能。 在我的示例中,这些功能是在单独的“活动”上启动的,并具有自己的导航等。也就是说,我们只需在此处启动“活动”就足够了。 活动消亡-功能消亡。 您可以按照“单一活动”的原则进行工作,然后通过api功能传递一个FragmentManager和一些回调,该功能通过该回调报告功能已完成。 有很多变化。
我们也可以说,我们有权将扫描仪和防盗功能等视为独立的应用程序。 与购买功能不同,购买功能是对某物本身的附加功能,它在某种程度上并不特别存在。 是的,它是独立的,但这是对其他功能的逻辑补充。
可以想象,必须有一些点将功能,其实现和依赖项的必要功能连接起来。 这就是Dagger组件。
扫描仪功能部件的示例: @Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class
我认为您没有新意。
过渡到多模块
因此,您和我能够通过依赖项的api和外部api清楚地定义功能的边界。 我们还想出了如何在Dagger中启动它。 现在,我们进入下一步的逻辑和有趣的步骤-分成模块。
立即打开
测试用例 -会更容易。
让我们大致看一下图片:

并查看示例的包结构:

现在,让我们仔细讨论每个项目。
首先,我们看到四个大块:
Application ,
API ,
Impl和
Utils 。 在
API ,
Impl和
Utils中,您可能会注意到所有模块都
以core-或
feature-开头。 让我们先谈谈它们。
分为核心和功能
我将所有模块分为两类:
core-和
feature- 。
您可能已经猜到在
feature-中 ,我们的功能。 在
核心中,有诸如实用程序,与网络,数据库等协同工作的东西。但是那里没有功能接口。
核心不是一个整体。 我将
核心模块分解为逻辑部分,并避免将其与其他功能接口一起加载。
首先以模块的名称编写
core或
feature 。 模块名称中进一步是逻辑名称(
扫描仪 ,
网络等)。
现在大约有四个大块:应用程序,API,Impl和Utils
API每个
功能或
核心模块均分为
API和
Impl 。
API包含一个外部api,您可以通过它访问功能或核心。 仅此而已:

此外,
api模块对任何人一无所知,这是一个绝对孤立的模块。
实用程序可以将上述规则的唯一例外视为完全功利主义的事物,将其分解为api和实现毫无意义。
Impl在这里,我们细分为
core-impl和
feature-impl 。
core-impl中的模块也完全独立。 它们唯一的依赖性是
api模块 。 例如,看一看
core-db-impl模块的
build.gradle :
// bla-bla-bla dependencies { implementation project(':core-db-api') // bla-bla-bla }
现在介绍
feature-impl 。 应用程序逻辑已经占有很大的份额。
feature-impl组的模块可以知道
API或
Utils组的模块,但是他们当然对
Impl组的其他模块一无所知。
我们记得,功能的所有外部依赖关系都累积在外部依赖关系中。 例如,对于扫描功能,此api如下所示:
public interface ScannerFeatureDependencies {
因此,
build.gradle feature-scanner-impl将如下所示:
// bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla }
您可能会问,为什么外部依赖项的api不在api模块中? 事实是,这是实施的细节。 也就是说,这是一个需要某些特定依赖项的特定实现。 对于依赖关系api扫描器在这里:
小型建筑撤退让我们总结以上所有内容,并
亲自了解有关
功能-...- impl-modules及其对其他模块的依赖关系的一些体系结构要点。
我遇到了两种最受欢迎的模块依赖映射模式:
- 一个模块可以知道任何人。 没有规则。 没有什么可评论的。
- 模块只知道核心模块 。 并且在核心模块中 ,所有功能的所有接口都被集中。 这种方法对我来说不是很吸引人,因为存在将核心变成另一个垃圾场的风险。 另外,如果要将模块转移到另一个应用程序,则需要将这些接口复制到另一个应用程序中,并将其放置在core中 。 当可以更新接口时,接口本身及其自身的愚蠢复制粘贴在将来不会很吸引人并且不可重用。
在我们的示例中,我主张了解api和仅api(以及utils-groups)模块的知识。 功能对实现一无所知。
但是事实证明,功能可以了解其他功能(当然,通过api)并运行它们。 可能是一团糟吗?
公平的言论。 很难制定出一些非常明确的规则。 一切都应该有措施。 我们已经在上面稍微提到了这个问题,将功能分为独立的功能(扫描仪和防盗)-完全独立且独立,并且功能“在上下文中”,也就是说,它们始终作为事物的一部分(购买)启动,通常暗示着业务逻辑没有ui。 这就是为什么Scanner和Anti-Theft知道购买的原因。
另一个例子。 想象一下,在防盗中有擦除数据之类的事情,也就是说,绝对清除了手机中的所有数据。 ui有很多业务逻辑,它是完全隔离的。 因此,将擦除数据分配为单独的功能是合乎逻辑的。 然后是叉子。 如果擦除数据始终仅从防盗启动并且始终存在于防盗中,则逻辑上防盗将知道擦除数据并自行运行。 然后,累积模块app将仅了解反盗窃。 但是,如果擦除数据可以从其他地方开始或者在防盗中并不总是存在(也就是说,在不同的应用程序中可能有所不同),那么合乎逻辑的是,防盗不知道此功能,而只是说一些外部信息(通过路由器,通过某种回调,这没关系)用户按下了这样的按钮,在该按钮下启动的功能已经是消费者的防盗功能(特定应用程序,特定应用程序)。
关于将功能转移到另一个应用程序,还有一个有趣的问题。 例如,如果我们要将扫描仪传输到另一个应用程序,那么除了模块
:feature-scanner-api和
:feature-scanner-impl以及扫描仪所依赖的模块(
:core-utils ,: core-network- api:core-db-api ,: feature-purchase-api )。
是的,但是! 首先,您所有的api模块都是完全独立的,并且只有接口和数据模型。 没有逻辑。 这些模块在逻辑上显然是分开的,并且
:core-utils通常是所有应用程序的通用模块。
其次,您可以收集aar形式的api模块并将它们通过maven传递到另一个应用程序,也可以以gig子模块的形式连接它们。 但是您将拥有版本控制,控制权,完整性。
因此,在另一个应用程序中模块(更确切地说是实现模块)的重用看起来更加简单,清晰和安全。
申请书
似乎我们对功能,模块,它们的依赖项有了苗条且易于理解的画面,仅此而已。 现在我们进入了一个高潮-这是api及其实现的组合,替换了所有必要的依赖项等,但是从Gredloi模块的角度来看。 连接点通常是
应用程序本身。
顺便说一下,在我们的示例中,这一点仍然是
feature-scanner-example 。 通过上述方法,您可以将每个功能作为单独的应用程序运行,从而大大节省了活动开发期间的构建时间。 美女!
首先,让我们考虑一下已经流行的Scanner示例如何通过
应用程序进行所有操作。
快速回顾该功能:Sci外部依赖项api是:
public interface ScannerFeatureDependencies {
因此
:feature-scanner-impl取决于以下模块:
// bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla }
基于此,我们可以创建一个实现外部依赖关系api的Dagger组件:
@Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { }
为了方便起见,我将此接口放在
ScannerFeatureComponent中:
@Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class }, dependencies = ScannerFeatureDependencies.class) @PerFeature public abstract class ScannerFeatureComponent implements ScannerFeatureApi { // bla-bla-bla @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } }
现在是应用程序。 应用知道所需的所有模块(
core-,feature-,api,impl ):
// bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-db-api') implementation project(':core-db-impl') implementation project(':core-network-api') implementation project(':core-network-impl') implementation project(':feature-scanner-api') implementation project(':feature-scanner-impl') implementation project(':feature-antitheft-api') implementation project(':feature-antitheft-impl') implementation project(':feature-purchase-api') implementation project(':feature-purchase-impl') // bla-bla-bla }
接下来,创建一个助手类。 例如,
FeatureProxyInjector 。 这将有助于正确初始化所有组件,通过此类,我们将转向功能。 让我们看看如何初始化扫描仪功能部件:
public class FeatureProxyInjector {
在外部,我们提供了功能接口(
ScannerFeatureApi ),在内部,我们只是初始化了整个实现依赖图(通过
ScannerFeatureComponent.initAndGet(...)方法)。
DaggerPurchaseComponent_PurchaseFeatureDependenciesComponent是Dagger生成的
PurchaseFeatureDependenciesComponent的实现,我们在上面已经谈到过,在该
示例中,我们在构建器中替换了api模块的实现。
太神奇了。 再次参见
示例 。
说起
例子 。 在这个
例子中,我们还必须满足所有的外部
依赖:功能扫描仪实现了一套。 但是,由于这是一个示例,因此我们可以替换伪类。
外观如何:
并且
示例中的扫描程序功能本身
是通过清单启动的,以免阻止其他空活动:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.scanner_example"> <application android:name=".ScannerExampleApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.example.scanner.presentation.view.ScannerActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
从单模转换到多模的算法
生活是一件艰苦的事情。 现实情况是,我们所有人都与Legacy合作。 如果有人现在看到一个全新的项目,您可以在其中立即祝福一切,那么我羡慕您,兄弟。 但这不是我的情况,那个家伙也是错误的=)。
如何将您的应用程序转换成多模块? 我主要听到两种选择。
第一个 在这里和现在对应用程序进行分区。 没错,您的项目可能在一两个月内无法组装=)。
第二个。 尝试逐渐拉出功能。 但是,与此同时,这些功能的各种依赖性也在扩展。 从这里开始乐趣。 依赖项代码可以拉出另一个代码,整个过程将迁移到
通用模块 ,
核心模块 ,反之亦然,依此类推。 结果,拉动一个功能可能需要与应用程序的另一半一起工作。 同样,在开始时,您的项目将不会花费可观的时间。
我主张将应用程序逐步转移到多模块性,因为与此同时,我们仍然需要看到新功能。 关键思想是,
如果您的模块需要某些依赖项,则也不应立即将这些代码也物理地拖到模块中 。 让我们以扫描仪为例看一下模块拆卸算法:
- 创建api功能,将其放入新的api模块中。 也就是说,要完全创建一个具有所有接口的模块:feature-scanner-api 。
- 创建:feature-scanner-impl 。 将与功能相关的所有代码物理传输到此模块。 您的功能所依赖的所有内容,工作室都会立即突出显示。
- 识别外部功能依赖性。 创建适当的接口。 这些接口分为逻辑api模块。 也就是说,在我们的示例中,使用相应的接口创建模块:core-utils ,: core-network-api ,: core-db-api ,: feature-purchase-api 。
我建议您立即投资这些模块的名称和含义。 显然,随着时间的流逝,接口和模块可能会稍微改组,折叠等,这是正常的。 - 创建外部依赖项( ScannerFeatureDependencies )的api。 取决于:feature-scanner-impl注册新创建的api模块。
- 由于我们在应用程序中拥有所有遗留物,因此我们将进行以下操作。 在应用程序中,我们连接为功能创建的所有模块(功能api模块,功能隐含模块,功能外部依赖项api模块)。
超重要的一点 。 接下来,在应用程序中,我们创建所有必需的功能依赖项接口的实现(本例中为Scanner)。 这些实现可能只是从您的api依赖项到项目中这些依赖项的当前实现的代理。 初始化功能部件时,请替换实现数据。
言语困难,想举个例子吗? 所以他已经是! 实际上,feature-scanner-example中已经存在类似的东西。 再一次,我将给它一个稍微修改的代码:
也就是说,这里的主要信息是这个。 像执行此操作一样,让该功能所需的所有外部代码都存在于应用程序中。 并且该功能本身已经可以通过api(即api依赖项和api-modules)以正常方式使用。 将来,实施将逐步移至模块。 但是,如果将功能所需的外部代码从模块拖到模块中,我们将避免无休止的游戏。 我们可以进行清晰的迭代! - 获利
这是一个简单但有效的算法,可让您逐步实现目标。
其他提示
功能应该有多大/小?这完全取决于项目,等等。 但是,在向多模块过渡的开始,我建议将其拆分为大块。 此外,如有必要,您将从这些模块中选择更多模块。 但是不要磨。
不要这样做:一个/几个类=一个模块。应用程序模块的纯度当切换到多模块应用程序时,我们将有很多东西,从那里,您突出显示的功能会抽搐。在工作过程中,您可能必须对此遗产进行更改,在此完成某件事,或者您只是发布了一个版本,而您又无法适应模块的要求。在这种情况下,您希望该应用(以及所有旧版)仅通过api了解突出显示的功能,而无需了解实现。但是,该应用程序实际上结合了api 模块和impl-modules,因此该应用程序了解所有人。在这种情况下,您可以创建一个特殊的模块:adapter,这只是api和impl的连接点,然后该应用将仅了解api。我认为这个主意很明确。您可以在clean_app分支中看到一个示例。我将使用Moxy或更确切地说是MoxyReflector来补充一点,在拆分为模块时会出现一些问题,因此我不得不创建另一个附加模块:stub-moxy-java。一小撮魔法,没有它。唯一的修正。仅当功能和相关依存关系已物理转移到其他模块时,此方法才有效。如果您创建了一个功能,但是依赖项仍然存在于应用程序中(如上述算法中那样),则此功能将无效。后记
文章篇幅很大。但是,我希望它确实可以帮助您与单模块性作斗争,了解其应有的方式以及如何与DI交朋友。如果您有兴趣陷入构建速度问题,如何衡量所有问题,那么我建议您对Denis Neklyudov和Zhenya Suvorov的报道(Mobius 2018 Piter,视频尚未公开)。关于Gradle。在gradle这个API和实现之间的差别完全显示Vova高木。如果要减少多模块样板,可以从本文开始。我将很乐意评论,更正以及喜欢!所有干净的代码!