Object-Oriented Programming (OOP) has long been the cornerstone of software development, promoting encapsulation, modularity, and reuse. However, as Michael Feathers puts it:
"OO makes code understandable by encapsulating moving parts. FP makes code understandable by minimizing moving parts."
This perspective challenges conventional OOP thinking and invites developers to explore how functional programming (FP) principles can enhance their design choices.
But before we dive deeper into this paradigm shift, let’s explore Michael Feathers and his contributions.
Who is Michael Feathers?
Michael Feathers is a well-respected figure in the software development community, known for his expertise in software design, refactoring, and legacy code management.
His book, Working Effectively with Legacy Code, has been a guiding light for developers dealing with complex, inherited codebases.
Feathers’ insights on OOP and FP highlight the balance between encapsulation and immutability, offering a practical approach to managing code complexity.
The Challenge of "Moving Parts"
In OOP, the fundamental idea is to bundle data with the functions that operate on it.
However, a critical issue arises when object methods mutate the internal state of an object.
This mutation introduces unpredictability and makes debugging harder.
For example, consider a typical class with a method that modifies internal state:
class Counter {
constructor() {
this.value = 0;
}
increment() {
this.value++;
}
}
The increment
method modifies this.value
, which can be problematic in concurrent environments or when debugging unexpected state changes.
In contrast, a more functional approach would return a new instance rather than modifying the existing one:
class Counter {
constructor(value = 0) {
this.value = value;
}
increment() {
return new Counter(this.value + 1);
}
}
Here, increment()
does not modify the existing object; instead, it returns a new instance with the updated value.
This design choice enhances predictability and thread safety.
Performance Implications
One of the common concerns with a functional approach is performance.
Avoiding mutation often involves copying data, which can introduce inefficiencies.
Feathers highlights the example of drawing a triangle on a framebuffer:
"You can write a pure DrawTriangle() function that takes a framebuffer as a parameter and returns a completely new framebuffer with the triangle drawn into it. Don’t do that."
In scenarios where performance is critical, direct memory manipulation is optimal.
However, modern multi-threaded applications often benefit from immutability because it enables safer parallel execution.
Rethinking Object-Oriented Design
Feathers suggests that we analyze our codebases to identify mutable state dependencies. Some actionable insights include:
- Track External State: Document every piece of state a function can reach and modify. This practice improves code clarity and maintainability.
- Prefer Pure Functions: Organize computations around pure functions, where inputs determine outputs without modifying shared state.
- Modify Utility Classes: Transition methods from self-mutating to returning new copies where feasible.
-
Emphasize
const
: Useconst
aggressively to prevent unintended mutations and enforce immutability.
Consider the following example where we apply functional principles to list transformations:
// OOP-style mutation
class NamesList {
constructor(names) {
this.names = names;
}
toUpperCase() {
this.names = this.names.map(name => name.toUpperCase());
}
}
// Functional approach
class NamesList {
constructor(names) {
this.names = names;
}
toUpperCase() {
return new NamesList(this.names.map(name => name.toUpperCase()));
}
}
By returning a new instance instead of modifying the existing one, we ensure immutability while maintaining readability.
Final Thoughts
Michael Feathers' perspective bridges the gap between OOP and FP, challenging developers to think critically about state management and program structure.
While OOP encapsulates moving parts, FP seeks to minimize them, reducing the risk of unpredictable behavior.
The key takeaway is not to abandon OOP but to integrate functional paradigms where they offer tangible benefits.
"Maybe if all objects just referenced a read-only version of the world state, and we copied over the updated version at the end of the frame… Hey, wait a minute…"
This realization underscores the evolving nature of software design—where embracing functional principles can lead to more maintainable, robust, and scalable systems.
I’ve been working on a super-convenient tool called LiveAPI.
LiveAPI helps you get all your backend APIs documented in a few minutes
With LiveAPI, you can quickly generate interactive API documentation that allows users to execute APIs directly from the browser.
If you’re tired of manually creating docs for your APIs, this tool might just make your life easier.
Top comments (4)
This solution is so wierd to me:
because this is really fare from Functional thinking, on second example even worst: call the constructor but if constructor prupose is reset the whole object. So instead just make some modification on our datal list, instead we rerender everything and give a different instance of our class, which is a risky move.
That why FP is much clean solution, because don't tight together the data and the functions. Instead giving a bunch of usefull functions to handle our data. At the end when we just using a few of these functions our actual data handling then don't need to bundle unused functions.
That why I prefered the core functional programming.
It's a very interesting point, but I also think there's another bit of design thinking in there. Direct object manipulation is faster in parallel too, presuming that you've structured your data to not have overlaps, or only edge case ones where synchronisation can work well. The issue is that memory allocation can end up being non-parallel anyway, so you can get stalls through constantly creating lots of tiny objects and garbage collecting them in single thread or multi-thread alike.
I always wonder why people are always ranting about mutablity and thread safety. Instead of learning how to structure concurrent/parallel tasks which is super easy now (because it's not 20 years ago), we should shift the paradigm to FP and throw many good aspects of OOP into the trash? Instead of bundling data and behaviours and modeling the object as a state machine that satisfies some predictable contracts, we should decouple data and behaviours for "more" predictability and we should create new version of the data for every single state change to future prove thread safety? No, thanks. This thread safety thing is not why we use FP.
Thanks for sharing information.