咱们聊聊软件设计里怎么划分模块,这事儿没那么玄乎。如果你问一个老程序员,他可能会告诉你一堆原则,像SOLID之类的。但说到底,很多原则都指向一个更根本的东西,一个你可以马上用起来的准则。
这个准则就是:把需要一起修改的东西,放在一起。把不需要一起修改的东西,分开。
听起来是不是特别简单?简单得像句废话。但你仔细琢磨一下,这背后就是大名鼎鼎的“高内聚,低耦合”思想,只是我用了一句人话把它说出来了。
我们先拆开看。
什么是“需要一起修改的东西”?
这就是“内聚”。一个模块里的代码,应该是因为同一个“变化的原因”而存在的。
举个具体的例子。假设我们做一个电商系统,肯定有用户管理功能。我们创建一个 UserService
模块。这个模块里应该放什么?
很自然,你会想到放 register()
(注册)、login()
(登录)、updateProfile()
(更新个人资料)、changePassword()
(修改密码) 这些函数。
为什么把它们放一起?因为它们都服务于“用户身份管理”这个核心职责。如果将来产品经理说:“我们要在注册时增加一个邮箱验证步骤”,你需要修改 register()
函数。如果他说:“用户登录后,我们要记录最后登录IP”,你需要修改 login()
函数。如果他说:“修改密码的规则要加强,必须包含大小写字母和数字”,你需要修改 changePassword()
函数。
你看,这些修改的“原因”,都和“用户身份”这件事有关。所以,把它们放在 UserService
这个模块里,就是高内聚。当需求变化时,你很清楚要去哪里改代码。你要找的东西都在一个地方,改动范围也相对集中。
反过来,如果你的 UserService
里,还放了一个叫 createOrder()
(创建订单) 的函数,这就坏事了。
创建订单和用户身份管理是两码事。订单逻辑的变化原因通常是:“我们要支持新的支付方式”、“订单要增加一个发货状态”、“优惠券逻辑要调整”。这些变化跟用户注册、登录没有半毛钱关系。
如果你把 createOrder()
硬塞进 UserService
,结果就是:
1. UserService
变得臃肿,职责不清。别人一看这个模块,根本不知道它到底是干嘛的。
2. 当支付逻辑要改时,你却要去动一个叫 UserService
的文件。这完全不符合直觉,增加了心智负担。
3. 最糟糕的是,修改订单逻辑时,你可能会不小心影响到旁边的用户登录功能,引发意想不到的bug。
这就是低内聚。一个模块里混杂了各种不同变化原因的代码。像一个大杂烩抽屉,什么都往里扔,找东西费劲,还容易弄乱别的东西。
所以,划分模块的第一步,就是识别出这些“变化的原因”,把因为同一个原因而变化的代码,关在一起。
再看什么是“不需要一起修改的东西”?
这就是“耦合”。模块和模块之间,难免要有联系,但这种联系要越少越好,越清晰越好。
我们还用电商的例子。现在我们有两个模块了:UserService
和 OrderService
。OrderService
负责创建订单,它里面有个 createOrder()
函数。
创建一个订单,肯定要知道是“谁”创建的。所以 OrderService
不可避免地要和 UserService
打交道。它需要从 UserService
获取当前登录用户的信息,比如用户ID。
这种联系是必要的,无法消除。但是,我们能控制联系的“紧密程度”。
一种好的设计是:OrderService
的 createOrder()
函数,接收一个 userId
作为参数。它只关心“是谁下单”,至于这个用户是怎么登录的、他的密码是什么、个人资料有哪些字段,OrderService
一概不知,也不需要知道。它只是通过一个稳定的接口(比如调用 UserService.getCurrentUserId()
)拿到它唯一需要的信息。
这时候,UserService
和 OrderService
之间的关系就是“低耦合”。它们各自独立,通过一个很窄、很明确的接口进行通信。
如果有一天,UserService
内部的密码加密方式从 MD5 换成了 bcrypt,这对 OrderService
来说毫无影响。它还是只要它的 userId
,其他一概不管。你可以放心大胆地升级用户模块,不用担心把订单系统搞挂。
一种坏的设计是:OrderService
直接去读取 UserService
内部的数据库表,或者直接依赖 UserService
里面某个具体的实现细节,比如一个包含用户所有信息的巨大对象。
如果这样设计,当 UserService
里的用户表结构变了,或者那个巨大对象里的某个字段改名了,OrderService
马上就崩溃了。
这就是“高耦合”。一个模块的改动,像推倒多米诺骨牌一样,引发一连串其他模块的连锁反应。你在A处改代码,结果C处、D处、F处全都报错了。这种代码维护起来就是噩梦,因为你根本无法预测一个简单的修改会带来多大的破坏。
所以,在设计模块间关系时,要问自己:
– 模块A真的需要知道模块B内部的这些细节吗?
– 我能不能只提供一个简单的、稳定的接口,让它们之间的依赖降到最低?
怎么在实践中运用?
这个准则不是一个数学公式,它更像一种感觉,需要你不断练习。
下次你写代码,或者做代码审查(Code Review)时,可以试着问自己几个问题:
-
这个新加的功能,应该放在哪个文件/模块里? 想想这个功能的“变化原因”和哪个模块的现有职责最匹配。如果感觉放在哪里都别扭,那可能说明你需要创建一个新模块。
-
我这次修改,动了几个模块? 如果只是改一个很小的功能,却需要同时修改三四个不相干的模块文件,那很可能就是耦合太高了。你要停下来想一想,是不是模块划分出了问题,或者模块间的通信方式不对。
-
这个模块是不是太大了? 如果一个文件动不动就几千行代码,里面什么逻辑都有,那它肯定是“低内聚”的重灾区。这种“上帝类”(God Class)是每个程序员都应该避免的。可以考虑按职责把它拆分成几个更小的、更专注的模块。
-
模块之间的调用关系清晰吗? 是不是A调B,B又反过来调A,形成一个环形依赖?这种关系会让系统变得脆弱。理想的关系应该更像一条单向的流水线,数据和指令在一个方向上流动。
坚持用“把需要一起修改的东西放一起”这个简单的准则去审视你的代码,久而久之,你的设计能力就会提高。代码会变得更容易理解,更容易维护,也更容易让新同事接手。这比背一堆设计模式的定义要有用得多。因为好的设计,最终目的就是为了让未来的自己和同事,少加点班。
评论前必须登录!
立即登录 注册