耦合(Coupling)在移动开发中 — 关键概念、类型及如何降低

作者: IT Sectr 发布日期: 2026-05-13 阅读时间: 9 分钟

耦合(Coupling)是一个度量指标,显示应用程序的一个模块对另一个模块的依赖程度。根据维基百科,低耦合(low coupling)是设计良好的系统的标志,模块可以在不破坏相邻模块的情况下进行修改。管理耦合是架构师在设计移动应用程序时的主要任务之一。

要点

  • 耦合 — 模块之间的依赖程度:高 = 强耦合,低 = 弱耦合
  • 内容耦合 — 最差的类型,当一个模块修改另一个模块的内部数据时
  • 数据耦合 — 最好的类型,当模块仅通过参数交换简单数据时
  • 依赖注入 — 移动开发中降低耦合的主要工具
  • 接口和抽象 — 削弱应用程序各层之间耦合的主要机制

什么是耦合

耦合(Coupling)是一个度量指标,用于确定一个模块或类与另一个模块或类的关联程度。一个模块了解另一个模块的内部结构越多,耦合就越高,系统就越难以修改。在良好设计的架构中,耦合应最小化 — 模块仅通过严格定义的接口进行交互。

耦合有两个方面:传入依赖(afferent — 有多少模块依赖于给定模块)和传出依赖(efferent — 给定模块依赖于多少模块)。对这些度量的分析可以识别架构中的"热点",即一个模块的变化会影响许多其他模块。诸如IntelliJ Dependency Analyzer和Xcode Graph之类的工具可以可视化这些关系。

重要的是要理解,零耦合是不可能的 — 模块必须以某种方式进行交互,否则就不是系统,而是一组孤立的程序。架构师的任务是使耦合变得可管理和透明。理想情况:模块仅通过接口进行交互,仅传递简单数据,而不了解彼此的内部结构。这称为松散耦合(loose coupling)。

耦合类型从弱到强

六种耦合类型构成了从最好到最差的尺度。理解这个尺度有助于评估现有代码并选择重构方向。大多数移动项目具有混合耦合类型,架构师的任务是逐步用弱类型替换强类型。

数据耦合 — 最好的类型

数据耦合(Data coupling)— 模块仅通过方法参数交换简单数据。模块A调用模块B的方法,传递基本类型或简单结构,并接收结果。模块A不知道B内部如何实现。这是最理想的耦合类型:最大限度地减少了变更的影响。

示例:EmailValidator.isValid(email: String): Boolean。消费者类传递一个字符串并接收布尔值,而不了解验证器内部的正则表达式或验证规则。验证逻辑的更改不需要修改消费者 — 耦合最小。数据耦合是应用程序中所有公共接口的目标。

印记耦合 — 可接受但不理想

印记耦合(Stamp coupling)— 模块交换复合对象,但仅使用其中的部分字段。模块A将User对象传递给calculateDiscount方法,该方法仅使用user.status。问题:如果User结构发生变化(添加了必填字段),calculateDiscount模块不会变化,但创建User对象的消费者会变化。

在实践中,印记耦合是不可避免的,如果传递的对象是标准数据模型(Entity),则是可以接受的。当模块仅为一个字段而接收整个对象时,问题就出现了。在这种情况下,最好直接传递具体值(数据耦合)。解决方案 — 分析接收方对字段的使用情况。

控制耦合、外部耦合、公共耦合和内容耦合

控制耦合 — 一个模块向另一个模块传递控制其行为的标志(calculate(useNewAlgorithm: Boolean))。这比印记耦合更糟糕,因为消费者模块必须了解被调用模块的内部工作方式。解决方案:将方法一分为二 — calculateWithNewAlgorithm()和calculateWithLegacyAlgorithm()。

外部耦合 — 模块依赖于外部协议、数据格式或API。所有解析相同JSON或使用相同数据库的模块都具有外部耦合。无法完全避免,但可以隔离:在外部格式和内部模型之间创建一个映射层。公共耦合 — 模块共享公共全局状态。内容耦合 — 最差的类型,当一个模块直接修改另一个模块的内部数据时。

耦合类型级别描述
数据最好通过参数传递简单数据
印记可接受传递对象并部分使用
控制中等通过标志控制行为
外部依赖外部协议/格式
公共非常高共享全局状态
内容不可接受直接修改模块内部数据

耦合尺度从数据(理想)到内容(灾难) — 代码审查的实用工具。如果在项目中看到公共耦合或内容耦合 — 这是重构的优先目标。数据耦合和印记耦合是可接受的,并且存在于每个项目中,但它们的数量应该受到控制。

为什么耦合在移动开发中至关重要

高耦合将开发变成一个缓慢的过程,每次更改都需要检查数十个可能被破坏的模块。在移动开发中,这尤其关键:平台每年更新(Android API Level, iOS SDK),库每季度更新,而业务需求持续不断变化。弱耦合是应对这种变化流而不产生持续回归的唯一方法。

实际示例:一个所有屏幕都直接导入NetworkingManager和DatabaseManager的移动应用程序。在将HTTP客户端从Retrofit替换为Ktor(Android)或从URLSession替换为Alamofire(iOS)时,开发人员必须修改每个屏幕。在低耦合的情况下,只需更改隐藏在NetworkDataSource接口后面的一个实现 — 消费者不会注意到替换。

耦合对单元测试的影响也非常大。高耦合的类(通过构造函数直接创建依赖项)无法隔离测试 — 它会拖带数据库、网络和UI。要测试这样的类,必须启动模拟器并等待集成测试。低耦合的类通过构造函数注入接收依赖项,可以轻松模拟。

kotlin
// 高耦合 — 类自己创建自己的依赖项
class ProfileViewModelHigh {
    private val api = RetrofitApi()
    private val db = RoomDatabase.getInstance()
    private val cache = MemoryCache()
}

// 低耦合 — 依赖项通过构造函数传递
class ProfileViewModelLow(
    private val api: ApiService,
    private val db: DatabaseService,
    private val cache: CacheService
)

在第一种情况下,ProfileViewModelHigh紧密耦合到具体的实现 — 用Ktor替换Retrofit需要更改ViewModel的代码。在第二种情况下,ProfileViewModelLow仅依赖于接口,其实现由外部提供。测试第二个类很简单:传递模拟实现并在没有模拟器的情况下检查逻辑。

降低耦合的模式

依赖倒置原则(SOLID中的D)— 降低耦合的基础。该原则要求依赖抽象而不是具体实现。类不应该直接创建RetrofitApi对象,而应该接收ApiService接口。这将依赖从特定库转移到抽象级别,可以在不修改消费者的情况下进行替换。

观察者模式(或其响应式版本 — StateFlow, Combine Publishers)降低了数据源和订阅者之间的耦合。订阅者不知道数据来自哪里 — 它只对变化做出反应。这分离了发送者和接收者:可以添加新的数据源而无需更改现有的订阅者。EventBus和SharedFlow也基于相同的原理工作。

桥接模式分离了抽象和实现,使它们能够独立变化。在移动开发中,桥接模式用于平台相关模块:具有iOS(Kingfisher, Nuke)和Android(Glide, Coil)不同实现的通用ImageLoader接口。使用ImageLoader的代码不依赖于所选库,可以通过简单的实现更改来替换它。

依赖注入作为耦合管理工具

依赖注入(DI)— 移动开发中降低耦合的最实用工具。类不自己创建依赖项,而是由DI容器(Android的Hilt、Koin、Dagger;iOS的Swinject、Factory)从外部提供。类通过构造函数注入、方法注入或属性注入接收依赖项,而不了解具体的实现。

DI显式记录了类的依赖关系:只需查看构造函数即可了解类与哪些模块交互。如果构造函数接收来自不同层的8个参数 — 这是过度耦合的信号,需要重构。良好的实践是每个类不超过3-4个依赖项。更多的数量表明违反了单一职责原则并存在过度耦合。

DI还简化了测试:对于每个测试,您创建一个带有模拟依赖项的类,而无需真实的数据库或网络。在Flutter中,DI通过Provider、Riverpod或GetIt实现。无论框架如何,目标都是相同的:通过使依赖项显式且可替换来削弱模块之间的耦合。在移动项目中使用DI自2020年代以来已成为事实上的标准。

swift
// DI容器构建依赖关系图
protocol AuthServiceProtocol {
    func login(email: String, password: String) async throws -> User
}

final class AuthService: AuthServiceProtocol {
    func login(email: String, password: String) async throws -> User {
        // 实现
    }
}

// ViewModel不知道具体服务 — 只有协议
final class LoginViewModel {
    private let auth: AuthServiceProtocol

    init(auth: AuthServiceProtocol) {
        self.auth = auth
    }
}

// DI容器 — 创建具体类型的唯一地方
final class DIContainer {
    lazy var authService: AuthServiceProtocol = AuthService()
    lazy var loginViewModel: LoginViewModel {
        LoginViewModel(auth: self.authService)
    }
}

在这里,LoginViewModel仅依赖于AuthServiceProtocol协议,而不是具体的AuthService。替换实现(例如,从Firebase Auth迁移到自己的服务器)只需要在DIContainer中进行更改。AuthServiceProtocol的所有消费者保持不变 — 通过抽象和DI将耦合降至最低。

常见问题

耦合和内聚有什么区别?

内聚衡量模块的内部一致性,而耦合衡量模块之间的外部连接。良好的架构追求高内聚和低耦合。这些度量成反比:提高内聚通常会降低耦合,反之亦然。

哪种耦合类型在生产代码中是可接受的?

数据耦合和印记耦合是正常的,在每个项目中都存在。控制耦合在有限的情况下(例如策略模式)是可接受的。外部耦合在使用外部API时是不可避免的,但应该在映射层后面进行隔离。公共耦合和内容耦合是架构问题的标志,需要立即重构。

如何在项目中测量耦合?

静态分析工具:IntelliJ IDEA Dependency Matrix、Xcode Graph、Gradle Dependencies report、SonarQube。度量指标:传入耦合(Ca)、传出耦合(Ce)、不稳定性(Ce/(Ca+Ce))。高不稳定性(接近1)意味着模块易于更改且很少被引用 — 这很好。

低耦合会有害吗?

极低的耦合可能意味着过多的抽象和接口,使代码导航变得困难。如果每个类都创建了单独的接口,程序员就会在文件之间跳转而浪费时间。平衡:为模块的外部API创建接口,但不要为每个内部辅助类创建。

如何处理遗留代码中的耦合?

使用绞杀者模式(Strangler Fig)技术 — 逐步通过接口替换直接调用。从最常引用的类的提取接口开始。然后引入DI容器。用表征测试覆盖隔离的代码,以确保重构不会改变系统的行为。

总结

  • 耦合 — 模块间依赖的度量:弱耦合是良好架构的目标
  • 数据耦合 — 最好的类型,内容耦合 — 最差的,在生产代码中不可接受
  • 依赖倒置和接口 — 削弱耦合的主要机制
  • 依赖注入 — 使依赖关系显式和可替换的实用工具
  • 高耦合使代码脆弱:一个更改会破坏许多模块
  • 低耦合简化测试:每个模块无需模拟器即可独立模拟
  • 平衡耦合和抽象 — 过多的接口会使代码复杂化

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读