高内聚、低耦合:代码世界的“乐高哲学”
摘要:从朱卫军老师的 CLI 编程课出发,用“造遥控车”和"Type-C 接口”的比喻,拆解软件工程黄金法则。拒绝“万能杂物间”,拥抱“插拔式架构”,让代码从“盘丝洞”进化为“乐高积木”。
引言:为什么你的代码越写越像“盘丝洞”?
很多新手在学完基础语法后,会陷入一个怪圈:代码能跑,但越写越乱。
加个功能要改三个文件,修个 Bug 引出两个新 Bug。最后看着满屏的 import 和纠缠不清的逻辑,感叹一句:“这代码只有上帝和我能看懂,现在只有上帝知道了。”
朱卫军老师在讲 CLI(命令行界面)编程时,反复强调模块化思维。这并非什么高深莫测的架构玄学,其底层逻辑,归根结底就是软件工程的六个字:
高内聚,低耦合。
今天,我们不谈枯燥的 UML 图,我们用造一辆遥控车的逻辑,把这六个字掰开揉碎。
一、 高内聚:拒绝“万能杂物间”
1. 什么是内聚?
内聚(Cohesion),衡量的是一个模块内部元素之间的关联紧密程度。
反面教材:低内聚的“万能杂物间”
你一定见过这样的文件,通常被命名为 utils.py、common.py 或者 helpers.py。它就像一个塞满杂物的抽屉:
# utils.py —— 一个糟糕的杂物间
def send_email(): ...
def calculate_tax(): ...
def draw_chart(): ...
def connect_database(): ...
def format_date(): ...
后果:
- 认知过载:要修“发邮件”的 Bug,你得在一堆税率计算和画图代码里翻找。
- 冲突频发:张三在改数据库连接,李四在改日期格式,两人同时提交
utils.py,Merge Conflict 在所难免。
2. 正面示范:高内聚的“专用工具箱”
高内聚要求:一个模块只做一件事,并把这件事做好。
# email_service.py —— 只干和邮件相关的事
class EmailSender:
def __init__(self, smtp_server):
self.server = smtp_server
def send_welcome(self, user_email):
# 构造邮件、连接服务器、发送、记录日志
...
def send_report(self, report_data):
...
3. 进阶理解:内聚的层级
内聚是有等级的,从低到高:
- 巧合内聚(最低):毫无关系的代码凑在一起(如
utils.py)。 - 逻辑内聚:逻辑上相似,但功能不同(如一个函数处理所有输入:
handle_input(type))。 - 功能内聚(最高):所有元素共同完成一个单一、明确的功能(如
EmailSender)。
通俗比喻:
高内聚就像瑞士军刀里的剪刀。它只为“剪”而生,弹簧、刀片、铆钉全包在一起。你用的时候掏出来就能剪,不用去别的地方找零件。
二、 低耦合:换零件别拆整辆车
1. 什么是耦合?
耦合(Coupling),衡量的是模块与模块之间的依赖程度。
反面教材:高耦合的“焊死电路”
class Car:
def __init__(self):
# 糟糕:Car 内部直接实例化具体的 V8 引擎
self.engine = V8Engine()
def start(self):
self.engine.ignite() # 强依赖 V8 引擎特有的点火方法
后果:
- 牵一发而动全身:老板明天说把 V8 引擎换成电动机。你不仅要写
ElectricMotor类,还得回头修改Car类的内部代码。 - 无法测试:你想测试
Car的启动逻辑,却必须启动一个真实的V8Engine,测试成本高得吓人。
2. 正面示范:低耦合的“插拔式接口”
低耦合的核心是:依赖抽象,而非具体实现。
class Car:
def __init__(self, engine): # 依赖注入:我只认接口,不认牌子
self.engine = engine
def start(self):
self.engine.start() # 我只管叫你“启动”,具体怎么启我不管
# 定义接口规范(Python 中可用 Protocol 或 ABC)
class Engine:
def start(self):
raise NotImplementedError
# 具体实现
class V8Engine(Engine):
def start(self): print("轰隆隆...")
class ElectricMotor(Engine):
def start(self): print("嗡嗡嗡...")
# 使用
car = Car(ElectricMotor()) # 换上电动机,Car 类一行代码都不用改!
通俗比喻:
低耦合就是手机充电口是 Type-C。不管另一头插的是充电宝、电脑还是插座,只要符合 Type-C 标准,手机就能充电。如果手机和充电器是焊死的,那就是高耦合。
三、 为什么必须“双剑合璧”?
你可能会问:“我把所有代码都写在一个 main.py 里,那它内部极其‘高内聚’啊(都在一个文件里),这算好吗?”
这是最大的误区。高内聚和低耦合是硬币的两面,缺一不可。
| 场景 | 只有高内聚 | 只有低耦合 | 高内聚 + 低耦合 |
|---|---|---|---|
| 形态 | 铁板一块 | 意大利面条 | 乐高积木 |
| 表现 | 一个模块包揽所有事,但模块间死死咬合 | 模块分得很细,但互相乱调用,跳转 50 次才能看懂逻辑 | 模块内部紧密,模块间通过清晰接口交互 |
| 修改 | 改一行代码要重启整个程序 | 改一个模块,发现十个模块报错 | 换一块积木,整车升级 |
| 测试 | 无法单独测试,必须运行全量环境 | 很难 Mock 依赖,测试环境搭建困难 | 单元测试极易编写 |
平衡点:
模块内部像一块磁铁,紧紧吸住相关的东西;
模块之间像乐高积木,轻轻一扣就能换,拔下来也不留痕迹。
四、 CLI 编程中的实战落地
回到朱卫军老师的 CLI 编程场景。假设我们要写一个**“批量图片下载器”**。
❌ 错误做法(低内聚、高耦合)
downloader.py 文件里塞了 500 行代码:
- 用
argparse解析命令行参数 - 用
requests发起网络请求 - 用
os创建文件夹 - 用
sqlite3记录下载日志 - 甚至直接打印了彩色的进度条
结果:如果你想把这个下载逻辑用到 GUI 程序里,你得把 argparse 和 print 的代码全删了重写。
✅ 正确做法(高内聚、低耦合)
我们将代码拆分为三个高内聚模块,通过低耦合接口连接:
cli.py(接口层):- 只负责解析参数,调用核心逻辑。
- 不关心图片怎么下载,不关心日志存哪。
core.py(业务层):- 只负责下载逻辑。
- 接收 URL 和保存路径,返回下载结果。
- 不关心参数是怎么来的(是 CLI 给的还是 GUI 给的)。
storage.py(数据层):- 只负责读写数据库。
- 提供
save_log()接口。
架构图:
[CLI] --(参数)--> [Core] --(日志数据)--> [Storage]
^ |
| v
(User) [Network]
当你需要开发 GUI 版本时,复用 core.py 和 storage.py,只写一个新的 gui.py 即可。这就是架构的威力。
五、 灵魂两问:代码写完后的自检清单
下次写代码犹豫结构对不对时,停下来,问自己两个问题:
1. 问内聚:“它跟这个文件里的其他兄弟是一家人吗?”
- 如果
calculate_tax和draw_chart放在一个文件里,它们是一家人吗?显然不是。 - 行动:分家。按业务领域或功能职责拆分文件。
2. 问耦合:“我要是删了这个类,隔壁老王家的代码会报错吗?”
- 如果删了
Database类,User类直接报错,说明User强依赖了Database的具体实现。 - 行动:引入接口。让
User依赖StorageInterface,而不是具体的Database。
结语:代码是写给人看的
Martin Fowler 曾说:
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
高内聚,是为了让阅读代码的人,打开一个文件就能看懂一个完整的故事。
低耦合,是为了让修改代码的人,换掉一个零件时,不用把整辆车拆散。
编程不仅是与机器对话,更是与未来的自己、与队友的协作。
当你开始用“乐高思维”审视代码时,你就已经从“码农”进化为“工程师”了。
雨轩于听雨轩 🌧️🏠