Cluster 2: #5 funções puras, #7 evitar null/booleanos, #12 command query separation
Cluster 2: #5 Pure Functions, #7 Avoid Null/Booleans, #12 Command Query Separation
Introduction
As previously stated: The restriction is that when a function’s behavior depends on invisible state at the time of reading (a global variable, a side effect hidden behind a “query,” a boolean flag whose meaning is not at the call site), reproducing a bug requires reconstructing that entire invisible context — and that is exactly why these bugs consume a disproportionate amount of debugging time compared to bugs in pure code.
正如之前所提到的:当一个函数的行为取决于读取时不可见的状态(全局变量、隐藏在“查询”背后的副作用、含义不在调用处的布尔标志)时,重现一个 Bug 就需要重建整个不可见的上下文——这正是为什么这些 Bug 相比纯代码中的 Bug,会消耗不成比例的调试时间。
The most serious case documented in software engineering literature is the Therac-25 (1985-1987): a radiotherapy machine whose software had a race condition (mutable shared state, timing-dependent) which, combined with the removal of hardware safety interlocks that existed in previous versions, released radiation doses up to 100x higher than intended, killing at least three patients. The report by Nancy Leveson and Clark Turner (1993) points directly to the problem of relying on shared state without isolation.
软件工程文献中记录的最严重案例是 Therac-25(1985-1987):一台放射治疗机器,其软件存在竞态条件(依赖于时序的可变共享状态),再加上移除了先前版本中存在的硬件安全联锁装置,导致辐射剂量释放比预期高出 100 倍,造成至少三名患者死亡。Nancy Leveson 和 Clark Turner (1993) 的报告直接指出了依赖无隔离共享状态所带来的问题。
In a much less tragic but equally didactic record, the Knight Capital collapse in 2012 — a loss of US$460 million in 45 minutes — happened because a repurposed boolean flag (which turned on 8-year-old dead code called “Power Peg”) was accidentally activated in an incomplete deployment: no one could say for sure what that flag really controlled anymore. This is principle #7 in practice: booleans and nulls accumulate ambiguous meaning over time until no one can reason about what they actually turn on or off.
在一个不那么悲剧但同样具有教学意义的记录中,2012 年 Knight Capital 的崩溃(45 分钟内损失 4.6 亿美元)是因为一个被重复利用的布尔标志(开启了 8 年前名为“Power Peg”的死代码)在一次不完整的部署中被意外激活:没人能确定那个标志到底控制着什么。这就是原则 #7 的实践:布尔值和空值(null)随着时间的推移积累了模糊的含义,直到没人能推断出它们到底开启或关闭了什么。
The common thread remains the same: hidden state is the root cause of the most expensive bugs to track. The difference between a bug in clean code and a bug in code with hidden state is the difference between finding a key under the rug and finding a key that was hidden somewhere random in the house three months ago by someone who doesn’t even work here anymore.
共同的线索是一样的:隐藏状态是追踪成本最高的 Bug 的根本原因。干净代码中的 Bug 与隐藏状态代码中的 Bug 之间的区别,就像是在地毯下找钥匙,与找一把三个月前被一个早已离职的人藏在屋里某个随机角落的钥匙之间的区别。
#5 Pure Functions
The foundation is referential transparency (Strachey, 1967): an expression can only be replaced by its value without changing the program’s behavior if it does not depend on or alter anything outside of it. Backus, in his 1977 Turing Award lecture, calls the opposite style (shared state mutation, von Neumann architecture style) a generator of a huge conceptual distance between the problem and the code — each pure function eliminates this distance because it can be understood and tested in isolation from the rest of the universe.
其基础是引用透明性(Strachey, 1967):如果一个表达式不依赖于也不改变其外部的任何事物,那么它就可以在不改变程序行为的情况下被其值所替换。Backus 在 1977 年的图灵奖演讲中,将相反的风格(共享状态变异,冯·诺依曼架构风格)称为在问题与代码之间产生巨大概念距离的源头——每个纯函数都消除了这种距离,因为它可以脱离宇宙的其他部分被理解和测试。
Why this matters in practice: a pure function is testable without mocks, without setup, without execution order. You call it with the same arguments and always receive the same result. An impure function like CreateOrder(items) in the example hides two sources of variation (Guid.NewGuid(), DateTime.Now) inside the business rule — this means that testing this function requires either accepting non-determinism in the test or wrapping/mocking static system calls, which is a sign that impurity has leaked into something that should be pure logic.
在实践中,这为何重要:纯函数无需 Mock、无需设置、无需考虑执行顺序即可测试。你用相同的参数调用它,总是得到相同的结果。像示例中 CreateOrder(items) 这样的不纯函数,在业务规则内部隐藏了两个变体来源(Guid.NewGuid(),DateTime.Now)——这意味着测试该函数要么需要接受测试中的非确定性,要么需要对静态系统调用进行包装/Mock,这表明不纯性已经渗透到了本应是纯逻辑的地方。
The fix is not just “passing as a parameter” for aesthetics — it’s about where the non-deterministic decision should live. Guid.NewGuid() and DateTime.Now are infrastructure decisions (which ID to use, which clock to consult), not business decisions. Pushing this to the edge (controller, handler, whatever) means the business rule (CreateOrder) remains free of anything that isn’t pure domain logic.
修复方法不仅仅是为了美观而“作为参数传递”——而是关于非确定性决策应该存在于何处。Guid.NewGuid() 和 DateTime.Now 是基础设施决策(使用什么 ID,查询什么时钟),而不是业务决策。将其推向边缘(控制器、处理器等)意味着业务规则(CreateOrder)不再包含任何非纯领域逻辑的内容。
// Impure — Guid.NewGuid() and DateTime.Now hidden inside the business rule
Order CreateOrder(List<Item> items) {
return new Order(Guid.NewGuid(), items, DateTime.Now);
}
// Pure — id and date enter as parameters; the edge (e.g., controller) generates these values
Order CreateOrder(List<Item> items, Guid id, DateTime createdAt) {
return new Order(id, items, createdAt);
}
#7 Avoid Null/Booleans
The article connects this directly to the Knight Capital case: a repurposed boolean flag activated 8-year-old dead code, and no one knew for sure what that flag controlled anymore. This is not about “boolean is bad.” It is about the following: a boolean only carries one bit of information, but the meaning of that bit needs to be reconstructed mentally by whoever reads the code, and this meaning tends to degrade over time — the original flag had a clear purpose, but after years of repurposing, no one can say for sure what turning that bit on/off actually does in the system as a whole today.
文章将此直接与 Knight Capital 案例联系起来:一个被重复利用的布尔标志激活了 8 年前的死代码,而没人再确定该标志控制着什么。这并不是说“布尔值很糟糕”。而是关于以下几点:布尔值只携带一位信息,但该位的含义需要阅读代码的人在脑海中重建,而且这种含义往往会随着时间的推移而退化——最初的标志有明确的目的,但经过多年的重复利用,今天没人能确定开启/关闭该位在整个系统中到底做了什么。
With null, the problem is analogous but with an extra variant: null usually means simultaneously “we don’t have this value yet,” “this value does not apply,” and “error fetching this value” — three completely different situations, compressed into the same representation. Whoever reads if (cliente.Endereco == null) doesn’t know which of the three cases they are handling, and the compiler doesn’t help differentiate.
对于 null,问题是类似的,但多了一个变体:null 通常同时意味着“我们还没有这个值”、“此值不适用”和“获取此值时出错”——三种完全不同的情况被压缩成了同一种表示。阅读 if (cliente.Endereco == null) 的人不知道他们正在处理哪种情况,编译器也无法帮助区分。
// Ambiguous — null can mean "not processed", "not applicable" or "error"
public decimal? DesconTotalAplicado { get; set; }
if (pedido.DesconTotalAplicado == null) {
// Not calculated yet? Order without discount? Calculation failure?
// Reading this if doesn't answer that.
}
// Explicit — each state is a named case
public enum StatusDesconto { NaoCalculado, SemDesconto, ErroNoCalculo, Aplicado }
public record ResultadoDesconto(StatusDesconto Status, decimal Valor);
if (pedido.Desconto.Status == StatusDesconto.NaoCalculado) {
// Now there is no ambiguity — the call site already says what is happening
}
// Dangerous — generic flag that any future feature can repurpose
public bool ModoAlternativo { get; set; }
if (config.ModoAlternativo) {
ExecutarFluxoLegado();
// In 2 years: will someone reactivate this thinking it's something else?
}
// Explicit — each mode has a name and purpose documented in the type itself
public enum ModoExecucao { Padrao, ReprocessamentoLegado, MigracaoDados }