复杂度是一个成本中心
“在添加那个抽象(abstraction)之前,先问问:谁会永远支付这笔维护税(maintenance tax)?”
软件工程中有一个危险的时刻。
你发现了一个问题。
你找到了一个巧妙的抽象,可能解决它。
它优雅。
可复用。
可扩展。
能应对未来。
你对这个设计很满意。
然后你把它加了进去。
六个月后,没人记得它为什么存在。
但每个人都得维护它。
这就是复杂度的隐性成本。
复杂度并不免费
当工程师谈论成本时,我们通常想到的是:
- 云基础设施
- 数据库容量
- 开发者薪资
- 许可证
- 运营开支
但还有一种成本,很少出现在电子表格上:
理解和维护系统的成本。
你引入的每一个抽象、框架、服务、队列、缓存、事件和配置,都会产生一笔小额维护账单。
而这笔账单每一天都得支付。
总得有人:
- 理解它
- 测试它
- 监控它
- 为它编写文档
- 升级它
- 调试它
- 帮助新工程师熟悉它
最初的开发者可能花了两天把它构建出来。
接下来的20位工程师,可能要花上数年为它付出代价。
本质复杂度与偶发复杂度
这里就涉及一个重要的区分。
本质复杂度(essential complexity)
这是问题本身所要求的复杂度。
支付系统需要处理:
- 幂等性(idempotency)
- 重试(retries)
- 一致性(consistency)
- 欺诈(fraud)
- 对账(reconciliation)
你不能仅仅因为这些事情麻烦,就把它们去掉。
这个领域本来就复杂。
这就是本质复杂度。
偶发复杂度(accidental complexity)
这是我们自己引入的复杂度。
例如:
“我们要为规模扩张做准备,所以创建12个微服务(microservices)吧。”
但你的系统只有500位用户。
现在你有了:
- 服务发现(service discovery)
- 网络调用(network calls)
- 分布式追踪(distributed tracing)
- 部署流水线(deployment pipelines)
- 重试
- 超时(timeouts)
- 服务责任归属(service ownership)
- 分布式调试(distributed debugging)
业务问题并不要求这一切。
是我们要求的。
这就是偶发复杂度。
而偶发复杂度是最昂贵的一种,因为它不提供任何业务价值。
抽象陷阱
有一种模式,我见过许多次。
一位工程师说:
“我们构建一个抽象吧,这样将来就能支持多种实现。”
听起来合理。
但请问:
我们今天真的有多种实现吗?
如果答案是“没有”,你可能正在为一个永远不会到来的未来付出代价。
看看下面这个例子:
Today:
PaymentProvider
↓
Stripe
Over-engineered:
PaymentProvider
↓
PaymentStrategy
↓
PaymentFactory
↓
PaymentResolver
↓
ProviderRegistry
↓
StripeAdapter
恭喜。
你成功地为将来某一天可能替换Stripe的情况做了设计。
但现在,每位工程师仅仅为了处理一笔支付,就需要理解六层结构。
这个抽象最终可能会派上用场。
也可能变成架构考古(architecture archaeology)。
每个抽象都有一笔维护税
在引入一个新抽象之前,先问:
1. 它今天解决什么问题?
不是明年。
今天。
2. 它引入了什么复杂度?
别只统计代码行数。
想想:
- 认知负荷(cognitive load)
- 运维负担(operational burden)
- 测试
- 部署
- 调试
3. 谁来维护它?
创建它的那个人?
多半不会。
会有别人接手它。
4. 如果我们不构建它,会怎么样?
这是最重要的问题。
如果答案是:
“没什么大不了的。”
那就别构建它。
复杂度会累积
复杂度可怕的地方在于,它很少单独出现。
一个抽象带来了另一个需求。
那个需求又带来了另一个抽象。
突然间:

图中文字(译):新需求 → 新抽象 → 新配置 → 新故障模式 → 新监控 → 新运维成本 → 更多认知负荷 → 更多复杂度。图中箭头从“更多复杂度”回到“新抽象”。
系统就是这样变得难以修改的。
不是因为有人故意设计了一个糟糕的系统。
而是因为每一个单独的决定,在当时看起来都合理。
“能应对未来”往往是一个警示信号
我最喜欢的架构问题之一是:
“我们是在解决一个真实的问题,还是在保护自己免受一个假设问题的影响?”
为未来做设计没有什么错。
但未来有它发生的概率。
如果你只有5%的概率会需要某样东西,今天就应该承担它100%的复杂度吗?
有时应该。
通常不应该。
好的架构并不在于完美地预测未来。
它在于让系统在未来变得明朗时,依然能以低成本修改。
资深工程师的优势
这就是为什么有经验的工程师有时会选择乏味的解决方案。
他们明白一件在职业生涯初期并不显而易见的事:
简单的代码不一定是不成熟的代码。
有时候,简单是经验的结果。
一位资深工程师可能会说:
“今天先实现最简单的版本。如果第二个用例出现,我们到时候再提取抽象。”
这不是懒惰。
这是把复杂度推迟到它能够证明自身价值的时候。
复杂度预算
把复杂度当作一笔预算。
你应该把它花在业务确实需要的地方。
把复杂度花在:
- 正确性(correctness)
- 安全性(security)
- 可靠性(reliability)
- 有需要时的可扩展性(scalability)
- 特定领域的约束(domain-specific constraints)
对于因以下原因引入的复杂度,要保持怀疑:
- 假设的需求
- 流行的架构
- 过早优化(premature optimization)
- 不必要的抽象
- “大家都用这个”的推理
每多一个活动部件,都应该证明自己存在的理由。
架构师的问题
在添加另一个服务、抽象、框架、队列、缓存或层之前,先问:
“我在解决什么问题,五年后谁会为这个决定付出代价?”
因为架构的成本不只是构建系统需要付出的代价。
还包括长期与这个系统打交道需要付出的代价。
而这笔成本,通常由那些在决定做出时甚至都不在场的人支付。
复杂度是一个成本中心(cost centre)。
谨慎地花这笔预算。
此文章由 AI 翻译