Embedded Programming Model | Simple Factory Pattern

Follow our official account to keep receiving embedded knowledge!

1. Simple Factory Pattern

The Simple Factory Pattern, also known as the Static Factory Method Pattern, is a type of creational pattern.

It encapsulates the object creation logic through a factory class, dynamically instantiating specific product classes based on input parameters, achieving decoupling of creation and usage.

This pattern is particularly suitable for embedded systems in the following scenarios:

  • Multi-hardware driver compatibility
  • Resource-constrained environments
  • Cross-platform adaptation

The core structure of the Simple Factory Pattern includes the following components:

  • Factory class: Responsible for implementing the internal logic for creating all instances.
  • Abstract product interface: The parent class of all created objects, responsible for describing the common interfaces shared by all instances.
  • Concrete product classes: Instances of specific classes that represent all created objects.

2. Embedded Application Cases

1. LCD Factory Block Diagram

Embedded Programming Model | Simple Factory Pattern
  • Factory class: Creates the corresponding LCD driver instance based on the model parameter passed in.
  • Abstract product interface: LCD driver interface (includes methods for initialization, writing commands, writing data, etc.).
  • Concrete product classes: Different models of LCD drivers (e.g., ST7789Driver, ILI9341Driver).

2. Code Implementation

UML Class Diagram:

Embedded Programming Model | Simple Factory Pattern

Embedded devices need to support multiple LCD screens (such as ST7789, ILI9341), which have significant differences in initialization, command writing, etc., but the upper-level application needs to call a unified interface.

Code Implementation:

C Language:

typedef enum{ 
    LCD_ST7789, 
    LCD_ILI9341
} LCD_Type;

// Abstract product: LCD operation interface
typedef struct {
    void (*Init)(void);
    void (*WriteCommand)(uint8_t cmd);
    void (*DisplayText)(const char *text);
    void (*Clear)(void);
} LCD_Driver;

// Concrete product: ST7789 driver
void ST7789_Init(void){}
void ST7789_WriteCommand(uint8_t cmd){}
void ST7789_DisplayText(const char *text){}
void ST7789_Clear(void){}
LCD_Driver ST7789_Driver = {
    ST7789_Init, 
    ST7789_WriteCommand,
    ST7789_DisplayText,
    ST7789_Clear,
};

// Concrete product: ILI9341 driver
void ILI9341_Init(void){}
void ILI9341_WriteCommand(uint8_t cmd){}
void ILI9341_DisplayText(const char *text){}
void ILI9341_Clear(void){}
LCD_Driver ILI9341_Driver = {
    ILI9341_Init, 
    ILI9341_WriteCommand,
    ILI9341_DisplayText,
    ILI9341_Clear,
};

// Factory class: Returns driver instance based on screen type (this part is hidden)
//typedef struct {
//    void (*Create)(LCD_Type type);
//} LCD_Factory;

LCD_Driver* LCD_Factory_Create(LCD_Type type) {
    switch (type) {
        case LCD_ST7789: return &ST7789_Driver;
        case LCD_ILI9341: return &ILI9341_Driver;
        default: return NULL;
    }
}

C++:

// Abstract product class: LCD operation interface
class LCD_Driver {
public:
    virtual void Init() = 0;
    virtual void WriteCommand(uint8_t cmd) = 0;
    virtual void DisplayText(const char *text) = 0;
    virtual void Clear() = 0;
    
    virtual ~LCD_Driver() {}
};

// Concrete product class: ST7789 driver
class ST7789_Driver : public LCD_Driver {
public:
    void Init() override;
    void WriteCommand(uint8_t cmd) override;
    void DisplayText(const char *text) override;
    void Clear() override;
};
void ST7789_Driver::Init() {}
void ST7789_Driver::WriteCommand(uint8_t cmd) {}
void ST7789_Driver::DisplayText(const char *text) {}
void ST7789_Driver::Clear() {}

// Concrete product class: ILI9341 driver
class ILI9341_Driver : public LCD_Driver {
public:
    void Init() override;
    void WriteCommand(uint8_t cmd) override;
    void DisplayText(const char *text) override;
    void Clear() override;
};

void ILI9341_Driver::Init() {}
void ILI9341_Driver::WriteCommand(uint8_t cmd) {}
void ILI9341_Driver::DisplayText(const char *text) {}
void ILI9341_Driver::Clear() {}

// Factory class
class LCD_Factory {
public:
    enum LCD_Type { LCD_ST7789, LCD_ILI9341 };
    
    // Create LCD driver instance
    static LCD_Driver* Create(LCD_Type type) {
        switch (type) {
            case LCD_ST7789: 
                return new ST7789_Driver();
            case LCD_ILI9341: 
                return new ILI9341_Driver();
            default: 
                return nullptr;
        }
    }
};

Usage Example:

LCD_Driver *lcd = LCD_Factory_Create(LCD_ST7789);
lcd->Init();

3. Advantages and Disadvantages

Advantages:

(1) Changing the LCD requires only one line of code:

// Switch from ST7789 to ILI9341
LCD_Driver *lcd = LCD_Factory_Create(LCD_ILI9341);

(2) Unified operation interface:

// Regardless of the LCD model, the calling method is the same
lcd->DisplayText("Temp: 25.5C");
lcd->Clear();

(3) Easy to extend new LCD models:

// Concrete product: xxx driver
void xxx_Init(void){}
void xxx_WriteCommand(uint8_t cmd){}
void xxx_DisplayText(const char *text){}
void xxx_Clear(void){}
LCD_Driver xxx_Driver = {
    xxx_Init, 
    xxx_WriteCommand,
    xxx_DisplayText,
    xxx_Clear,
};

Disadvantages:

(1) Violates the Open/Closed Principle: Adding a new driver requires modifying the factory

LCD_Driver* LCD_Factory_Create(uint8_t type) {
    switch (type) {
        case LCD_ST7789: return &ST7789_Driver;
        case LCD_ILI9341: return &ILI9341_Driver;
        // Adding a new driver must modify this part ↓
        case LCD_XXX: return &XXX_Driver;
        default: return NULL;
    }
}

Every time a new LCD model is added, the factory function needs to be modified, violating the principle of “open for extension, closed for modification.” This may introduce risks during firmware upgrades.

(2) Overloaded factory responsibilities: Centralizes all creation logic

LCD_Driver* LCD_Factory_Create(uint8_t type) {
    switch (type) {
        case LCD_ST7789: return &ST7789_Driver;
        case LCD_ILI9341: return &ILI9341_Driver;
        // There may be dozens of other drivers
        default: return NULL;
    }
}

When supporting 10+ types of LCDs, the function becomes bloated, and the compiled code size increases.

3. Summary of Applicability in Embedded Scenarios

The key point of the Simple Factory Pattern is: when we need something, we only need to pass a correct parameter to the factory to obtain the product instance we need, without knowing the details of its creation.

Scenario Advantages Case
Multi-hardware driver compatibility Unified interface, reduced coupling LCD/Sensor driver management
Dynamic algorithm selection Load lightweight modules on demand Image recognition algorithm switching
Cross-platform runtime adaptation Encapsulate OS/chip-related code RTOS vs Linux drivers

You might also like:

Embedded Device Networking: From Basics to Practice!

Embedded Software: Functional vs Non-functional Programming

Embedded Field: The Ultimate Showdown between Linux and RTOS!

Embedded Performance Metrics Hide These Secrets, How Many Do You Know?

Embedded Software Advanced Guide, Let’s Level Up Together!

Embedded Programming Model | MVC Model

Embedded Programming Model | Observer Pattern

Step-by-step guide to building an embedded containerized development environment!

An elegant multifunctional embedded debugger!

A very lightweight embedded logging library!

A very lightweight embedded thread pool library!

Popular C Language Projects on GitHub!

Practical | Teach you to light up a webpage in 10 minutes

Essential Skills for Embedded Development | Git Submodules

Leave a Comment