Implementing the Factory Pattern in Embedded Systems: Elegantly Creating Objects with C Language

Introduction

In embedded development, have you ever written code like this: creating different objects based on various hardware configurations, resulting in code filled with complex conditional statements?

// Confusing object creation logic
void* create_sensor(int sensor_type) {
    if (sensor_type == TEMP_SENSOR) {
        if (hardware_version == V1) {
            return create_temp_sensor_v1();
        } else if (hardware_version == V2) {
            return create_temp_sensor_v2();
        } else {
            return create_temp_sensor_v3();
        }
    } else if (sensor_type == HUMIDITY_SENSOR) {
        if (interface_type == I2C) {
            return create_humidity_i2c();
        } else if (interface_type == SPI) {
            return create_humidity_spi();
        }
    } else if (sensor_type == LIGHT_SENSOR) {
        // More complex checks...
    }
    return NULL;
}

This code is not only hard to maintain but also violates the Open/Closed Principle. Every time a new sensor is added, this function must be modified. Today, we will learn about the Factory Pattern, which can make object creation elegant, flexible, and easy to extend.

What is the Factory Pattern?

The Factory Pattern is a creational design pattern that provides an interface for creating objects, but it is the subclasses that determine which class to instantiate. The Factory Pattern defers the instantiation of classes to subclasses.

Factory

+create_product(type) : Product

ConcreteFactoryA

+create_product(type) : ProductA

ConcreteFactoryB

+create_product(type) : ProductB

Product

<>

+operation()

ProductA

+operation()

ProductB

+operation()

Core Ideas:

  • Decoupled Creation Process: The client does not need to know how to create objects.
  • Unified Interface: All products adhere to the same interface specification.
  • Easy to Extend: Adding a new product type does not require modifying existing code.

Why Do Embedded Systems Need the Factory Pattern?

Embedded systems have the following characteristics that make the Factory Pattern particularly suitable:

Diversity of Hardware

The same functionality may correspond to multiple hardware implementations, such as different models of sensors and communication modules with different interfaces.

Modular Design

Embedded systems typically adopt a modular design, requiring loose coupling between modules.

Code Reusability Needs

In resource-constrained environments, code reuse and maintainability become even more important.

Configuration Flexibility

It is necessary to dynamically select the appropriate implementation based on different hardware configurations.

Implementing the Factory Pattern in C Language

Since C language does not have object-oriented features, we simulate the Factory Pattern using function pointers and structures.

Basic Implementation Framework

// Product interface (using function pointers)
typedef struct {
    int (*init)(void);
    int (*read_data)(float *data);
    int (*deinit)(void);
} ProductInterface;

// Factory function type definition
typedef ProductInterface* (*FactoryFunction)(void);

// Factory registry
typedef struct {
    const char* type_name;
    FactoryFunction create_func;
} FactoryRegistry;

// General factory function
ProductInterface* create_product(const char* type);

The core of this framework is:

  1. 1. ProductInterface: Defines a unified interface for all products.
  2. 2. FactoryFunction: Type definition for factory functions.
  3. 3. FactoryRegistry: Factory registry that stores type names and corresponding creation functions.

Four Classic Application Scenarios

Scenario 1: Sensor Management Factory

In IoT devices, multiple sensors need to be supported, each with different initialization methods and data reading approaches.

Sensor Factory
Temperature Sensor
Humidity Sensor
Light Sensor
DHT22
DS18B20
SHT30
AM2302
BH1750
TSL2561

Code Implementation:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// Sensor interface definition
typedef struct {
    int (*init)(void);
    int (*read_data)(float *data);
    int (*deinit)(void);
    const char* name;
} SensorInterface;

// Temperature sensor implementation
static int temp_sensor_init(void) {
    printf("Temperature sensor initialized\n");
    return 0;
}

static int temp_sensor_read(float *data) {
    *data = 25.5f;  // Simulate reading temperature
    printf("Read temperature: %.1f°C\n", *data);
    return 0;
}

static int temp_sensor_deinit(void) {
    printf("Temperature sensor deinitialized\n");
    return 0;
}

// Humidity sensor implementation
static int humidity_sensor_init(void) {
    printf("Humidity sensor initialized\n");
    return 0;
}

static int humidity_sensor_read(float *data) {
    *data = 60.0f;  // Simulate reading humidity
    printf("Read humidity: %.1f%%\n", *data);
    return 0;
}

static int humidity_sensor_deinit(void) {
    printf("Humidity sensor deinitialized\n");
    return 0;
}

// Light sensor implementation
static int light_sensor_init(void) {
    printf("Light sensor initialized\n");
    return 0;
}

static int light_sensor_read(float *data) {
    *data = 1200.0f;  // Simulate reading light intensity
    printf("Read light: %.0f Lux\n", *data);
    return 0;
}

static int light_sensor_deinit(void) {
    printf("Light sensor deinitialized\n");
    return 0;
}

// Specific sensor objects
static SensorInterface temp_sensor = {
    .init = temp_sensor_init,
    .read_data = temp_sensor_read,
    .deinit = temp_sensor_deinit,
    .name = "Temperature Sensor"
};

static SensorInterface humidity_sensor = {
    .init = humidity_sensor_init,
    .read_data = humidity_sensor_read,
    .deinit = humidity_sensor_deinit,
    .name = "Humidity Sensor"
};

static SensorInterface light_sensor = {
    .init = light_sensor_init,
    .read_data = light_sensor_read,
    .deinit = light_sensor_deinit,
    .name = "Light Sensor"
};

// Sensor factory
SensorInterface* sensor_factory(const char* sensor_type) {
    if (strcmp(sensor_type, "temperature") == 0) {
        return &temp_sensor;
    } else if (strcmp(sensor_type, "humidity") == 0) {
        return &humidity_sensor;
    } else if (strcmp(sensor_type, "light") == 0) {
        return &light_sensor;
    }
    return NULL;
}

// Usage example
void sensor_demo(void) {
    const char* sensor_types[] = {"temperature", "humidity", "light"};
    int num_sensors = sizeof(sensor_types) / sizeof(sensor_types[0]);

    for (int i = 0; i < num_sensors; i++) {
        SensorInterface* sensor = sensor_factory(sensor_types[i]);
        if (sensor) {
            sensor->init();
            float data;
            sensor->read_data(&data);
            sensor->deinit();
            printf("---\n");
        }
    }
}

Scenario 2: Protocol Parser Factory

Embedded devices often need to support multiple communication protocols, and the Factory Pattern can create the corresponding parser based on the protocol type.

Parser Protocol Factory
Client
Request to create parser("UART")
Create UART parser
Return parser instance
Return parser
Call parse_data()
Return parsing result

Code Implementation:

// Protocol parser interface
typedef struct {
    int (*init)(void);
    int (*parse_data)(const uint8_t* data, size_t len, void* result);
    int (*deinit)(void);
    const char* protocol_name;
} ProtocolParser;

// UART protocol parser
static int uart_parser_init(void) {
    printf("UART parser initialized\n");
    return 0;
}

static int uart_parser_parse(const uint8_t* data, size_t len, void* result) {
    printf("UART protocol parsing: data length=%zu\n", len);
    // Implement specific parsing logic for UART protocol
    *(int*)result = 0x55;  // Simulate parsing result
    return 0;
}

static int uart_parser_deinit(void) {
    printf("UART parser deinitialized\n");
    return 0;
}

// SPI protocol parser
static int spi_parser_init(void) {
    printf("SPI parser initialized\n");
    return 0;
}

static int spi_parser_parse(const uint8_t* data, size_t len, void* result) {
    printf("SPI protocol parsing: data length=%zu\n", len);
    *(int*)result = 0xAA;  // Simulate parsing result
    return 0;
}

static int spi_parser_deinit(void) {
    printf("SPI parser deinitialized\n");
    return 0;
}

// I2C protocol parser
static int i2c_parser_init(void) {
    printf("I2C parser initialized\n");
    return 0;
}

static int i2c_parser_parse(const uint8_t* data, size_t len, void* result) {
    printf("I2C protocol parsing: data length=%zu\n", len);
    *(int*)result = 0xFF;  // Simulate parsing result
    return 0;
}

static int i2c_parser_deinit(void) {
    printf("I2C parser deinitialized\n");
    return 0;
}

// Protocol parser objects
static ProtocolParser uart_parser = {
    .init = uart_parser_init,
    .parse_data = uart_parser_parse,
    .deinit = uart_parser_deinit,
    .protocol_name = "UART"
};

static ProtocolParser spi_parser = {
    .init = spi_parser_init,
    .parse_data = spi_parser_parse,
    .deinit = spi_parser_deinit,
    .protocol_name = "SPI"
};

static ProtocolParser i2c_parser = {
    .init = i2c_parser_init,
    .parse_data = i2c_parser_parse,
    .deinit = i2c_parser_deinit,
    .protocol_name = "I2C"
};

// Protocol parser factory
ProtocolParser* protocol_parser_factory(const char* protocol_type) {
    if (strcmp(protocol_type, "UART") == 0) {
        return &uart_parser;
    } else if (strcmp(protocol_type, "SPI") == 0) {
        return &spi_parser;
    } else if (strcmp(protocol_type, "I2C") == 0) {
        return &i2c_parser;
    }
    return NULL;
}

Scenario 3: Device Driver Factory

Different peripherals require different drivers, and the Factory Pattern can create the corresponding driver based on the device type.

// Device driver interface
typedef struct {
    int (*init)(void);
    int (*write)(const void* data, size_t len);
    int (*read)(void* data, size_t len);
    int (*control)(int cmd, void* arg);
    int (*deinit)(void);
    const char* device_name;
} DeviceDriver;

// LED driver implementation
static int led_driver_init(void) {
    printf("LED driver initialized\n");
    return 0;
}

static int led_driver_write(const void* data, size_t len) {
    int brightness = *(int*)data;
    printf("Set LED brightness: %d\n", brightness);
    return 0;
}

static int led_driver_control(int cmd, void* arg) {
    printf("LED control command: %d\n", cmd);
    return 0;
}

// Motor driver implementation
static int motor_driver_init(void) {
    printf("Motor driver initialized\n");
    return 0;
}

static int motor_driver_write(const void* data, size_t len) {
    int speed = *(int*)data;
    printf("Set motor speed: %d RPM\n", speed);
    return 0;
}

static int motor_driver_control(int cmd, void* arg) {
    printf("Motor control command: %d\n", cmd);
    return 0;
}

// Device driver objects
static DeviceDriver led_driver = {
    .init = led_driver_init,
    .write = led_driver_write,
    .read = NULL,  // LED usually does not require read operation
    .control = led_driver_control,
    .deinit = NULL,
    .device_name = "LED Driver"
};

static DeviceDriver motor_driver = {
    .init = motor_driver_init,
    .write = motor_driver_write,
    .read = NULL,
    .control = motor_driver_control,
    .deinit = NULL,
    .device_name = "Motor Driver"
};

// Device driver factory
DeviceDriver* device_driver_factory(const char* device_type) {
    if (strcmp(device_type, "LED") == 0) {
        return &led_driver;
    } else if (strcmp(device_type, "MOTOR") == 0) {
        return &motor_driver;
    }
    return NULL;
}

Scenario 4: Data Processor Factory

In data acquisition systems, raw data needs to be processed by different algorithms, and the Factory Pattern can create the corresponding processor based on the processing type.

// Data processor interface
typedef struct {
    int (*init)(void);
    int (*process)(const float* input, float* output, size_t len);
    int (*reset)(void);
    const char* processor_name;
} DataProcessor;

// Filter processor
static int filter_processor_init(void) {
    printf("Filter processor initialized\n");
    return 0;
}

static int filter_processor_process(const float* input, float* output, size_t len) {
    // Simple average filter
    for (size_t i = 0; i < len; i++) {
        if (i == 0) {
            output[i] = input[i];
        } else {
            output[i] = (input[i] + output[i-1]) * 0.5f;
        }
    }
    printf("Filtering completed, processed %zu data points\n", len);
    return 0;
}

// Calibration processor
static int calibration_processor_init(void) {
    printf("Calibration processor initialized\n");
    return 0;
}

static int calibration_processor_process(const float* input, float* output, size_t len) {
    // Linear calibration: output = input * scale + offset
    float scale = 1.2f;
    float offset = -0.5f;

    for (size_t i = 0; i < len; i++) {
        output[i] = input[i] * scale + offset;
    }
    printf("Calibration completed, applied scale factor %.1f and offset %.1f\n", scale, offset);
    return 0;
}

// Data processor objects
static DataProcessor filter_processor = {
    .init = filter_processor_init,
    .process = filter_processor_process,
    .reset = NULL,
    .processor_name = "Filter Processor"
};

static DataProcessor calibration_processor = {
    .init = calibration_processor_init,
    .process = calibration_processor_process,
    .reset = NULL,
    .processor_name = "Calibration Processor"
};

// Data processor factory
DataProcessor* data_processor_factory(const char* processor_type) {
    if (strcmp(processor_type, "filter") == 0) {
        return &filter_processor;
    } else if (strcmp(processor_type, "calibration") == 0) {
        return &calibration_processor;
    }
    return NULL;
}

Implementation Points and Considerations

Design Principles

  1. 1. Unified Interface: Ensure that products of the same type have the same interface definition.
  2. 2. Clear Responsibilities: The factory is only responsible for creation, not business logic.
  3. 3. Easy to Extend: Adding new products should not modify existing code.

Common Pitfalls

  1. 1. Memory Management: Clearly define who is responsible for releasing created objects.
  2. 2. Function Pointer Safety: Check if function pointers are NULL before use.
  3. 3. Type Matching: Ensure that the type of the object returned by the factory is correct.

Best Practices

// Safe object usage example
void safe_use_example(void) {
    SensorInterface* sensor = sensor_factory("temperature");

    if (sensor == NULL) {
        printf("Failed to create sensor\n");
        return;
    }

    // Check necessary function pointers
    if (sensor->init && sensor->read_data && sensor->deinit) {
        if (sensor->init() == 0) {
            float data;
            if (sensor->read_data(&data) == 0) {
                printf("Sensor data: %.2f\n", data);
            }
            sensor->deinit();
        }
    } else {
        printf("Sensor interface is incomplete\n");
    }
}

// Factory registry approach (more flexible implementation)
typedef struct {
    const char* type_name;
    SensorInterface* (*create_func)(void);
} SensorFactoryRegistry;

static SensorFactoryRegistry sensor_registry[] = {
    {"temperature", create_temp_sensor},
    {"humidity", create_humidity_sensor},
    {"light", create_light_sensor},
    {NULL, NULL}  // End marker
};

SensorInterface* advanced_sensor_factory(const char* type) {
    for (int i = 0; sensor_registry[i].type_name != NULL; i++) {
        if (strcmp(sensor_registry[i].type_name, type) == 0) {
            return sensor_registry[i].create_func();
        }
    }
    return NULL;
}

Conclusion

The Factory Pattern is a very practical design pattern in embedded development that can:

  • Simplify Object Creation: Create different types of objects through a unified interface.
  • Improve Code Reusability: Objects with the same interface can be used interchangeably.
  • Enhance Extensibility: Adding new types does not require modifying existing code.
  • Reduce Coupling: The client does not need to understand the specific creation details.

Learning Suggestions:

  1. 1. Start practicing with simple function pointer implementations.
  2. 2. Focus on understanding the importance of interface design.
  3. 3. Gradually apply in actual projects.

Advanced Directions:

  • • Abstract Factory Pattern
  • • Factory Method Pattern
  • • Builder Pattern

Master the Factory Pattern to make your embedded code more flexible and professional!

Follow me for more embedded development design pattern tips!

Leave a Comment