在现代 C++ 中桥接面向对象与函数式思维

发布: (2026年4月29日 GMT+8 15:15)
12 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容,我将按照要求将其翻译为简体中文并保留原始的格式、Markdown 语法以及技术术语。谢谢!

依赖如何让 C++ 系统难以测试和演进 —— 而函数式思维可以改变这一点

在软件开发中,抗击复杂性至关重要。然而,在许多真实的 C++ 项目中,系统往往朝相反的方向演进。

  • 紧耦合 – 组件之间耦合紧密,导致更改会波及系统的大部分。
  • 状态分散 – 状态散布在对象图中,降低了对其随时间演变的透明度。
  • 涌现行为 – 行为由众多交互部件产生,使得难以在孤立的情况下推理单个部分。

这些特征同样体现在测试上:仅仅为了让测试工作,就需要大量的样板代码和间接层。建立特定的系统状态需要大量的准备工作,且维护测试成本高,因为即使是小的生产改动也会迫使大量测试代码同步更新。结果是系统难以正确实现——一旦运行起来,又显得僵硬且难以演进。

以测试为中心的视角

从测试的角度审视问题,情况会更加清晰。

  1. 可单元测试的代码 – 我们追求将代码结构化为可以单独执行的更小单元。
  2. 关注点分离 – 为测试提供独立的单元。
  3. 依赖注入 (DI) – 为了独立运行模块,模块的依赖通过构造函数参数等方式传入。

在一个需要单独运行的单元测试中,所有依赖的模块都必须 mock。测试不使用真实依赖,而是把自己的 mock 作为构造函数参数传入。

第一个问题:没有 mock 就没有测试

  • Mock 在技术上可行——单元测试得以开启。
  • 代价高昂:必须编写并维护 mock,导致测试专用代码增多,整体测试复杂度提升。
  • 只有当所有 mock 正确实现时,测试才有意义。

第二个问题:接口导致紧耦合

  • Mock 必须实现与被替换真实组件相同的接口。
  • 接口往往包含多个相互依赖的函数签名,且具有非平凡的前置条件和后置条件。
  • 对接口的任何修改都会影响 所有 实现以及 所有 mock,增加维护工作量。
  • 在 DI 与大量 mock 的情境下,这种基于接口的耦合尤为明显且代价高。
  • 我们最终在可测试性与灵活性之间做了不划算的权衡。

第三个问题:面向对象中的状态处理

  • 被封装在类内部的状态难以在测试中直接修改。
  • 私有可变状态完全隐藏,测试无法直接设置。
  • 为了达到特定的内部配置,测试必须通过公共接口间接驱动状态,通常需要多次方法调用、复杂的调用顺序以及 mock。
  • 这种间接的准备增加了复杂度,使测试变得脆弱。

第四个问题:状态分布

  • 状态通常散落在对象图中,产生难以察觉的隐藏依赖。
  • 状态对象越多,越难推断状态如何演变,进一步加剧前述的测试复杂性。

第五个问题:继承导致的耦合

  • 实现之间通过共享接口产生的紧耦合可以更一般地描述为 继承导致的紧耦合
  • 一旦建立了类层次结构,基类接口几乎不可能在不影响所有派生类的情况下进行修改。
  • 层次越深,问题越严重:即使是微小的调整也会向下传播,使得层次结构僵硬且修改成本高。

对根本原因的总结

所有这些问题共同制造了大量复杂性,进而导致开篇所述的痛点。其根本原因是 依赖

  • 通过注入相互依赖的组件会…(未完)

Source:

only be executed in isolation by introducing mocks.

  • The broader an interface, the more complex the dependency between components sharing it.
  • Inheritance creates dependencies between classes within hierarchies.
  • Hidden, distributed, and evolving state creates implicit dependencies across the system.

Every issue we examined boils down to this: dependencies force parts of the system to change together, to be set up together, and to be understood together. As they reinforce each other, complexity grows increasingly fast.

A different way of structuring code

I started questioning the design ideas I had followed for years and searched for alternative ways to manage these dependencies. The blog post Mocking is Code Smell helped me see the problem more clearly and pushed me to explore functional ideas that turned out to be particularly useful.

Put simply, functional programming offers techniques that can complement a traditional C++ toolbox. These techniques work because they change how we design systems—and with that, which dependencies emerge.

What changes with pure functions?

AspectTraditional OOPFunctional approach
Dependency declaration隐式,通过注入的对象显式,所有依赖都是函数参数
Mocking需要替换注入的对象通常不需要,因为纯函数没有隐藏的协作者
State隐藏、可变、分散在对象中没有隐藏状态;任何状态都显式传递且不可变
Interface size往往很大,涵盖许多职责更窄,只关注单一转换
Inheritance用于共享行为,会引入层次耦合不使用;函数组合取代继承

通过使用纯函数作为构建块:

  • 依赖变为显式——它们作为参数传入,而不是隐藏在对象内部。
  • Mock 需求减少——函数不依赖外部可变协作者,几乎不需要 mock。
  • 状态可见——函数需要的任何状态都直接提供,消除隐藏、分散的状态。
  • 接口保持小——函数签名通常比类接口更窄,使依赖更简单。
  • 没有继承层次——消除了整个类层次结构的耦合问题。

Takeaway

  • 当依赖通过注入、宽接口、继承和隐藏的可变状态累积时,C++ 系统会变得难以测试和演进。
  • 函数式编程技术——尤其是纯函数——重新暴露这些依赖,使其显式、窄化且不可变。
  • 通过围绕纯函数(或类似 std::function、lambda、值语义类型 的函数抽象)重新设计 C++ 代码库的部分,你可以显著降低对 mock 的需求,简化状态处理,打破导致复杂性的紧耦合。

采用这些想法并不意味着完全抛弃 OOP;它意味着 结合两种范式的优势,以控制依赖,使你的 C++ 系统更易于测试、演进和维护。

# Class of Dependencies

In conclusion, when you compose your business logic out of pure functions, dependencies become **explicit, fewer, and simpler**. This directly mitigates complexity.

> **Note:** I am not claiming that functional programming will solve every problem, nor that it can be applied in C++ without limits. There are, however, aspects that are highly valuable when designing C++ systems. I advocate for **complementing OOP with FP where appropriate*

*.

采用不同的代码结构方式需要付出努力,所需的努力取决于你的学习路径。如果你像我一样来自面向对象、C++ 风格的背景,我可以用你的语言来进一步阐明这些技术和设计选择。

我最初的动机并不是写系统设计或函数式编程的文章。我只是想弄清楚如何在实际的 C++ 代码中有效地应用这些思想。经过一段时间的探索并找到可行的技术后,我觉得自然地分享和讨论它们。

funkyposts 博客旨在通过展示现代 C++ 中实用的函数式编程模式,搭建传统 C++ 与函数式编程之间的桥梁。目标是提供具体的方法来改进系统结构——同时立足于真实世界的约束。我也欢迎反馈和不同的观点,以进一步完善这些想法。

下一步是将这些洞见付诸实践——在不重写全部代码的情况下改进现有设计。系列的后续文章将展示具体做法。

系列即将发布的文章

  1. 在现代 C++ 中处理副作用:将纯函数与我们的命令式世界对接
    使用函数式核心–命令式外壳模式来降低依赖。

  2. 当一个外壳不够用时:使用 C++ 中的 Actor 扩展函数式核心–命令式外壳模式
    如何将不断增长的 C++ 系统组织成 actor 驱动的核心–外壳对,以隔离依赖。

本系列的更多文章即将推出。

关于博客

属于 funkyposts 博客——在 C++ 中桥接面向对象与函数式思维。

GitHub Repository – funkyposts (v06)

0 浏览
Back to Blog

相关文章

阅读更多 »