跳转到内容

模块化架构 (TMA)

模块化架构 (TMA) {#the-modular-architecture-tma}

Section titled “模块化架构 (TMA) {#the-modular-architecture-tma}”

TMA 是一种构建应用的方法,旨在实现可扩展性、优化构建和测试周期,并确保团队遵循良好的实践。其核心思想是通过构建使用清晰简洁的 API 相互连接的独立功能来构建应用。

这些指南介绍了该架构的原则,帮助你识别和组织不同层中的应用功能。如果你决定使用该架构,还提供了技巧、工具和建议。

::: info µFeatures

该架构以前称为 µFeatures。我们已将其重命名为模块化架构 (TMA),以更好地反映其背后的目的和原则。

:::

开发者应该能够快速构建、测试和尝试他们的功能,独立于主应用,同时确保 Xcode 功能(如 UI 预览、代码补全和调试)可靠地工作。

模块代表一个应用功能,由以下五个目标的组合组成(目标指的是 Xcode 目标):

  • 源码: 包含功能源码(Swift、Objective-C、C++、JavaScript…)及其资源(图片、字体、storyboard、xib)。
  • 接口: 这是一个配套目标,包含功能的公共接口和模型。
  • 测试: 包含功能的单元测试和集成测试。
  • 测试支持: 提供可在测试和示例应用中使用的测试数据。它还提供模块类和协议的模拟,供其他功能使用(我们稍后会看到)。
  • 示例: 包含示例应用,开发人员可以使用它在某些条件下尝试该功能(不同的语言、屏幕尺寸、设置)。

我们建议为目标遵循命名约定,你可以借助 Tuist 的 DSL 在项目中强制执行此约定。

目标依赖内容
FeatureFeatureInterface源码和资源
FeatureInterface-公共接口和模型
FeatureTestsFeature, FeatureTesting单元和集成测试
FeatureTestingFeatureInterface测试数据和模拟
FeatureExampleFeatureTesting, Feature示例应用

::: tip UI 预览

Feature 可以将 FeatureTesting 用作开发资源,以允许 UI 预览

:::

::: warning 使用编译器指令而非测试目标

或者,你可以使用编译器指令在为目标编译时将测试数据和模拟包含在 FeatureFeatureInterface 目标中。这样可以简化图,但你最终会编译运行应用时不需要的代码。

:::

清晰简洁的 API {#clear-and-concise-apis}

Section titled “清晰简洁的 API {#clear-and-concise-apis}”

当所有应用源码都存在于同一目标中时,很容易在代码中建立隐式依赖,最终得到众所周知的 spaghetti code。一切都紧密耦合,状态有时不可预测,引入新更改成为噩梦。当我们在独立目标中定义功能时,我们需要将公共 API 作为功能实现的一部分来设计。我们需要决定什么应该是公共的,我们的功能应该如何被消费,什么应该保持私有。我们对如何让功能客户端使用功能有更多控制权,并且可以通过设计安全的 API 来强制执行良好的实践。

分而治之。在小模块中工作可以让你更专注、更独立地测试和尝试功能。此外,开发周期要快得多,因为我们可以有选择性地编译,只编译让我们的功能工作所必需的组件。整个应用的编译只在工作结束时才是必要的,那时我们需要将功能集成到应用中。

使用框架或库可以鼓励跨应用和其他产品(如扩展)重用代码。通过构建模块,重用非常简单。我们可以通过组合现有模块并添加*(必要时)*特定平台的 UI 层来构建 iMessage 扩展、Today 扩展或 watchOS 应用。

当一个模块依赖于另一个模块时,它声明对其接口目标的依赖。这样做的好处有两个方面。它防止一个模块的实现与另一个模块的实现紧密耦合,并加快干净构建,因为它们只需要编译我们功能的实现以及直接和传递依赖的接口。这种方法受到 SwiftRock 的使用接口模块减少 iOS 构建时间想法的启发。

依赖接口要求应用在运行时构建实现图,并将依赖注入到需要它们的目标中。虽然 TMA 对如何做到这一点没有强烈偏好,但我们建议使用依赖注入解决方案或模式,或不增加构建时间接寻址或使用非为此目的设计的平台 API 的解决方案。

构建模块时,你可以为目标选择库和框架以及静态和动态链接。没有 Tuist,这个决定会更复杂,因为你需要手动配置依赖图。然而,由于有 Tuist 项目,这不再是一个问题。

我们建议在开发过程中使用动态库或框架,并使用bundle 访问器将 bundle 访问逻辑与目标的库或框架性质解耦。这对于快速编译时间和确保 SwiftUI 预览可靠工作至关重要。在发布版本中使用静态库或框架以确保应用快速启动。你可以利用动态配置在生成时更改产品类型:

Terminal window
# 你必须从清单中读取变量的值
# 并使用它来更改链接类型
TUIST_PRODUCT_TYPE=static-library tuist generate
// 你可以将此放在清单文件或辅助工具中
// 在实例化目标时使用返回的值
func productType() -> Product {
if case let .string(productType) = Environment.productType {
return productType == "static-library" ? .staticLibrary : .framework
} else {
return .framework
}
}

::: warning 可合并库

苹果试图通过引入可合并库来减轻在静态和动态库之间切换的麻烦。然而,这引入了构建时非确定性,使你的构建不可重现且更难优化,因此我们不建议使用它。

:::

TMA 对你的模块的代码架构和模式没有强烈偏好。然而,我们想分享一些基于我们经验的知识:

  • 充分利用编译器是很好的。 过度依赖编译器可能最终变得低效,并导致某些 Xcode 功能(如预览)不可靠工作。我们建议使用编译器来强制执行良好的实践并及早发现错误,但不要达到使代码更难阅读和维护的程度。
  • 谨慎使用 Swift 宏。 它们可能非常强大,但也可能使代码更难阅读和维护。
  • 拥抱平台和语言,不要抽象它们。 试图提出复杂的抽象层可能最终适得其反。平台和语言足够强大,可以在不需要额外抽象层的情况下构建出色的应用。以良好的编程和设计模式为参考来构建你的功能。