2026/10/12 21:42:32

耦合性与内聚性:用代码示例拆解软件设计的核心指标

耦合性与内聚性:用代码示例拆解软件设计的核心指标 写架构文章这件事我最怕看到那种通篇讲“高内聚低耦合是良好设计的目标”却一个实际代码都不给的空话。今天我把耦合性和内聚性这两个最基础也最容易被忽略的指标结合几个能直接跑的小例子拆开讲它们到底是什么意思、烂代码长什么样、我平时重构的时候具体会怎么动手。这篇文章适合刚入行的开发也适合正在做系统重构的负责人看完你至少能少踩几个我之前踩过的坑。1. 先弄清楚耦合和内聚到底衡量的是什么1.1 它不是考试名词而是一把衡量代码质量的尺子耦合性描述的是模块与模块之间的依赖程度内聚性描述的是模块内部各元素之间关联的紧密程度。两者看起来是两个方向实际上是一枚硬币的两面。模块内部东西越相关对外暴露的依赖越少整体设计就越稳。我经常用一个比喻一个公司有研发部和市场部每个部门内部各司其职部门之间只通过固定的会议和文档对接这就是高内聚低耦合。反过来如果市场部需要天天跑到研发部看代码才能知道项目进度研发部还得派人去市场部帮忙改PPT这就是严重耦合。放到代码里一个方法只负责“创建订单并把它存起来”内部逻辑清晰自然这就是高内聚。而创建订单时还要顺手去更新用户积分、调物流接口、发短信通知把不相关的逻辑全揉进一个函数这就是低内聚和高耦合同时爆发的典型案例。1.2 高耦合低内聚的代码通常有哪些症状这类代码有一个非常明显的特征改一行代码牵动一片逻辑。比如你在订单模块里改了一个字段名结果用户模块、报表模块、营销模块全都报错。修完一个bug另一个原本能跑的功能又挂了。第二个特征是类普遍万能化。我见过一个叫OrderService的类从数据库读取、订单状态计算、客户验证到发邮件全在里面。代码量有四千多行任何人都能往里面加函数但没人敢重构因为不知道改了哪里会炸。第三个特征是测试特别难写。你想测一个函数结果它需要连接真实数据库、依赖外部系统返回数据、还要发送邮件。单测跑起来像做集成测试成本高到团队直接放弃写测试。这些症状的本质都是因为我们在写第一版代码时没有考虑过哪些逻辑属于模块内部、哪些逻辑应该通过接口对外交互。等到业务压力一来所有人都在同一个类里堆代码耦合度和混乱度同步上升。2. 用一份订单代码演示什么叫“牵一发动全身”2.1 先写一个典型的反面教材我用一个最常见的业务场景——用户下单——来演示。下面这段代码我给它起名糟糕版订单创建你在很多项目里都能看到它的影子def create_order(user_id, product_id, quantity): db connect_db() user db.query(SELECT * FROM users WHERE id ?, user_id) if user[status] ! active: raise Exception(用户不可用) product db.query(SELECT * FROM products WHERE id ?, product_id) stock product[stock] - quantity if stock 0: raise Exception(库存不足) db.execute(UPDATE products SET stock ? WHERE id ?, stock, product_id) order_id db.execute( INSERT INTO orders (user_id, product_id, quantity) VALUES (?, ?, ?), user_id, product_id, quantity ) order_amount product[price] * quantity db.execute(UPDATE users SET balance balance - ? WHERE id ?, order_amount, user_id) send_sms(user[phone], f您的订单号为{order_id}) send_email(user[email], f您购买了{product_id}) response requests.post(http://inventory-service/api/deduct, json{order_id: order_id}) if response.status_code ! 200: log_error(response.text) return order_id这段代码的问题太多了。首先是函数内部直接操作数据库外部API调用、短信和邮件发送全部混杂在一个方法里任何一个环节出错整个下单流程都要受影响。其次是对内部实现细节的假设太多比如数据库中每条用户记录必须有什么字段、send_sms内部什么逻辑、库存服务API返回什么格式所有这些细节都硬编码在同一个函数里。2.2 耦合性的几种常见形态对号入座我把耦合按照严重程度从高到低列了一个表每一档都给一个常见代码特征耦合类型严重程度代码特征改造难度内容耦合最高直接访问另一个类的内部字段或私有方法极高公共耦合很高多个模块共享同一个全局变量或数据库连接高控制耦合较高通过参数传入flag函数内部根据flag走不同逻辑中数据耦合中可接受模块之间传递基本类型参数但参数很多时隐患很大中消息耦合低只传递消息不关心对方内部实现低内容耦合最典型的就是A对象直接改B对象的内部字段。比如在Python里直接访问obj._private_field或者在Java里跨类操作友元变量。这种代码一出现就说明两个类的关系已经绑定到无法分离。控制耦合也很常见。我见过一个类似process_order(order, is_domestic)的方法里面诸多的if分支分别去处理国内和国际订单的逻辑。你以为传一个布尔值很方便实际上等于把两个完全不同的业务流程塞进了同一个方法调用方需要知道这个布尔值怎么传才正确接收方也必须维护两条业务线。公共耦合的例子更多全局配置文件被十几个模块直接引用共享数据库连接池给所有业务模块用。改一个全局配置影响范围完全不可控。2.3 解耦实操把责任拆分到该去的地方现在给糟糕版本做第一次重构。核心原则只有一个让每个模块只依赖接口或抽象而不是依赖具体实现。具体做法如下class OrderRepository: def create(self, user_id, product_id, quantity) - int: pass def deduct_stock(self, product_id, stock) - bool: pass def get_product(self, product_id): pass class UserRepository: def get_by_id(self, user_id): pass def deduct_balance(self, user_id, amount): pass class SmsService: def send_order_message(self, phone, order_id): pass class InventoryClient: def deduct(self, order_id): pass def create_order(user_id, product_id, quantity, order_repo: OrderRepository, user_repo: UserRepository, sms: SmsService, inventory: InventoryClient) - int: user user_repo.get_by_id(user_id) product order_repo.get_product(product_id) order_repo.deduct_stock(product_id, quantity) order_id order_repo.create(user_id, product_id, quantity) amount product.price * quantity user_repo.deduct_balance(user_id, amount) sms.send_order_message(user.phone, order_id) inventory.deduct(order_id) return order_id重构完的create_order只做一件事编排各个模块的调用。它不需要关心用户数据是怎么从数据库查出来的也不需要关心库存服务是HTTP调用还是RPC调用。将来把短信改成站内信你只需要替换传入的SmsService实现create_order一行都不用改。还有一个额外的好处是单元测试终于能写了。只要mock掉OrderRepository和UserRepository就能专心验证订单创建流程本身而不是每次测试都要连接数据库。3. 内聚性一个类到底该干多少活才算“正正好”3.1 低内聚的两类大杂烩类低内聚类最典型的特征是“类名模糊职能多样”。OrderServiceBizServiceUtils这样的名字本身就是危险信号。名字太泛说明你心里也没想清楚它到底负责什么。我看过一个Utils类里面同时有字符串转驼峰、时间戳格式化、价格汇率换算、短信模板拼接、Token解析函数。十几个函数之间没有任何关系只是没人愿意新建文件把函数挪出去最后全堆在一起。把它叫“大杂烩”都是客气的。另一类是God Object上帝类。业务类被不断扩展慢慢知道所有事情。比如某个PayService既能处理支付宝回调又能做微信支付对账还负责生成支付二维码甚至顺便维护了商户结算单。每次增加一种支付渠道它都要改一遍测试也越来越难写。3.2 内聚性的七个层级实用看三层教科书会把内聚性从高到低分为七层功能内聚、顺序内聚、通信内聚、过程内聚、时间内聚、逻辑内聚、偶然内聚。实际工程里我一般只看三层功能内聚理想态模块内部所有元素共同完成一个单一功能完成之后对外就没有多余影响了。比如一个generatePaymentQrCode(payment_id)方法内部生成字符串、调接口、返回二维码数据职责明确。顺序内聚可接受模块内部元素按步骤执行前一步的输出是后一步的输入。比如一个submitInvoice方法先把数据校验再组装参数最后提交到税务系统。顺序内聚通常是一个方法内部不可避免的状态流转只要它仍然在描述同一业务场景问题不大。偶然内聚应避免模块内部元素只是因为碰巧放在同一个文件里而聚集在一起相互之间毫无关联。比如上面说的Utils类就是典型。判断一个类内聚性高不高有个最简单的自测方法你尝试用一个不超过五个字的句子说明这个类是干什么的。如果你说出来的是“这个类负责订单的创建、用户余额的扣减、短信发送还有库存扣减”那就是一个低内聚类。如果你能说“这个类负责创建订单”那就是合格的内聚。3.3 拆分类的正向手法按“改变原因”拆分真正要动手拆分的时候我遵循的不是按功能命名而是按变化的频率和原因归类。比如在OrderService里订单状态流转的规则属于领域逻辑可能因为业务规则变化而变动通知用户的手段属于外部集成可能因为供应商更换而变动订单数据的存取属于持久化细节可能因为存储方案调整而变动。这三类变化的频率、原因完全不同把他们绑在同一个类里任何一项变动都要重新测试整个类。拆成三个类之后每项变动只影响自己的类和依赖方。class OrderStatusService: def can_transit(self, current_status, event) - bool: ... class OrderNotificationService: def notify(self, order_id, event, channel_config): ... class OrderDataMapper: def save(self, order): ...这一步做完你可能觉得代码文件变多了一点心里有点慌。实际上这是正常的一个类从四千行拆成四个一百行的类阅读理解成本会急剧下降。我一直认为代码可读性好不好取决于读者需要同时多少上下文才能看懂一个方法。拆分之后每个方法只需要很少上下文开发和评审速度都会提升。4. 高内聚低耦合到底怎么落地不是靠口号4.1 平时写代码把这五条当红线这几条红线是我自己在代码评审里反复用的第一新增依赖之前问自己“这个类真的必须知道另一个类的存在吗”大多数时候可以把一个行为封装成参数传进来而不是自己去找依赖。第二一个方法不要超过二十行超过就说明它可能干了多件事。拆分的信号是代码里出现了和主流程无关的异常处理、日志记录逻辑。第三避免在循环里调用远程服务。这不仅是性能问题而且制造了调用方对远程服务内部性能的隐性依赖。第四不要直接跨模块操作数据库字段。每个模块对外只暴露业务语义的方法比如user_repo.get_active_user_by_id()而不是db.query(select * from users)。语义接口才能让其他模块不感知你的表结构。第五参数列表不要超过三个。参数一多调用方必须知道所有参数的来源和含义这也是控制耦合的来源之一。解决办法是封装成值对象比如CreateOrderCommand。红线违反了会出现什么最佳替代做法类不直接依赖其他类的内部实现改其中一个类的字段其他类也要改给类定义清晰的外部行为接口方法不做无关任务功能蔓延测试困难按“改变原因”拆分成独立方法不跨层调用基础设施模块分层名存实亡通过接口隔离基础设施细节不暴露数据库表结构和字段数据模型变更扩散到业务层用仓储模式封装数据库访问不用魔法值控制逻辑分支维护者看不懂分支含义用枚举或策略对象表达业务语义4.2 代码评审时我会盯这几个地方如果给你的团队引入“耦合性检查”最有效的执行点就是代码评审阶段。我会在评审时专门看第一个地方是import列表。一个新引入的文件会让多少现有模块生成依赖如果改一个通用工具方法会导致十几个模块都要重新测试这就要警惕了。第二个地方是方法签名。一个方法参数超过五个且调用方多达十处这不是方法设计问题而是耦合信号。第三个地方是测试是否能独立运行。如果测试需要连真实库、调外部系统那就说明被测代码和外部环境耦合太紧应该先通过依赖注入把边界打薄。第四个地方是异常处理是否泄漏内部实现。比如把底层数据库的DuplicateKeyException直接抛给调用方这等于强迫调用方知道底层的存储细节。这些观察点不需要专业工具靠人眼就能看出来。有一种工具可以自动扫描代码的扇入扇出数字高的模块大概率存在问题可以辅助人工判断。工具只是辅助真正让代码变好的是我们在写的时候就开始关注边界。4.3 架构层的“大圈套小圈”思路模块级的解耦只能解决局部问题系统级耦合还得靠架构约束。我最常用的是三层依赖模型领域层定义核心业务实体和业务规则不依赖任何技术细节。应用层负责业务流程编排依赖领域层接口。基础设施层实现数据库访问、外部API调用、消息队列等依赖领域层定义的接口。关键约束是依赖方向必须从外向内基础设施层可以依赖应用层和领域层应用层不能反向依赖基础设施层。如果哪一天你发现领域层代码里直接出现了requests.post说明依赖方向已经出问题了。这种做法的最大好处是外部系统更换的成本极低。比如把某短信供应商换成另一个只需要修改基础设施层实现类领域层和应用层一行都不改。我在一个实际项目里见过一个反例业务逻辑代码和支付系统的接口定义直接放在同一个包下面还互相引用。后来换了支付供应商整个业务逻辑都要跟着改项目组花了两周才理清楚哪些代码可以直接扔。如果当初按依赖方向分层这个工作最多两天。5. 常见问题与避坑技巧实录5.1 最常见的坑以为抽个类就是解耦不少人做重构把一个大类拆成两个类就宣布“已经解耦了”。但实际上如果两个类彼此持有引用互相调用对方的内部方法那只是把一个名字换成两个名字耦合一点都没减少。举个例子OrderService调UserService的getUserWithBalance而UserService又调OrderService的getUnpaidOrders。这里无论怎么拆类它们依然是一团互相缠绕的逻辑只是搬进了不同的文件夹。真正的解耦要领是先搞清楚谁依赖谁再定义接口然后让依赖方向单向化。单向依赖看起来简单做起来难因为业务天然是交叉的。这时候需要上调抽象层级把双方都要访问的东西提升到一个更通用的模块。比如让OrderService和UserService都依赖一个CustomerAccountContext对象而不是互相依赖。5.2 判断重构是否成功的几个可量化信号每次我做完一次以“降低耦合”为目的的重构都会测试这样几个信号改动一个模块的实现是否只需要修改该模块自身和它的直接调用方新增一个同类型的实现类旧代码是不是一行都不用改单元测试能不能在不启动整个系统的情况下独立运行删掉一个模块的无关依赖后编译错误是不是只在有限区域内如果这四个问题的答案都是肯定的这次重构才真正有效。很多团队明明重构完了改动别人的模块还是牵一发动全身那就是把逻辑换了个位置没解决本质问题。5.3 我的实操经验从坏味道开始逐步推进根据我的经验不建议一上来就推倒重来风险太大。正确节奏是先找到一个最令人头疼的功能点比如每次改订单状态都要触达所有渠道的那种模块。把这个模块的调用方和依赖方列出来画一个简单的依赖清单然后按依赖方向整理把基础设施相关的代码移出业务类。每拆一步跑一次测试提交一次代码。拆到哪天发现新需求只需要在新增的类里改旧代码不用动这一步就足够了。剩下的模块可以按同样的节奏慢慢处理不需要一个“大重构日”把所有事做完。还有人会问内聚性和耦合性哪个更重要我的看法是它们是同一个问题的两面。内聚性差模块内部职责混乱外部依赖自然就多外部依赖多了内聚性不可能好到哪去。所以抓任何一个都能带动另一个。实际动工时从最容易下手的地方开始即可不需要等一个完美的方案。真正高质量的代码往往是这种一点一点磨出来的而不是一次设计出来的。