耦合(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。要测试这样的类,必须启动模拟器并等待集成测试。低耦合的类通过构造函数注入接收依赖项,可以轻松模拟。
// 高耦合 — 类自己创建自己的依赖项
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年代以来已成为事实上的标准。
// 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应用程序。我们将为您提供咨询并提出最佳解决方案。