最近在做代码评审的时候,看到一个业务模块的单文件代码接近 3000 行。文件里塞了 Excel 导入、库存扣减、消息通知、对账统计,甚至还有一小段定时任务的启动逻辑。问负责的同学,他说“都能跑,就是改起来有点慌,改一个分支经常担心影响别的地方”。这其实就是软件工程里最经典的问题:系统已经能运行,但没有人能完全说清楚它是怎么被组织起来的。
这也是我为什么一直觉得,软件工程的核心不在于“写代码的速度”,而在于管理复杂性。代码写出来只是第一步,能不能让它在需求变化、人员流动、功能膨胀之后仍然被理解、被修改、被验证,才是判断一个开发者水平的分水岭。本文会从概念出发,聊清楚复杂性的来源,再用一个 Python 项目的重构过程,演示如何用抽象、模块化、测试和工程化手段把复杂性压下去。
1. 为什么说软件工程的核心是管理复杂性
1.1 从“软件危机”说起
软件工程这门学科的诞生,并不是因为有人突然想研究“怎么写代码”,而是因为 20 世纪 60 年代出现了大量大型软件项目失败案例。项目延期、预算超支、Bug 多到修不完,业界把这种现象称为“软件危机”。
当时人们发现一个很反直觉的事实:单纯靠增加人手、延长开发时间,很多软件项目不仅没有变好,反而越来越乱。原因在于,软件系统一旦超过某个规模,逻辑之间的关系数量会远超人脑能同时追踪的范围。程序不再只是“对计算机说话”,而是变成了一个需要多人协作、长期演进、不断修改的社会化产物。
软件工程导论里通常会讲软件生命周期、需求分析、设计、测试、维护这些概念,乍看像是一堆流程。但把这些流程串起来看,核心目标只有一个:在复杂度失控之前,用结构和纪律把它约束住。
1.2 本质复杂性 vs 偶然复杂性
谈到复杂性,绕不开《人月神话》的作者 Fred Brooks 在论文里提出的经典划分:本质复杂性和偶然复杂性。
本质复杂性,是指问题本身天然存在的难度。比如一个支付系统必须处理金额精度、并发扣减、账务一致性问题,这是业务规则决定的,不论换成什么语言、什么框架都躲不掉。再比如一个电商订单需要关联商品、库存、优惠券、物流状态,这些领域规则本身就是复杂的。
偶然复杂性,则是由我们的实现方式带来的额外成本。比如代码里出现大量重复逻辑、部署需要人肉执行 20 条命令、依赖版本没人记录导致换台机器就跑不起来、模块之间随意互相调用导致改一处要动十处。这些并不是业务的必然要求,而是工程管理不善造成的。
软件工程做的事情,第一是尽量消除偶然复杂性,第二是把本质复杂性组织成容易被人类理解的形式。你不能让支付逻辑不复杂,但你可以让支付逻辑变得清晰、可测试、可追溯。
1.3 为什么不是“管理代码”而是“管理复杂性”
有人会问:软件工程的核心难道不是把代码写好、写规范吗?代码确实很重要,但代码只是复杂性的一个载体。
很多时候,即使每个文件、每个函数看起来都不错,项目整体依然可能很难维护。真正让人头疼的是模块之间的依赖关系、数据流动的方向、需求变更的传导范围,以及团队协作时知识传递的损耗。这些东西都不是“代码规范”能单独解决的。
从工程实践来看,代码本身有一定的自愈能力,可以通过重构逐步变好。但系统架构层面的混乱、模块边界的模糊、上下文信息的割裂,往往会让重构成本指数级上升。所以需要把“管理复杂性”作为第一原则,让代码组织、技术选型、流程设计都围绕它展开。
2. 软件复杂性到底从哪里来
2.1 需求变化与不确定性
软件复杂性最直接的压力来源,是需求永远在变。业务方一开始说“只需要一个简单的报表”,等做完之后会追加“增加筛选条件”“支持导出 Excel”“按部门权限隐藏数据”。每一条需求听起来都很合理,但它们叠加在一起,会让原来的简单设计变得千疮百孔。
需求变化带来的不只是新增代码。新功能可能会破坏旧假设,比如原本认为“订单状态只有三种”,现在要增加“退款中”“已关闭”等状态,那所有依赖订单状态的地方都要重新检查一遍。这种连锁反应,本质上是信息依赖关系变多变密的结果。
所以软件工程里特别强调需求分析和变更管理,目的不是阻止需求变化,而是在需求变化时,能够评估影响范围、控制传导风险。
2.2 规模膨胀与耦合增多
一个小工具可能只需要一个文件就能搞定,但一个中大型系统通常包含几十个模块、几百张表、上千个接口。系统的规模一旦上来,模块之间的交互数量就会快速增长。
假设有 N 个模块,每两个模块之间可能产生依赖,那么依赖关系在理论上会接近 N² 量级。人脑不可能同时掌握这么多关系。工程上的解法,是通过分层、模块化、接口等方式,把这种网状依赖变成有规则的树状或层次化结构,让每个层次只需要关注与它相邻的层次。
这也解释了为什么很多系统在早期“跑得很爽”,到后期“改得很痛苦”。早期模块少,混乱的成本低;后期模块多了,混乱的成本会指数上升。
2.3 技术栈、协作与时间带来的额外成本
除了业务和规模,软件开发方法本身也会引入复杂度。
技术栈越来越多,一个系统可能同时涉及前端、后端、消息队列、缓存、搜索引擎、大数据组件。这些组件各有各的配置、版本、运维方式。没有统一规范时,光是“让项目在本地跑起来”就能消耗半天。
团队协作也是一大来源。A 同学写了一个工具函数,B 同学不知道,又自己写了一份类似实现;模块之间的命名不一致、接口含义模糊、缺少文档,都会让协作成本上升。时间就更不用说了,业务迭代紧张时,很多人会选择“先上线再说”,技术债日积月累,最终变成一座随时可能倒塌的危楼。
理解了这些来源,就会明白:管理复杂性不是某一个人的任务,而是一套贯穿需求、设计、编码、测试、发布、运维的持续动作。
3. 管理复杂性的核心方法
3.1 抽象与封装:隐藏细节,暴露意图
抽象是计算机科学里最古老也最有效的降复杂度手段。它的核心思想是:把复杂的实现细节藏起来,只暴露一个简单的、稳定的使用入口。
来看一个最直观的例子。假设项目里需要实现两种支付方式:银行卡支付和支付宝支付。如果每次支付都在业务代码里写 if-else,那么所有调用支付的地方都会被支付细节污染:
# 不推荐的写法:业务代码被支付细节渗透 def checkout(order): if order.pay_type == "bank_card": bank = BankCardService() bank.pay(order.no, order.amount) elif order.pay_type == "alipay": ali = AlipayService() ali.pay(order.no, order.amount)更合理的做法是定义一个统一的支付抽象:
class Payment: def pay(self, order_no: str, amount: float) -> bool: raise NotImplementedError class BankCardPayment(Payment): def pay(self, order_no: str, amount: float) -> bool: return BankCardService().pay(order_no, amount) class AlipayPayment(Payment): def pay(self, order_no: str, amount: float) -> bool: return AlipayService().pay(order_no, amount)这样业务层只需要面向Payment这个抽象编程,不需要关心具体是哪种支付渠道。后续新增微信支付,只需要新增一个WechatPayment类,业务代码不用改。这就是抽象和封装的价值:它把“策略选择的复杂度”和“具体实现的复杂度”剥离开了。
3.2 模块化与分层:控制依赖方向
模块化是把系统切成若干职责单一的单元,分层则是给这些单元约定依赖方向。常见的后端分层是 Controller -> Service -> Repository。Controller 负责接收请求,Service 负责业务逻辑,Repository 负责数据访问。
这样做的好处是,每一层只允许依赖它下面的层,不能跨层调用。比如 Repository 不能反过来调用 Service,Controller 不能直接写 SQL。依赖方向一旦被约束住,修改的范围就变得可控:改数据库实现时,最多影响 Service 层;改接口入参时,不需要关心存储细节。
模块化更重要的是“职责单一”。每个模块应该对一类变化封闭,对一个功能负责。如果一个模块既要做用户认证,又要做订单导出,那它面对的需求变化可能来自两个完全不同的方向,修改频率和理由也会各不相同,合并在一起只会让代码更难维护。
3.3 接口与契约:让协作变得可预期
模块之间不能靠互相读源码来协作,而应该靠接口契约。接口约定了“输入是什么、输出是什么、什么时候抛异常”,至于内部怎么实现,调用方不需要关心。
在 Python 里,协议和抽象基类都承担了这种角色。一个更简单的协作方式,是定义好数据结构和函数签名。比如订单处理模块约定process_orders(input_path, output_path)接收文件路径、生成结果文件,其他模块只需要知道这个约定,不需要关心内部解析逻辑。
接口稳定带来的直接收益是:团队可以并行开发。A 同学负责订单处理,B 同学负责统计报表,只要接口约定先定好,双方可以独立推进,最后通过集成测试验证。复杂协作因此变成了相对简单的对接。
3.4 为什么这些方法能降低复杂度
抽象、模块化、分层、接口,本质上都在做同一件事:减少人脑需要同时关注的信息量。
人类的工作记忆容量有限,很难一次性处理超过 7 个左右的信息单元。如果代码里每个函数都超过 200 行,每个类都牵扯几十个字段,系统没有清晰的层次划分,那么每当你想修改一个点时,大脑就需要反复跳转上下文,很快就会被细节淹没。
反过来,当代码被拆成短小、独立、有明确边界的小单元时,你每次只需要理解一个局部,其他部分通过接口和命名就能推测用途。这种“局部推理优先”的体验,是管理复杂性最直接的体现。
4. 用工程化手段消除偶然复杂性
4.1 版本控制:收敛协作复杂度
日常开发中,版本控制工具(如 Git)已经是最基础的基础设施。它解决了两个核心问题:一是让多人可以并行修改同一份代码而不会互相覆盖;二是让每次变更都有历史记录,可以追溯、对比、回滚。
这套机制本身也是在管理复杂性。想象一个没有版本控制的项目:A 同学改了 3 个文件,B 同学也改了 3 个文件,其中 2 个文件是重叠的。时间一长,没人记得谁改过什么、为什么改、改错了怎么恢复。这种“信息丢失带来的混乱”,就是典型的偶然复杂性。
在使用 Git 时,建议养成小步提交的习惯。一个提交只做一件事,提交信息写清楚“为什么”,不要等到功能全部写完再一次性提交。这样在排查问题时,可以通过git log和git diff快速定位变更范围。
4.2 依赖与配置管理:消除环境差异
“在我电脑上能跑”这句话,在工程上几乎等价于“问题无法复现”。环境差异是偶然复杂性里最令人头疼的一类。
Python 项目里常见的问题是:有人用 Python 3.10,有人用 3.8;有人本地装了一堆包,有人从零开始装。最后项目运行结果完全不同。解决办法是使用虚拟环境,并把依赖记录到文件里:
python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate pip install -r requirements.txtrequirements.txt建议锁定具体版本:
pytest==8.0.0 requests==2.31.0锁定版本的意义在于,让每个人的环境尽量一致,减少“版本不同导致行为不同”这一类问题。更严格的项目还会使用 lock 文件和哈希校验,思路完全相同:把环境不确定性降到最低。
4.3 自动化测试:降低回归风险
代码规模变大后,最怕的不是写不出新功能,而是改旧功能时把原来正常的行为弄坏。自动化测试就是用来兜住这类回归风险的。
比如订单处理脚本里有一个金额计算逻辑,如果我们在重构时改了格式化规则,又没有测试,代码可能还是能运行,但输出的格式已经错了。而如果有测试,运行一次pytest就能立刻发现。
pip install pytest pytest -q测试代码本身也是在给系统建立“行为契约”。它告诉后来者:这个函数在给定输入下,应该得到什么输出。维护测试需要成本,但它换来的安全感和重构空间,在项目进入维护期后会非常值钱。
4.4 CI/CD 与可观测性:让发布和排查更可控
持续集成(CI)是在代码每次提交后自动运行测试、构建、静态检查。持续部署(CD)则是把验证通过的版本自动或半自动发布到环境。这套自动化流程,本质上消灭了“手工部署步骤复杂”造成的偶然复杂性。
以 GitHub Actions 为例,只需要在仓库里放一个.github/workflows/ci.yml文件,就能在每次 push 时自动跑测试:
name: CI on: push: pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.10" - run: pip install -r requirements.txt - run: pytest可观测性同样重要。线上系统出问题时,如果日志分散、格式混乱、没有唯一请求 ID,排查问题会像大海捞针。规范日志格式、记录关键业务事件、接入集中式日志平台,都是把“定位问题”的复杂度降低的手段。
5. 实战案例:一个订单处理脚本的工程化演进
5.1 原始需求与第一版代码
先假设一个非常简单的需求:有一个orders.csv文件,里面是订单数据,包含订单号、商品名、数量、单价。程序需要读取它,校验数据合法,计算每个订单的金额和所有订单的总金额,最后把结果写入result.txt。
示例输入orders.csv:
order_id,product,quantity,price A001,书本,2,50 A002,钢笔,3,12.5很多刚入门的人会直接写成下面这样:
import csv def main(): orders = [] with open('orders.csv', 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: order_id = row['order_id'] product = row['product'] quantity = int(row['quantity']) price = float(row['price']) total = quantity * price orders.append((order_id, product, quantity, price, total)) total_amount = 0.0 lines = [] for order in orders: order_id, product, quantity, price, total = order lines.append(f"{order_id},{product},{quantity},{price},{total}") total_amount += total lines.append(f"TOTAL,{total_amount}") with open('result.txt', 'w', encoding='utf-8') as f: f.write("\n".join(lines)) if __name__ == '__main__': main()这段代码能跑通,但它已经把“读取文件、解析字段、业务校验、金额计算、格式化、写文件”全部耦合在一个 main 函数里了。如果明天要支持“数量为 0 时跳过该订单”“价格保留两位小数”“增加订单状态字段”,这个函数会越来越臃肿,最终变成谁都不敢动的“屎山”。
5.2 第一轮重构:拆出职责单一的函数
第一轮重构的目标,是把不同的职责拆成独立函数,让每个函数只做一件事:
import csv def load_orders(filepath: str) -> list[dict]: """读取 CSV 文件,返回原始订单字典列表。""" with open(filepath, 'r', encoding='utf-8') as f: return list(csv.DictReader(f)) def validate_order(row: dict): """校验单条订单数据,返回解析后的字段。""" order_id = row.get('order_id', '').strip() product = row.get('product', '').strip() quantity = int(row['quantity']) price = float(row['price']) if quantity <= 0: raise ValueError(f"订单 {order_id} 数量必须为正数") if price < 0: raise ValueError(f"订单 {order_id} 价格不能为负数") return order_id, product, quantity, price def calculate_amount(order_id: str, product: str, quantity: int, price: float) -> dict: """计算单个订单金额,并组装成结构化数据。""" return { 'order_id': order_id, 'product': product, 'quantity': quantity, 'price': price, 'total': round(quantity * price, 2) } def format_order(order: dict) -> str: """把单个订单格式化为输出行。""" return ( f"{order['order_id']},{order['product']},{order['quantity']}," f"{order['price']:.2f},{order['total']:.2f}" ) def write_result(output_path: str, orders: list[dict]) -> None: """写入结果文件,并附带总金额。""" lines = [format_order(order) for order in orders] total = round(sum(order['total'] for order in orders), 2) lines.append(f"TOTAL,{total:.2f}") with open(output_path, 'w', encoding='utf-8') as f: f.write("\n".join(lines)) def main() -> None: orders = [] for row in load_orders('orders.csv'): order_id, product, quantity, price = validate_order(row) orders.append(calculate_amount(order_id, product, quantity, price)) write_result('result.txt', orders) if __name__ == '__main__': main()这个版本比第一版已经清晰很多:每个函数名字都在描述“它负责什么”,调用方一眼就能看出主流程是“读取 -> 校验 -> 计算 -> 写入”。
5.3 第二轮重构:引入数据类和校验模块
第一轮重构用了字典来传数据,但字典的字段名只能靠约定,调用方很容易写错键名,而且类型也不够直观。第二轮可以引入dataclass,把订单定义成真正的数据结构。
项目结构可以调整为:
order_processor/ ├── models.py ├── validators.py ├── processor.py └── tests/ └── test_processor.pymodels.py:
from dataclasses import dataclass @dataclass class Order: order_id: str product: str quantity: int price: float total: floatvalidators.py:
class ValidationError(Exception): pass def validate_order(row: dict) -> Order: order_id = row.get('order_id', '').strip() product = row.get('product', '').strip() quantity = int(row['quantity']) price = float(row['price']) if quantity <= 0: raise ValidationError(f"订单 {order_id} 数量必须为正数") if price < 0: raise ValidationError(f"订单 {order_id} 价格不能为负数") total = round(quantity * price, 2) return Order( order_id=order_id, product=product, quantity=quantity, price=price, total=total )processor.py:
import csv from models import Order from validators import validate_order def load_orders(filepath: str) -> list[dict]: with open(filepath, 'r', encoding='utf-8') as f: return list(csv.DictReader(f)) def format_order(order: Order) -> str: return ( f"{order.order_id},{order.product},{order.quantity}," f"{order.price:.2f},{order.total:.2f}" ) def write_result(output_path: str, orders: list[Order]) -> None: lines = [format_order(order) for order in orders] total = round(sum(order.total for order in orders), 2) lines.append(f"TOTAL,{total:.2f}") with open(output_path, 'w', encoding='utf-8') as f: f.write("\n".join(lines)) def process_orders(input_path: str, output_path: str) -> None: orders = [] for row in load_orders(input_path): orders.append(validate_order(row)) write_result(output_path, orders) if __name__ == '__main__': process_orders('orders.csv', 'result.txt')数据类的好处是很明显的:现在order.total是明确的字段,而不是字典里的字符串键;类型标注也会在 IDE 里给出自动补全,写错字段名能更快被发现。
5.4 补上测试与项目结构
有了独立函数和数据类,测试就变得容易了。tests/test_processor.py:
from processor import process_orders def test_process_orders(tmp_path): input_file = tmp_path / "orders.csv" input_file.write_text( "order_id,product,quantity,price\n" "A001,书本,2,50\n" "A002,钢笔,3,12.5\n", encoding="utf-8" ) output_file = tmp_path / "result.txt" process_orders(str(input_file), str(output_file)) lines = output_file.read_text(encoding="utf-8").splitlines() assert lines[0] == "A001,书本,2,50.00,100.00" assert lines[1] == "A002,钢笔,3,12.50,37.50" assert lines[2] == "TOTAL,137.50" def test_validate_order_rejects_negative_price(): import pytest from validators import validate_order, ValidationError with pytest.raises(ValidationError): validate_order({ "order_id": "A003", "product": "橡皮", "quantity": "1", "price": "-5" })这里用到了pytest内置的tmp_path临时目录 fixture,确保测试不会污染项目目录。运行方式:
pip install pytest pytest -q5.5 运行与验证
执行主程序:
python processor.py生成的result.txt内容为:
A001,书本,2,50.00,100.00 A002,钢笔,3,12.50,37.50 TOTAL,137.50可以看到,重构后的代码在功能上没有变化,但结构完全不同。后续如果要增加“订单数量不能超过 999”的规则,只需要改validators.py;如果要改成输出 JSON 格式,只需要调整write_result和format_order。每个需求变更都能被限定在一个局部区域内,这就是管理复杂性的价值。
6. 常见问题与排查思路
6.1 典型问题汇总表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 改一个功能,另外两个无关功能出问题 | 模块边界不清,全局状态被随意修改 | 重新梳理模块职责,把共享状态收敛到明确位置,补回归测试 |
| 项目换台电脑就跑不起来 | 依赖版本未锁定、缺少虚拟环境 | 使用 venv,生成 requirements.txt 并锁定版本 |
| 测试都通过了,上线还是出 Bug | 测试只覆盖正常路径,缺少边界和异常场景 | 增加异常输入、空数据、大数据量等用例,关注行为级断言 |
| 团队多人经常代码冲突 | 模块划分不够独立,大家都在改同一个文件 | 按功能边界拆分模块,减少文件级耦合 |
| 线上问题半天定位不到 | 日志不规范,没有请求追踪 ID | 统一日志格式,增加 trace_id,接入集中式日志 |
| 接手旧项目完全看不懂 | 缺少文档、函数命名随意、逻辑堆叠 | 先补充模块级文档,再逐步重构高风险函数 |
| 毕业设计一个文件写完所有功能 | 急于演示,缺乏模块意识 | 按“入口 -> 业务逻辑 -> 数据访问”拆分,控制单个文件规模 |
6.2 排查复杂度问题的通用清单
当你觉得代码“越来越难改”时,按下面顺序排查:
- 先看依赖方向:上层模块是否直接依赖了底层细节,是否存在跨层调用。
- 再看模块边界:每个模块是否在同时应对多个变化方向,是否职责过重。
- 然后看函数粒度:单个函数是否超过 50 行,是否做了多件独立的事。
- 接着看数据流:数据在模块之间是怎么传递的,有没有隐式的共享状态。
- 最后看测试:关键逻辑有没有测试兜底,没有测试的区域往往是重构最大的阻碍。
这套排查思路,比一上来就“重写整个系统”要稳妥得多。绝大多数复杂度问题,都是通过小步重构逐步缓解的。
7. 最佳实践与工程建议
7.1 写代码阶段
第一,函数要小,且职责单一。一个函数最好只做一件事,名字能准确描述这件事。如果你发现自己需要写很多行注释才能解释函数在干什么,那通常说明函数拆得不够。
第二,命名要表达意图。a、b、data、tmp这类名字虽然不会报错,但会强迫每个读者去追踪赋值来源。换成order_id、unpaid_orders、output_path后,代码的阅读成本会大幅降低。
第三,善用数据类和类型注解。Python 是一门动态语言,但类型标注和dataclass能让数据结构更明确,也能借助 IDE 静态检查提前发现低级错误。
7.2 工程管理阶段
依赖管理要放在项目第一天就做好。Python 项目建议从创建开始就启用虚拟环境,并维护requirements.txt。项目配置和业务代码分离,不要让数据库密码、API Key 散落在代码里。
测试不是可有可无的装饰,而是重构和扩展的底气。不要求一开始就追求覆盖率 100%,但要优先给业务核心逻辑、计算方法、数据转换这些高风险函数补上测试。
代码评审同样重要。让同事看看你的设计,很多时候比自己埋头写更高效。评审的重点不要只放在“代码风格”,而要关注“模块边界是否清晰”“有没有过度设计”“变更范围是否可控”。
7.3 安全意识与生产环境
涉及数据库更新、删除类操作时,先备份,再在测试环境验证,避免直接在生产环境执行未经验证的 SQL。服务账号遵循最小权限原则,能只读就不要给写权限,能限制在某几张表就不要给全库权限。
生产环境配置变更要谨慎,建议走配置中心或至少留存变更记录。线上日志要记录关键业务事件和异常堆栈,但不要记录密码、身份证号等敏感信息。接口层面做好输入校验和鉴权,防止越权访问。
技术债需要主动管理。每个迭代留出少量时间做重构和清理,不要一直“先上线再说”。欠下的债,最后都会以更昂贵的返工来偿还。
8. 总结与下一步学习路线
8.1 本文核心收获
软件工程的核心在于管理复杂性,这句话不是口号,而是贯穿所有工程活动的准绳。抽象、模块化、分层、接口、测试、CI/CD,本质上都是把复杂系统拆成“人能理解的小块”,并用自动化工具把偶然复杂性压到最低。
在实战案例里,一个简单的订单处理脚本通过两次重构,从 30 行堆在一起的 main 函数,变成结构清晰、可测试、易扩展的小项目。这个过程其实就是软件工程能力的缩影:不改变功能,只改变组织方式,却让后续维护的难度明显下降。
8.2 后续可以学什么
如果你是从零开始,建议按这个顺序往下走:
- 先掌握一门语言的基础,比如 Python,并练习用函数、数据类组织代码。
- 学习软件工程导论里的生命周期、需求分析、设计、测试概念,把这些概念映射到自己写的项目里。
- 学习常见设计模式和设计原则,比如单一职责、依赖倒置,理解什么时候该用、什么时候不要过度设计。
- 再往上是软件架构,比如分层架构、微服务、事件驱动,了解不同架构在面对不同复杂度时的取舍。
- 最后可以了解项目管理、版本管理、CI/CD 和监控体系,把工程能力扩展到团队协作层面。
如果你将来想转向机器视觉等 AI 方向,软件工程的方法论同样成立。数据清洗管道、模型训练实验、推理服务的模块化组织,都需要处理大量不确定性。能把复杂性管住的人,换到任何技术领域都不会太差。
8.3 给新人的一点提醒
很多同学做软件工程毕业设计时,习惯把“能跑”当成唯一目标,最后系统堆在一个文件里,演示时一切正常,答辩时被问几个问题就答不上来。我更希望大家把它当成一次完整的“管理复杂性”训练:系统的模块怎么划分、数据怎么流动、关键逻辑有没有测试、别人能不能看懂你的文档。
对我来说,一个项目是否算好工程,不在于它演示时多么顺滑,而在于半年后当你再次打开它时,会不会想删掉重写。把项目做出来只是起点,让项目在半年后仍然可以被人轻松接手和修改,才算真正跨过了软件工程的门槛。