简化你的代码:函数式核心,命令式外壳
Simplify your code: Functional core, imperative shell

原始链接: https://testing.googleblog.com/2025/10/simplify-your-code-functional-core.html

## 函数核心与命令式外壳:总结 将业务逻辑与副作用(如数据库调用或电子邮件)混合在一起会产生混乱且难以管理的 代码。更好的方法是使用**函数核心**和**命令式外壳**来分离关注点。 **函数核心**包含*纯粹*的逻辑——可测试的函数仅对输入数据进行操作,没有副作用。这个核心关注于*需要做什么*。 **命令式外壳**处理副作用——与外部世界(数据库、网络等)交互。它*调用*核心内的函数来执行逻辑。这个外壳关注于*如何做*。 这种分离提高了可测试性(核心逻辑可以独立测试)、可维护性和适应性。可以通过简单地创建新的核心函数,重用现有的函数来添加新功能,而无需修改外壳与外部系统的交互。示例演示了重构电子邮件发送逻辑来说明这种模式。

## 功能核心,命令式外壳:摘要 这次Hacker News讨论围绕着软件设计原则,即将“功能核心”(纯粹、可测试的逻辑)与“命令式外壳”(处理副作用的代码,如I/O和外部交互)分离。这个想法,被Destroy All Software等文章推广,旨在提高代码质量、可测试性和可维护性。 许多评论者强调这并非新概念,其根源在于较早的编程范式。虽然承认其益处,讨论也强调了实际挑战。担忧包括过度抽象的潜力、在复杂系统中(如事务管理)清晰分离关注点的难度,以及如果实施不当可能导致的性能问题。 一个关键点是,“功能性”方面并非严格遵守函数式编程,而是关于隔离核心逻辑以便于测试和推理。多位用户提倡将数据库过滤和其他操作*推入*数据库层,以避免将不必要的数据加载到内存中。最终,讨论强调了在实际软件开发中,在理论理想与务实考虑之间找到平衡的重要性。
相关文章

原文

Is your code a tangled mess of business logic and side effects? Mixing database calls, network requests, and other external interactions directly with your core logic can lead to code that’s difficult to test, reuse, and understand. Instead, consider writing a functional core that’s called from an imperativ​​e shell.

Separating your code into functional cores and imperative shells makes it more testable, maintainable, and adaptable. The core logic can be tested in isolation, and the imperati​​ve shell can be swapped out or modified as needed. Here’s some messy example code that mixes logic and side effects to send expiration notification emails to users:

// Bad: Logic and side effects are mixed

function sendUserExpiryEmail(): void {

  for (const user of db.getUsers()) {

    if (user.subscriptionEndDate > Date.now()) continue;

    if (user.isFreeTrial) continue;

    email.send(user.email, "Your account has expired " + user.name + “.”);

  }

}

A functional core should contain pure, testable business logic, which is free of side effects (such as I/O or external state mutation). It operates only on the data it is given.

An imperative shell is responsible for side effects, like database calls and sending emails. It uses the functions in your functional core to perform the business logic.

Rewriting the above code to follow the functional core / imperative shell pattern might look like:

Functional core

function getExpiredUsers(users: User[], cutoff: Date): User[] {

  return users.filter(user => user.subscriptionEndDate <= cutoff && !user.isFreeTrial);

}

function generateExpiryEmails(users: User[]): Array<[string, string]> {

  return users.map(user => 

    ([user.email, “Your account has expired “ + user.name + “.”])

  );

}

Imperative shell

email.bulkSend(generateExpiryEmails(getExpiredUsers(db.getUsers(), Date.now())));

Now that the code is following this pattern, adding a feature to send a new type of email is as simple as writing a new pure function and reusing getExpiredUsers:

// Sending a reminder email to users

function generateReminderEmails(users: User[], cutoff: Date): Array<[string, string]> {...}

const fiveDaysFromNow = ...

email.bulkSend(generateReminderEmails(getExpiredUsers(db.getUsers(), fiveDaysFromNow)));

联系我们 contact @ memedata.com