The first part of this article series on bufferevent introduced its use of the Strategy Pattern, which enables polymorphic behavior through the struct bufferevent_ops
.
The Strategy Pattern
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from the clients that use it.
The Strategy Pattern consists of the following key roles:
- Context: Maintains a reference to a Strategy object and uses it to execute an algorithm.
- Strategy: An interface common to all supported algorithms or behaviors.
- Concrete Strategies: Classes that implement the Strategy interface, providing specific algorithm implementations.
In embedded systems, this pattern is particularly useful for:
- Switching communication protocols: Dynamically switch between UART, I2C, and SPI.
- Sensor data processing: Apply different processing strategies for various sensor types, such as temperature, humidity, or light sensors.
- Power management: Change power consumption strategies based on the power source (e.g., battery or external power).
- Control algorithms: Dynamically switch between different control algorithms like PID or fuzzy logic control.
The following table compares designs with and without the Strategy Pattern:
Feature | With Strategy Pattern | Without Strategy Pattern |
---|---|---|
Algorithm Extensibility | New strategies can be added without modifying the context. | Requires modifying core logic. |
Conditional Logic | Eliminates complex switch/case statements. | Leads to numerous conditional branches. |
Code Reusability | Strategy objects can be reused in different contexts. | Algorithm logic is tightly coupled with its use case. |
Runtime Flexibility | Allows dynamic algorithm switching. | Behavior is determined at compile time. |
Testing Complexity | Strategy objects can be tested independently. | Requires simulating the entire context environment. |
Embedded Application Example
This example demonstrates using different strategies for processing data from different sensor types (temperature and humidity).
C Implementation
In C, the Strategy Pattern can be implemented using function pointers to represent different strategies.
1. Strategy Interface and Concrete Strategies
The strategy interface is a function pointer type. Each concrete strategy is a function that matches this signature.
// Strategy Interface
typedef void (*SensorStrategy)(void* data);
// Concrete Strategy: Temperature Sensor
void temperature_strategy(void* data) {
float* temp = (float*)data;
printf("[C] Processing temperature: %.1fC -> Calibrated: %.1fC\n", *temp, *temp + 0.5f);
}
// Concrete Strategy: Humidity Sensor
void humidity_strategy(void* data) {
int* humidity = (int*)data;
printf("[C] Processing humidity: %d%% -> Adjusted: %d%%\n", *humidity, *humidity + 2);
}
2. Context
The context holds a pointer to the current strategy and the data to be processed.
// Context: Sensor Processor
typedef struct {
SensorStrategy strategy;
void* sensor_data;
} SensorProcessor;
3. Complete Example in C
#include <stdio.h>
// Strategy Interface
typedef void (*SensorStrategy)(void* data);
// Concrete Strategy: Temperature Sensor
void temperature_strategy(void* data) {
float* temp = (float*)data;
printf("[C] Processing temperature: %.1fC -> Calibrated: %.1fC\n", *temp, *temp + 0.5f);
}
// Concrete Strategy: Humidity Sensor
void humidity_strategy(void* data) {
int* humidity = (int*)data;
printf("[C] Processing humidity: %d%% -> Adjusted: %d%%\n", *humidity, *humidity + 2);
}
// Context: Sensor Processor
typedef struct {
SensorStrategy strategy;
void* sensor_data;
} SensorProcessor;
void process_sensor(SensorProcessor* processor) {
processor->strategy(processor->sensor_data);
}
int main(void) {
printf("\n--- Strategy Pattern Demo ---\n");
float temp_data = 25.3f;
int humidity_data = 45;
SensorProcessor temp_processor = {temperature_strategy, &temp_data};
SensorProcessor hum_processor = {humidity_strategy, &humidity_data};
process_sensor(&temp_processor);
process_sensor(&hum_processor);
return 0;
}
C++ Implementation
In C++, the pattern is typically implemented using a common interface (an abstract base class) and concrete classes that inherit from it.
#include <iostream>
#include <memory>
// Strategy Interface
class SensorStrategy {
public:
virtual void process() = 0;
virtual ~SensorStrategy() = default;
};
// Concrete Strategy: Temperature
class TemperatureStrategy : public SensorStrategy {
public:
explicit TemperatureStrategy(float temp) : temp_(temp) {}
void process() override {
std::cout << "Processing temperature: " << temp_ << "C -> Calibrated: " << temp_ + 0.5f << "C\n";
}
private:
float temp_;
};
// Concrete Strategy: Humidity
class HumidityStrategy : public SensorStrategy {
public:
explicit HumidityStrategy(int humidity) : humidity_(humidity) {}
void process() override {
std::cout << "Processing humidity: " << humidity_ << "% -> Adjusted: " << humidity_ + 2 << "%\n";
}
private:
int humidity_;
};
// Context: Sensor Processor
class SensorProcessor {
public:
void set_strategy(std::unique_ptr<SensorStrategy> strategy) {
strategy_ = std::move(strategy);
}
void process_sensor() {
if(strategy_) strategy_->process();
}
private:
std::unique_ptr<SensorStrategy> strategy_;
};
int main(void) {
std::cout << "\n--- Strategy Pattern Demo ---\n";
SensorProcessor processor;
processor.set_strategy(std::make_unique<TemperatureStrategy>(25.3f));
processor.process_sensor();
processor.set_strategy(std::make_unique<HumidityStrategy>(45));
processor.process_sensor();
return 0;
}
Pros and Cons
Advantages
- Open/Closed Principle: New strategies can be introduced without modifying the context.
- Eliminates Conditional Logic: Replaces complex if-else or switch statements, leading to cleaner code.
- Separation of Concerns: Decouples the algorithm implementation from the client logic that uses it.
Disadvantages
- Increased Object Count: Each strategy is a separate class or object, which can increase the complexity of the program.
- Client Overhead: The client must be aware of the different strategies and choose the appropriate one.
- Risk of Over-engineering: The pattern can be overly complex for simple algorithms that are unlikely to change.