摘要:服务边界要同时满足"高内聚"和"低耦合",前者决定一个服务管的是不是"一件事",后者决定它跟别人是不是只说必要的几句话。本文把这两个词从口号拆成可衡量的东西,教你识别共享表、隐式依赖这类不自然耦合,并给出"先聚后切"的实操顺序。
上节我们用限界上下文按语义砍刀,方向对了。可刀口一深,就该谈具体手感:切出来的每一块,内部是不是咬得够紧(内聚),对外是不是只有必要的咧嘴(耦合)? 这节把它讲成可操作的东西。
内聚指一个模块内部的职责"拧成一股绳"。高内聚的服务意味着:属于它的数据、规则、行为都在它体内,改它的理由也只有一个方向。举个例子,一段"下单"逻辑如果有下面的样子,内聚已经出问题:
class CheckoutService: def place_order(self, order): self.order_db.save(order) # 订单数据 self.payment_client.charge(order) # 埋了一颗支付 self.stock_client.decrement(order) # 又调了库存 self.sms.snapshot(order.user, "已下单") # 还发短信
这一段把下单、付款、扣库存、通知全塞进来了。拆服务时,这四个动作大概率归属四个不同上下文(交易、支付、仓配、用户)。它们的"高内聚"在于各自管好自己,而不是在 checkout 里全都内联。判断标准很简单:这段改动常见的触发原因,是不是只有一个方向? 下单加个校验要动它,扣库存规则要动它,短信模板也要动它,那它就是不内聚。
耦合讲的是服务之间"联系的强度"。我们追求的是低耦合:契约清晰、依赖单一、能被替换。
把耦合分两档看:
征兆一:共享表。两个服务写同一张表,等于约定"字段语义我只信 ddl"。一旦一方加了字段、改了长度,另一方要么崩要么悄悄错。正确做法是"数据库每服务",这个概念我们在第 4 章展开,这里先记住它是对抗共享表耦合的正解。
征兆二:隐式事件耦合。服务 B 靠"扫描服务 A 的数据变化"来得知业务状态——比如轮询 A 的表看看有没有待处理的单。这种轮询式同步既慢又脆,正确的解是 A 主动发布领域事件,B 订阅。这又通向了我们第 3 章的消息队列。
给一个乱糟糟的边界做手术,务必"先聚后切",别反过来:
一个反例是急着"先切后聚":还没理清哪个上下文就开砍,结果把同一个业务逻辑半挂在两个服务里,比切开前更糟。宁可先花一周聚拢,也不要第二天就拆出十个服务来。
有些团队喜欢用"这个服务几个文件、几行代码"来间接判断内聚,把"服务要小"等同于"服务要高内聚"。大方向偏了。真正的标准是职责唯一与变更方向单一,而不是体量。一个 500 行但只干一件"库存扣减"的服务,内聚度很高;一个 80 行却同时管订单校验、物流跟踪、风控的服务,内聚度很低。别用行数自我安慰。
前面讲了直觉上的内聚与耦合,最后再给一道能落地的复查尺:当你改了这项能力的一行逻辑,它会影响几个部署单元?
假设你改"下单时校验优惠券是否过期"。理想下,这个改动只碰交易服务,别的服务无感——这是高内聚、低耦合。但如果你发现同一行改动要先改订单服务的表、再去库存服务打个补丁、还要通知支付服务的超时规则,那这一"业务"其实被摊在了多个部署单元里——内聚和耦合同时不合格。
这道复查不用等上线,在画边界时就能推演:把未来半年最常见的十次改动逐条写下来,看每一条会牵连几个服务。牵连数越少、越稳定收敛在 1,边界越健康;如果十条里有五六条都横跨多服务,请毫不犹豫回来重画刀线。这个"变更冲击面=1"的测试,比任何文件行数的指标都更能暴露你到底切得准不准。
理论说完,落回最容易踩的三个"看着像独立、其实没独立"的形态,能省你不少返工:
认这三类的共同点:真正的独立是"数据独立 + 规则独立 + 生命周期独立"三者一起成立,只满足其中一个,都不能叫高内聚。下次验收"拆没拆干净"时,就用这三条逐项过,比纠结服务数要准得多。