63 of 584

复杂度是一个成本中心

T
The Architect’s Notebook

“在添加那个抽象(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 翻译