确定性核心,非确定性外壳
Deterministic Core, Non-Deterministic Shell

原始链接: https://outdata.net/blog/260803

Gary Bernhardt 提出的“函数式核心,命令式外壳”(Functional Core, Imperative Shell)架构通过将业务逻辑(纯粹的、确定性的函数)与副作用繁多的 I/O(命令式外壳)分离开来,从而提升了代码的可测试性。 本文建议将这一概念扩展为“**确定性核心,非确定性外壳**”。虽然函数式纯度是实现确定性的理想方案,但状态机在命令式环境中也能达到同样的可重复行为。确定性意味着在给定相同输入的情况下,系统会产生相同的结果。非确定性的来源——例如网络调用、系统时钟和数据库交互——应被隔离在“外壳”中,由“核心”负责处理逻辑。 对于处理遗留代码库的开发者,作者建议采用“**碎片整理**”的过程。开发者不应追求单一、完美的架构,而应逐步识别并隔离现有代码中隐藏的确定性逻辑片段(如状态机)。通过将这些确定性片段归拢在一起,开发者可以缩小“难以测试”的范围。即使是在混乱的系统中,将这些确定性核心显现出来,也能使软件更易于测试、修改和维护,从而随着时间的推移逐步提高可靠性。

Hacker News 最新 | 往期 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 确定性核心,非确定性 Shell ( outdata.net ) 7 点 由 brandon_bot 发布于 1 小时前 | 隐藏 | 往期 | 收藏 | 1 条评论 帮助 Founderarcstone 1 小时前 | 下一条 [–] 干得好,这个世界需要更多这样的东西! 回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系方式 搜索:
相关文章

原文

3 Aug 2026

Fourteen years ago, Gary Bernhardt coined the term Functional Core, Imperative Shell . Like most good ideas in computing it was not entirely new, but his conception had great clarity, and it forms an excellent basis for talking about testing and determinism in existing systems.

Briefly, Functional Core/Imperative Shell architecture divides the code into two parts. The Functional Core is purely functional - that is no IO, and no destructive state updates. It is concerned with the business logic of the application. The Imperative Shell has comparatively little pathing, but maintains state, coordinates external dependencies, and deals with the outside world - that is to say IO. Its job is to query the core with values, receive values back as the result of some blackbox decision, and use that to interact with the outside world; whether that's writing to a database, sending a request, or updating a GUI.

The Shell and the Core in this model have distinct characteristics:

Core Shell
Makes decisions Coordinates dependencies
Many branching execution paths More linear execution
Isolated from the world Integrates with the world

This makes the core very amenable to testing. Since it's purely functional, the same inputs will always get the same results. Since it's isolated, there is nothing to mock or stub. And since it handles complex business logic, the tests can tell us a lot about how the system behaves.

Functional Purity and Determinism

A shorter way of describing the properties that make pure functions amenable to testing is that they are deterministic. That is - given a stream of inputs, a pure function always returns the same stream of outputs; their behaviour is repeatable. But pure functional programming is not the only way to get there. If we tilt our heads a little we can see that a stream of values and a sequence of assignments are different ways of expressing the same thing, and State Machines can bring us the same benefits. Consider the following code:

function add(ns) {
 return ns.reduce((a, b) => a + b, 0)
}

class AddMachine {
  #state = 0

  transition(input) {
    this.#state += input
  }

  get state() {
    return this.#state
  }
}

The function add is easy to reason about; it's pure and thus deterministic. But the AddMachine is also deterministic - given the same sequence of calls to the transition function, AddMachine will return the same state. It being imperative does not change that.

const output = add([1, 2, 3]) 
const a = new AddMachine()
a.transition(1)
a.transition(2)
a.transition(3)

const output = a.state 

Pure functional programming is a fine paradigm, but due to language or performance considerations, it is not always practical - I would not want to try it in C! But weakening the requirements from purely functional to merely deterministic, we retain the testability benefits of "Functional Core, Imperative Shell", while broadening its applicability. And so the title of this post: Deterministic Core, Non-Deterministic Shell.

Determinism can feel like a more abstract concept than functional purity. How do you know it when you see it? I find it's easier to start with what is not deterministic and work backwards. Here are some common examples of non-repeatable behaviour:

  • Calling RNGs that aren't seeded
  • Asynchronous and multi-threaded operations
  • Communication over the network
  • Communication with other processes
  • Reading/Writing to local storage
  • Database interactions
  • Asking the OS for the date or time

All these belong in the non-deterministic shell. Whenever you find them in your business logic, you have a natural target for defragmentation - either splitting the function in two around them, or lifting them up a layer and injecting their result as a parameter. It's illustrative to think of the "shell" metaphor quite literally; it should surround the logic, querying the heart of the application to get what it needs.

Working with what you have

"This is all well and good", you might think, "but what use of it is to me, toiling away in the legacy & vibe-code mines of industry?". A fair accusation, imaginary reader; not everyone can be Foundation DB and make that distinction from day one (they actually went a step further, but that's a topic for another post). Determinism and non-determinism are highly entwined in almost every real life codebase I have seen, and I've seen my fair share.

But don't let perfect be the enemy of good! One way to think of your average (ie, terrible) codebase is that it has many deterministic cores. There are thousands, strewn through the slop as stars in the sky. The glass half empty take is these codebases are an irredeemable legacy mess. But glass half full is that there are many deterministic cores hidden somewhere inside, and maybe only a handful.

Users of older Windows systems may remember the "Disk Defragmenter"; it took files whose contents were scattered physically across the spinning hard disk and made them contiguous. In an era where read speed depended on physical distance on the media, this mattered a lot.

Disk Defragmenter tool in Windows XP
There was something so satisfying about seeing the red segments slowly give way to the blue.

So one gradual approach for existing code is to practice the Defragmentation of Determinism. Identify it wherever you can - files, classes, even a few lines in individual functions - and start collecting them. The more determinism that can be grouped, the more easily testable functionality you have, and the more you can feel confident about the behaviour and reliability of the program as a whole. The surface area for "hard to test" (non-deterministic code) starts to shrink. On a large enough codebase you will likely never get to a single deterministic core, but even hundreds is better than thousands.

Unleash the State Machine Within!

Every nasty mess of a codebase I've seen has one or more much nicer deterministic state machines locked inside. I promise you they are there, even if it's not obvious. And once you find them, you'll be delighted with how much easier the software is to modify and test. Piece by piece, reliability can be wrought.

联系我们 contact @ memedata.com