Why functional programming got me — and the design changes that followed
C++ development is still heavily shaped by object-oriented thinking. In real-world projects, this can create friction—especially around testing and tight coupling. At the same time, modern C++ has evolved into a multi-paradigm language with increasingly strong support for functional programming. But having the tools doesn’t automatically change how we design systems. In this post, I will explore what actually changes when you start combining object-oriented and functional thinking in C++, based on the experiences that led me here.
My Early Days
In the first years of my career I aimed to master OOP as provided by C++98. What I knew about programming at that time brought me to the conclusion that writing high quality code is just a matter of being fluent in the language, the object-oriented paradigm and the design patterns around it. So, I was practicing, and as the years passed and my ability to write idiomatic object-oriented code improved I began to see its limits.
To better understand my journey here, let me share another complementary learning path of mine that is about testing. The first embedded projects I joined didn’t have any automated tests. Instead, testing was done manually by using the device, observing its behavior and pressing its buttons. We developers did this ourselves to see if our changes worked and a dedicated test department did so all day to “ensure” the device generally behaves as expected. In a later embedded project two colleagues were practicing unit testing for their production code. Given how many regressions we had experienced and how much effort it took to fix them in coordination with the test department, I realized the potential of automated tests. So, I started writing unit tests myself. At that time, the OOP patterns I was applying became limiting.
Where I Struggle with OOP
When diving into unit testing, an essential learning was: If you want to unit test your code, you actually must write your code to be unit-testable. In short, you must be able to execute smaller parts of your code in isolation. That is applying the “separation of concerns” principle, which provides the units for testing. And to actually run modules independently, dependency injection comes into play. This means if one module depends on another, it is passed in, normally as a constructor argument. In a unit test where a single module should run in isolation, all dependent modules need to be mocked. Instead of real dependencies, the test passes its mocks as constructor arguments.
No testing without mocking: Technically that mocking approach works well; unit testing is enabled. But actually it comes at a high price. The mocks need to be written and maintained, which increases test-specific code and in turn leads to higher test complexity. And we should not neglect this, as only if all the mocks are implemented correctly, the test is helpful.
Interfaces create tight coupling between alternative implementations: Mocks must implement the same interface as the real components they replace. These interfaces often contain several interdependent function signatures with non-trivial pre- and post-conditions. Any change affects all implementations and thus also all mocks, which increases maintenance effort. With dependency injection and many mocks, this interface-level coupling becomes especially visible and expensive. So, we often trade testability for flexibility here, which can be a poor deal.
State encapsulated within a class is hard to modify by a test: In traditional OOP, private mutable state is fully hidden, so tests cannot set it up directly. A test that needs the object in a specific internal configuration must drive the state there indirectly through the public interface, which often requires multiple method calls, complex sequences, and mocks. This indirect setup adds unnecessary complexity and makes tests more brittle.
Kudos to Eric Elliott who wrote the blog post Mocking is Code Smell that helped me gain a better understanding of these issues and how functional programming allows us to do better. This really got me. My interest in functional programming was born.
Looking at OOP from an FP perspective also made another long-standing issue clear: inheritance.
Inheritance creates tight coupling within the hierarchy: Once a class hierarchy is established, the base class interface becomes almost impossible to change without impacting every derived class. Even small adjustments propagate through the hierarchy, making class hierarchies rigid and expensive to evolve.
So, these were my pain points, and I was primed to find solutions in functional programming and I did.
What Functional Thinking Unlocks
The key idea is to start thinking in terms of pure functions alongside classes. A pure function is as simple as it gets: it takes input and returns output, with no hidden state and no implicit dependencies. Using pure functions as building blocks unlocks an additional solution space. For example, the issues described above evolve as follows:
Testability improves: This makes testing straightforward, you call the function with test data and check the result. No hidden state means no complicated setup, and no implicit dependencies mean you don’t need objects standing in for other objects. You may need to supply a function as an argument when testing. But this “mock” is just a single, stateless function, not an entire class with multiple methods and internal state. The complexity drops dramatically. So the takeaway is this: when you structure your logic as pure functions, the heavy mocking simply disappears.
Coupling decreases: Of course, class hierarchy coupling also goes away with FP, functionality still builds on top of other functionality, but when this essentially means composing pure functions, all dependencies are only at the function signature level which again reduces complexity.
In other words, when you compose your business logic out of pure functions, the design becomes cleaner, improving both testability and maintainability. Don't get me wrong, this is not about replacing OOP with FP, it's about complementing where appropriate.
If This Resonates
Sure thing, learning a new programming paradigm takes sustained effort, and how much depends on your learning path. If you’re coming from an OOP-heavy, C++-style background like I did, I might speak your language well enough to make functional programming feel more approachable. My original motivation wasn’t to blog about functional programming. I simply wanted to figure out how to apply its ideas effectively in real-world C++ code. After exploring this for a while and finding techniques that work for me, it feels natural to share and discuss them. The funkyposts blog is my attempt to build a bridge between traditional C++ and functional programming by illustrating practical FP patterns in modern C++. Hopefully this helps other C++ developers go more functional while inviting constructive feedback on my own understanding.
Dive deeper
As said, I advocate for implementing the business logic, the heart of an application in a functional style, while utilizing traditional C++ for everything else. The subsequent posts in this series will show how to structure an application accordingly.
- Interfacing Pure Functions with Our Impure World — How to structure applications using the functional core–imperative shell architecture
- More posts in this series coming soon
Part of the funkyposts blog — blogging to bridge traditional C++ and functional programming by exploring how functional patterns and architectural ideas can be applied in modern C++. Created with AI assistance for brainstorming and improving formulation. Original and canonical source: https://github.com/mahush/funkyposts (v05)
Top comments (0)