浅显讲解 Rich Hickey 的 Clojure 思想:简洁、不可变性与更好的默认值——关于如何构建更平静、更安全的复杂系统的实用经验。

软件很少一次性变得复杂。它是通过一次又一次“合理的”决定走到那里的:为了赶期限加了一个快速缓存、为了避免拷贝共享了可变对象、因为“这个例外特殊”而放宽规则。每个选择看起来都小,但累积起来就会产生一个对变更充满风险、错误难以复现、添加新功能比实现它们花更长时间的系统。
复杂性占上风因为它提供了短期的舒适感。引入新依赖通常比简化现有结构更快。修补状态比追问“为什么状态分散在五个服务里”更容易。当系统增长速度超过文档更新时,人们也更容易依赖约定和部落知识。
这不是一本 Clojure 教程,你也不需要懂 Clojure 才能获益。目标是借用一套与 Rich Hickey 的工作相关的实用思想——这些思想可应用于日常的工程决策,无论使用何种语言。
大多数复杂性并不是由你刻意编写的代码产生,而是由工具让某些做法成为默认变得容易而产生的。如果默认是“到处都是可变对象”,你会得到隐藏耦合。如果默认是“状态驻留在内存中”,你会在调试和可追溯性上吃亏。默认值塑造习惯,而习惯塑造系统。
我们将关注三个主题:
这些思想不会从领域中移除复杂性,但可以阻止你的软件将其成倍放大。
Rich Hickey 是一位长期的软件开发者和设计者,因创建 Clojure 以及在演讲中挑战常见编程习惯而闻名。他的关注点不是追逐潮流,而是分析系统变得难以更改、难以推理和难以信任的反复出现的原因。
Clojure 是一门现代编程语言,运行在诸如 JVM(Java 运行时)和 JavaScript 等常见平台上。它旨在与现有生态协作,同时鼓励一种特定的风格:把信息表示为纯粹的数据,偏好不变的值,并把“发生了什么”与“你在屏幕上显示什么”分离开。
你可以把它看作一门促使你更倾向于使用更清晰构建块、远离隐藏副作用的语言。
Clojure 的目标不是让小脚本更短,而是解决反复出现的项目痛点:
Clojure 的默认倾向促进更少的活动部件:稳定的数据结构、显式更新和让协作更安全的工具。
价值并不限于换语言。Hickey 的核心思想——通过消除不必要的相互依赖来简化、把数据视为持久事实、最小化可变状态——能改善 Java、Python、JavaScript 等语言中的系统。
Rich Hickey 清晰地区分了简单(simple)和容易(easy)——这是大多数项目在不知不觉中跨过的一条线。
容易关乎“现在的感觉”。简单关乎“有多少部件以及它们有多纠缠”。
在软件中,“容易”常意味着“今天敲代码快”,而“简单”意味着“下个月更难被破坏”。
团队常常选择能立即降低摩擦的捷径,但这些捷径会增加需要维护的隐性结构:
每个选择可能感觉像速度,但它增加了活动部件、特例和交叉依赖。这就是系统在没有任何戏剧性错误的情况下变脆弱的方式。
快速交付可以很好——但没有简化的速度通常意味着你在透支未来。利息表现为难以复现的 bug、拖慢的入职速度,以及需要“谨慎协调”的变更流程。
评审设计或 PR 时问自己:
“状态”就是系统中会改变的东西:用户的购物车、账户余额、当前配置、工作流的步骤。关键问题不是变化存在,而是每次变化都增加了不一致的机会。
当人们说“状态导致 bug”时,他们通常指的是:如果同一条信息在不同时间或不同地点可能不同,那么代码就必须不断回答“现在哪个版本才是真正的?”回答错误就会产生看起来随机的错误。
可变性意味着对象在原地被修改:“同一件事物”会随时间而异。听起来高效,但它让推理变得困难,因为你不能依赖之前看到的东西。
一个可感知的例子是共享的电子表格或文档。如果多个人可以同时编辑同一单元格,你的理解会立刻失效:总计改变、公式失效或行被重排。即便没人恶意操作,“可共享可编辑”的特性本身就会制造混乱。
软件状态也是如此。如果系统的两部分读取相同的可变值,其中一部分可以在另一部分仍然基于过时假设运行时悄悄改变它。
可变状态把调试变成考古学。一个 bug 报告很少会告诉你“数据在 10:14:03 被错误地改了”。你只看到结果:错误的数字、意外的状态、只在特定情况下失败的请求。
因为状态会随时间更改,最重要的问题变成:是什么编辑序列导致了现在的结果? 如果无法重构那个历史,行为就不可预测:
这就是为什么 Hickey 把状态视为复杂性的倍增器:一旦数据既被共享又是可变的,可能的交互数量增长得比你维持它们的能力还快。
不可变性简单来说就是数据在创建后不再改变。你不是在原有信息上就地编辑,而是创建一个反映更新的新信息。
把它想像成收据:打印出来后你不会擦掉某行再重写总计。如果有变动,你会开一张更正收据。旧的保留,新的一目了然地是“最新版本”。
当数据不能被悄悄修改时,你就不必担心背后有隐形改动。这让日常推理容易得多:
这是 Hickey 反复谈到简洁的一个重要原因:更少的隐藏副作用意味着更少需要跟踪的心理分支。
创建新版本听起来可能浪费,直到你比较替代方案。就地编辑会让你不断问:“谁改了它?什么时候改的?之前是什么?”有了不可变数据,变更是显式的:新版本存在,旧版本仍可用于调试、审计或回滚。
Clojure 倾向于让把更新视为产生新值而不是修改旧值变成自然的做法。
不可变性不是免费的。你可能会分配更多对象,习惯于“直接更新东西”的团队也需要时间适应。好消息是现代实现常常在底层共享结构以减少内存成本,回报通常是更平静的系统和更少难以解释的事故。
并发只是“许多事情同时发生”。一个处理数千请求的 web 应用、在生成收据时更新余额的支付系统、或后台同步的移动应用——这些都是并发场景。
难点不在于多件事同时发生,而在于它们常常触及相同的数据。
当两个工作者都可以读取然后修改同一值时,最终结果可能取决于时序。这就是竞态条件:不是容易复现的错误,而是在系统繁忙时出现的错误。
例如:两个请求尝试更新订单总额。
没有“崩溃”,但你丢失了一次更新。在高并发下,这类时序窗口更常见。
传统的修复办法——锁、同步代码块、谨慎的顺序控制——有效,但它们迫使所有人协调。协调代价高:降低吞吐量,并且随着代码库增长变得脆弱。
使用不可变数据时,值不会被原地编辑。相反,你创建一个代表更改的新值。
这一改变去除了整类问题:
不可变性并不意味着并发无成本——你仍需规则来决定哪个版本是当前的。但它让并发程序更可预测,因为数据本身不再是一个移动的目标。当流量暴增或后台作业堆积时,你不太可能看到神秘的、时序依赖的故障。
“更好的默认值”意味着安全的选择会自动发生,只有在你明确选择退出时才承担额外风险。
这听起来很小,但默认值会悄悄影响你周一早上写的代码、审查者在周五下午接受的改动,以及新队友从首个代码库学到的习惯。
“更好的默认值”不是替你做每个决策,而是让常见路径更不易出错。例如:
这些并不能消除复杂性,但能阻止复杂性蔓延。
团队不仅遵循文档——他们遵循代码“想要”你做的事。
当共享状态容易被修改时,它会成为惯常的捷径,审查者不得不争论意图:“这里安全吗?”当不可变性和纯函数是默认时,审查者可以专注于逻辑和正确性,因为高风险的操作会显得突出。
换句话说,更好的默认值创造了更健康的基线:大多数改动看起来一致,而不寻常的模式足够显眼值得被质疑。
长期维护主要是关于安全地读取和修改现有代码。
更好的默认值帮助新队友更快上手,因为隐藏规则更少(“小心,这个函数悄悄更新了那个全局映射”)。系统更易推理,从而降低未来每次功能、修复和重构的成本。
Hickey 演讲中一个有用的思维转变是把事实(发生了什么)与视图(我们当前相信的是什么)分开。大多数系统把它们混为一谈,只存储最新值——用今天覆盖了昨天——这会让时间消失。
事实是不可变记录:“订单 #4821 在 10:14 下单”,“支付成功”,“地址被更改”。这些不会被编辑;当现实变化时你会新增事实。
视图是应用当前所需的东西:“当前的收货地址是什么?”或“客户余额是多少?”视图可以从事实重算、缓存、建立索引或物化以提速。
保留事实会带来:
覆盖记录就像更新电子表格单元格:你只能看到最新数字。
追加式日志就像支票簿记账:每一条条目都是事实,“当前余额”是从条目计算出来的视图。
你不必采用完整的事件溯源架构就能受益。许多团队从小处开始:为关键更改保留追加式审计表、为少数高风险工作流存储不可变的“变更事件”,或保存快照加短期历史窗口。关键在于养成习惯:把事实当作持久的,把当前状态当作方便的投影。
Hickey 一个很实用的思想是数据优先:把系统信息当作纯值(事实)来处理,把行为当作针对这些值运行的东西。
数据是持久的。如果你存储清晰、自包含的信息,就可以在以后重新解读它、在服务间迁移、重新建立索引、审计或用于新功能。行为则不那么持久——代码会变、假设会变、依赖会变。
当你把两者混在一起时,系统会变得粘滞:你无法重用数据而不拖拽生成它的行为。
把事实与动作分离可以减少耦合,因为组件可以在数据形状上达成一致,而不必在共享代码路径上达成一致。
报告任务、支持工具和计费服务都可以消费相同的订单数据,各自应用自己的逻辑。如果你把逻辑嵌入到持久化表示中,每个消费者都会依赖那段嵌入逻辑——改变它就会变得风险极大。
干净数据(易于演化):
{
"type": "discount",
"code": "WELCOME10",
"percent": 10,
"valid_until": "2026-01-31"
}
存储中的微型程序(难以演化):
{
"type": "discount",
"rule": "if (customer.orders == 0) return total * 0.9; else return total;"
}
第二种看起来灵活,但它把复杂性推到了数据层:你现在需要安全的评估器、版本控制规则、安全边界、调试工具以及当规则语言变化时的迁移计划。
当存储的信息保持简单且显式时,你可以在不重写历史的情况下改变行为。旧记录依然可读。新服务可以添加而不必“理解”遗留的执行规则。你可以通过编写新代码引入新解释——新的 UI 视图、新的定价策略、新的分析——而不是通过改变数据的“含义”。
大多数企业系统不是因为某个模块“坏”而失败。它们失败是因为一切互相连接。
紧耦合以“小改动需要数周回归测试”的形式出现。给某服务加个字段会破坏三个下游消费者。共享数据库 schema 成为协调瓶颈。单一可变缓存或单例“配置”悄然成为半数代码库的依赖。
级联式变更是自然结果:许多部分共享同一可变事物时,冲击半径会扩大。团队通过增加流程、规则和移交来应对——往往让交付更慢。
你可以在不换语言或重写所有东西的情况下应用 Hickey 的思想:
当数据不会在脚下改变时,你花在调试“它是如何进入这种状态的?”上的时间就会减少,而更多时间可以用来推理代码的实际行为。
不一致从默认值悄然渗入:每个团队都自创时间戳格式、错误形状、重试策略和并发方案。
更好的默认看起来像:版本化的事件 schema、标准的不可变 DTO、清晰的写入归属,以及一小套被认可的序列化、校验和跟踪库。结果是更少的惊喜集成和更少的一次性修复。
从已经有变更的地方开始:
这种方法在保持系统运行的同时提升可靠性和团队协调,并把范围控制到可完成的规模。
当你的工作流支持快速、低风险的迭代时,这些思想更容易应用。例如,如果你在构建像 Koder.ai 这样的产品(一个面向 Web、后端和移动应用的聊天式即写即编码平台),有两项功能直接契合“更好默认值”思路:
即便你的栈是 React + Go + PostgreSQL(或移动端的 Flutter),核心观点依然:你每天使用的工具会悄悄教给你一种默认工作方式。选择那些让可追溯性、回滚和明确规划成为常态的工具,可以减少现场“就地修补”的压力。
简洁和不可变性是强有力的默认选择,而不是道德律令。它们在系统增长时能减少出乎意料的改变次数。但真实项目有预算、期限和约束——有时可变性是正确的工具。
当它被限制时,可变性是实际且合理的:性能热点(紧循环、高吞吐解析、图形、数值运算)、函数内部局部变量、通过接口隐藏的私有缓存、或单线程组件在明确边界内的使用。
关键是封装。如果“可变的东西”绝不会泄露出去,它就无法在代码库中传播复杂性。
即便在以函数式为主的风格里,团队仍需明确归属:
这也是 Clojure 倾向于数据与显式边界的好处所在,但这是一种架构纪律,而非语言独有的特性。
没有任何语言能修复糟糕的需求、不明确的领域模型或无法达成共识的团队。不可变性不会让一个混乱的工作流变得可理解,“函数式”代码仍然可能将错误的业务规则写得更整洁——但并不正确。
如果系统已上线,不要把这些思想当作非此即彼的重写理由。寻找能降低风险的最小举措:
目标不是追求纯粹,而是每次变更产生更少意外。
这是一个可在不更换语言、框架或团队结构下完成的冲刺级清单。
让你的“数据形状”默认不可变。 把请求/响应对象、事件和消息当作创建后不再修改的值。如果必须改变,就创建新版本。
在工作流中间偏好纯函数。 从一个工作流开始(例如定价、权限、结账),把核心重构为接受数据并返回数据的函数——不要有隐藏的读/写。
把状态移到更少、更清晰的位置。 每个概念选一个权威来源(客户状态、特性开关、库存)。如果多个模块各自保存一份副本,那就把它作为一个明确的决定并制定同步策略。
为关键事实增加追加式日志。 在一个领域记录“发生了什么”作为持久事件(即便你仍然存储当前状态)。这改善可追溯性并减少猜测。
在 API 中定义更安全的默认值。 默认应最小化惊讶:明确时区、明确空值处理、明确重试和显式的顺序保证。
寻找关于简单与容易的区别、管理状态、面向值的设计、不可变性,以及“历史”(随时间的事实)如何帮助调试与运营的材料。
简洁不是一个你贴上去的功能——它是通过一系列小而可重复的选择来实践的策略。
复杂性通过一系列看似合理的、局部优化的决定慢慢累积(额外的标志位、缓存、例外处理、共享工具等),这些决定增加了“模式”和“耦合”。
一个好的信号是:一次“小改动”需要在多个模块或服务间协调修改,或者审查者需要依赖“部落知识”来判断是否安全。
因为捷径优化的是“今天的摩擦”(上线速度),却把成本推到未来:调试时间、协调开销和变更风险。
一个有用的习惯是在设计/PR 评审时问:“这会引入哪些新的运动部件或特例?谁来维护它们?”
默认行为塑造工程师在压力下的选择。如果可变性是默认值,共享状态就会蔓延;如果默认假设“内存就足够”,可追溯性就会消失。
通过让安全路径成为最省力的路径来改进默认值:边界处使用不可变数据、明确的时区/空值/重试策略,以及清晰的状态归属。
“状态”是随时间变化的任何事物。难点在于变化带来了分歧的可能性:两个组件可能拥有不同的“当前”值。
因此,漏洞常表现为时序相关的行为(“本地可重现”、“在生产中不稳定”),因为问题变为:我们基于哪个版本的数据进行了操作?
不可变性意味着你不在原处编辑某个值;而是创建一个代表更新的新值。
实用层面上,它的好处包括:
并非总是不可变优于可变。在受限的情况下,可变性是合理甚至必要的:
关键规则是:不要让可变结构泄漏到许多组件可以读写的边界之外。
竞争条件通常来自共享且可变的数据被多个工作者先读后写。
不可变性减少了协调的表面积,因为写入者生成新版本而不是编辑共享对象。你仍需一个发布“当前版本”的规则,但数据本身不再是一个随时会变化的目标,从而降低时序相关失败的概率。
把事实视为不可变的追加记录(事件),把“当前状态”看作从这些事实导出的视图。
可以循序渐进地开始,不必全面采用事件溯源:
把信息以清晰、显式的数据(值)存储,然后针对这些数据执行业务逻辑。避免把可执行规则嵌入到持久化记录中。
这样系统更可演化,因为:
挑选一个经常变更的工作流并在下一个冲刺中应用三步:
衡量成功的指标包括更少的间歇性错误、更小的变更影响面,以及发布时更少的“精心协调”。