Cohesion(内聚性)是一种度量标准,用于衡量单个模块或类内部元素的紧密程度。根据 Wikipedia,高内聚性是良好设计模块的标志,其中所有方法和字段都围绕一个任务工作。Cohesion 直接影响代码的可维护性,并与 coupling(耦合度)相对立。
要点
Cohesion(内聚性)是一种评估单个类或模块内部方法、字段和属性之间逻辑关联程度的度量标准。高内聚性模块执行一个任务,并且只包含执行该任务所必需的元素。低内聚性模块试图同时做多件事情——方法在意义上关联薄弱。
在面向对象编程的语境中,内聚性与单一职责原则(Single Responsibility Principle,S)密切相关。如果一个类有一个明确的职责,其内聚性通常较高。如果一个类同时处理 UI、业务逻辑和网络操作——内聚性就低,这样的类应该拆分为多个职责更窄的独立类。
理解cohesion有助于开发人员做出重构决策。当您发现一个类中存在不使用该类字段的方法时,这是低内聚性的信号。这种方法要么是多余的,要么是类设计不当。追求高内聚性是在每个代码层级上持续改进架构的工作。
在软件工程中,内聚性分为七个等级,从最差到最好。理解这个等级体系可以客观评估模块质量,并明确重构方向。等级越高,代码越易于维护和理解。
偶然内聚(coincidental)——最差的等级,模块中的元素被随机组合在一起,没有任何逻辑联系。例如:Utilities 类中汇集了日期格式化、发送邮件和计算折扣的方法。不阅读所有方法就无法理解该类,而修改一个方法可能会破坏其他方法,仅仅因为它们位于同一处。
逻辑内聚(logical)——元素执行逻辑上相关但本质不同的任务。包含 parseJSON、parseXML 和 parseCSV 方法的类在逻辑上与“解析”主题相关,但每个方法做的工作完全不同。问题:添加新格式(YAML)时,类会膨胀,其接口也会变得臃肿。
时间内聚(temporal)——元素按照执行时间分组。AppInitializer 类配置数据库、加载配置、初始化分析——这些都在应用启动时发生,但任务本身互不相关。最好将它们拆分为各个职责领域的独立 Initializer。
过程内聚(procedural)——元素按执行顺序组合。“订单处理”模块包含 validateCart、processPayment、sendConfirmation 方法——每个方法严格在前一个之后调用。这比偶然或逻辑内聚好,但还不是理想状态:每个步骤都可以提取到单独的模块。
通信内聚(communicational)——元素处理相同的数据。UserService 类包含 getUser、updateUser、deleteUser 方法,由共同的 User 实体连接。这比过程内聚好得多:类有明确的领域范围。大多数移动项目中的 Repository 类具有通信内聚。
功能内聚(functional)——最高等级,模块中的每个元素都参与执行一个任务。PasswordValidator 类只有一个 validate 方法,用于检查密码长度、字符存在性和复杂度——这是功能内聚的例子。如果这样的类发生变更,那只是因为密码验证规则发生了变化。
实现功能内聚是架构重构的主要目标。每个类应该只有一个变更原因。在移动开发中,功能内聚通过提取独立的 Use Cases、自定义 View、格式化器和验证器来实现。每个这样的类都是一个具有明确职责范围的完整构建块。
Cohesion 和 coupling 是同一质量的两个方面。模块内部内聚性越高,模块之间的耦合度通常越低。设计良好的系统同时追求内部高内聚和外部松耦合。这一规则自 1970 年代以来就被视为软件工程的基本原则。
cohesion-coupling 关系可以看作一种平衡。如果开发人员为了在内聚性上妥协,将多个任务合并到一个类中,相邻模块就会获得更多依赖——它们必须为了不同目的访问这个过载的类,从而增加耦合度。反之,拆分为小型高内聚类减少了模块之间交互点的数量。
在实践中这意味着:当您提取一个具有功能内聚的新类时,同时使其他模块无需了解其实现细节。例如,将 EncryptionManager 提取为具有功能内聚的独立类,为其他模块提供简单的 encrypt/decrypt 接口,而无需理解加密算法的细节。
// 低内聚性——一个类同时处理所有事情
class UserManager {
fun fetchAndSaveUser(id: String) { }
fun parseUserJson(json: String): User { }
fun displayUserName(user: User): String { }
fun validateEmail(email: String): Boolean { }
}
// 高内聚性——每个类解决一个任务
class UserRepository {
fun fetchUser(id: String): User { }
}
class UserJsonParser {
fun parse(json: String): User { }
}
class UserNameFormatter {
fun format(user: User): String { }
}
class EmailValidator {
fun isValid(email: String): Boolean { }
}
示例展示了差异:UserManager 具有逻辑内聚——所有方法都与用户相关,但每个方法做的工作完全不同。重构后,每个类具有功能内聚,耦合度降低,因为其他模块只依赖它们需要的类,而不是整个 UserManager。
LCOM(Lack of Cohesion of Methods)——衡量类内聚性最著名的度量标准。LCOM 计算有多少对方法不使用公共字段。值为 0 表示理想的内聚性(所有方法处理相同字段),高值表示低内聚性。LCOM4(改进版)考虑了通过其他方法的传递性连接。
在Android 开发中,可以通过 Detekt 的 TooManyFunctions 规则获取内聚性度量。具有十多个方法且使用不同字段组的类很可能具有低内聚性。在 iOS 中,SwiftLint 有 file_length 和 function_body_length 规则——间接指标:长文件和方法通常表明低内聚性。
手动评估方法:问自己“这个类会因一个原因还是多个原因而变更?”如果您能列举出一个以上独立原因——该类具有低内聚性。第二个测试:“这个类能否拆分为两个独立的类?”如果可以——立即拆分。在代码审查中定期检查内聚性可以防止上帝类的出现并降低技术债务。
第一步——应用单一职责原则。每个类应有一个明确的职责。如果类中存在不属于其主要任务的方法,请将其提取到单独的类中。IDE 中的 Extract Class 或 Extract Delegate 技术可以自动化此过程。提取后,检查原始类是否更加专注于其职责。
第二步——使用 Facade 模式简化接口。如果一个类提供 20 个方法,而客户端仅使用其中 3-4 个,该类可能具有低内聚性——它提供了过多不同的功能。按主题对方法进行分组,为每组提取单独的类,然后将原始类设为外观或将其删除。
第三步——关注字段组。如果类中存在仅部分方法使用的字段,这是低内聚性的指标。按字段组划分该类。例如,如果类包含 userRepository、networkClient 和 analyticsTracker 字段,但第一组方法仅使用 userRepository,而第二组仅使用 networkClient——这应该是两个不同的类。
第四步——避免创建包含任意 static 方法的“工具”类。Utils 或 Helpers 类中的每个 static 方法都是提取到专业化类的候选者。FormatUtils.dateToString 最好移到 DateFormatter,而 ValidationUtils.isValidEmail 移入 EmailValidator。这提高了每个类的内聚性,并使代码具有自文档性。
常见问题
几乎总是。功能内聚使代码易于理解和预测。然而,走向极端可能导致过度碎片化:当每个操作都创建一个独立类时,架构变得过于复杂。平衡——每个功能使用几个类,每个类都具有功能内聚。
Cohesion 是单个模块或类的内部一致性度量标准。Modularity 是一种架构原则,应用被拆分为物理模块。高内聚性是设计单个类和整个模块时的目标。
Detekt(针对 Android)和 Xcode Analyzer(针对 iOS)会高亮显示方法或字段数量可疑过多的类。IntelliJ IDEA 和 AppCode 具有依赖关系可视化功能——您可以查看关系图并发现低内聚性的类。SonarQube 自动计算 LCOM 度量。
可以。包含 connect、disconnect 和 isConnected 方法的接口具有高内聚性——所有方法都与连接管理相关。包含 connect、parseData 和 renderUI 方法的接口具有低内聚性。接口隔离原则(Interface Segregation — SOLID)要求创建具有高内聚性的专用接口。
提出三个问题:能否用一句话描述该类的用途?所有方法是否都支持该用途?类中是否存在部分方法未使用的字段?如果对任一问题的回答是“否”——内聚性低,应该拆分该类。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。