The source code of high-performance databases and systems software is often a masterclass in design patterns. While exploring Redis, I came across several sophisticated implementations of the Strategy Pattern.
Commonly associated with Object-Oriented (OO) languages like C++ or Java, the Strategy Pattern is frequently dismissed as “too high-level” for procedural languages. But can we use it effectively in C? Let’s find out.
What is the Strategy Pattern?
At its core, the Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.
In practical terms: your application often needs to interact with an underlying system that changes based on context. While your core logic (the Orchestrator) remains generic, the code interacting with the dependency needs to handle specific, low-level details. This requires dedicated implementations for each specific use case.
The Problem: The “If-Else” Nightmare
Without this pattern, your business logic becomes littered with bulky if-else or switch blocks to determine which provider to execute. As your application grows, this approach becomes brittle, error-prone, and a maintenance headache.
The Solution: Swapping at Runtime
The Strategy Pattern replaces hardcoded conditionals with a generic interface. Your core code calls this interface without needing to know the underlying implementation. At runtime, you simply “plug in” the specific strategy (e.g., swapping a Stripe payment worker for a PayPal one).
Strategy Pattern at Runtime: C vs. C++
Implementing this in C isn’t as difficult as it sounds. Object-oriented programming is, in many ways, “syntactic sugar” over procedural concepts.
In C++, member functions are bundled inside a class. In C, we achieve the same result using structs and function pointers.
The C++ Approach
In OOP, the this pointer is passed implicitly:
class MyClass {
int value;
public:
void IncrementBy(int inc) {
value += inc;
}
};
int main() {
MyClass obj;
obj.IncrementBy(10); // Implicitly passes &obj
}
The C Equivalent
In reality, obj.IncrementBy(10) is functionally equivalent to IncrementBy(&obj, 10). We can replicate the “object” behavior in C by storing function pointers directly in the struct:
struct MyStruct {
int value;
void (*IncrementBy)(struct MyStruct* obj, int inc);
};
void ActualIncrementBy(struct MyStruct* obj, int inc) {
obj->value += inc;
}
int main() {
struct MyStruct obj;
obj.value = 0;
obj.IncrementBy = ActualIncrementBy; // Assigning the "Strategy"
obj.IncrementBy(&obj, 10);
}
Real-World Example: Redis Socket Handling
Redis uses this approach to handle TCP connections. Depending on the type of socket, the action required when data arrives varies significantly. For example, for ServerSockets, itmust accept new connections while for client sockets, it read/write data from the connection.
Redis uses the aeFileEvent struct as its Strategy interface:
/* File event structure in Redis */
typedef struct aeFileEvent {
int mask; /* AE_READABLE, AE_WRITABLE, etc. */
aeFileProc *rfileProc; // Function pointer for read events
aeFileProc *wfileProc; // Function pointer for write events
void *clientData;
} aeFileEvent;
The core logic in aeProcessEvents simply calls rfileProc. If the socket is a server socket, rfileProc is assigned to connSocketAcceptHandler. If the socket is an established client, rfileProc is assigned to connSocketEventHandler.
This keeps the event loop clean and agnostic of the specific connection logic.
Strategy Pattern at Compile-Time
Interestingly, Redis also utilizes the Strategy Pattern at compile-time using macros. This is particularly useful for cross-platform compatibility.
Redis relies on non-blocking I/O. However, different operating systems provide different APIs for this: epoll for Linux, kqueue for BSD/macOS, and evport for Solaris. Instead of littering the code with if blocks, Redis defines a generic interface (e.g., aeApiCreate, aeApiPoll) and swaps the implementation file during compilation.
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
By including the specific implementation file, the caller always calls aeApiPoll regardless of the OS, but the underlying “Strategy” is determined when the binary is built.
Conclusion
The Strategy Pattern is not just an OOP concept; it is a fundamental architectural tool for decoupling logic from implementation. Whether through function pointers at runtime or macros at compile-time, Redis proves that C is more than capable of handling complex design patterns with elegance and efficiency.