Skip to content
Home » Software Design » Factory Pattern in C — With Practical Examples

Factory Pattern in C — With Practical Examples

Factory Pattern featured image with purple background, Design Patterns badge, Fa icon, in C With Examples subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 15 of 31View Full Path →

KEY TAKEAWAYS

  • The Factory pattern centralizes object creation behind a common function
  • Callers use a uniform interface without knowing the concrete implementation
  • In C, factories return struct pointers with function pointers as the “interface”
  • Combine with the Strategy pattern for maximum flexibility

What Is the Factory Pattern?

The Factory pattern creates objects (or structs, in C) through a common interface without exposing the creation logic to the caller. Instead of the caller knowing how to construct every specific type, a factory function handles the details and returns a ready-to-use instance.In C, the Factory pattern typically means: a function that takes a type identifier (enum, string, or integer) and returns an initialized struct or interface pointer. This centralizes construction logic and hides implementation details.

Example 1: Communication Interface Factory

Bad — Caller Builds Each Type Manually

void app_init(int comm_type) {
    if (comm_type == COMM_UART) {
        uart_init(9600, 8, 1);
        uart_set_flow_control(FLOW_NONE);
        // 5 more lines of UART setup...
    } else if (comm_type == COMM_SPI) {
        spi_init(SPI_MODE0, 1000000);
        spi_set_cs_pin(GPIO_PIN_4);
        // 5 more lines of SPI setup...
    } else if (comm_type == COMM_I2C) {
        i2c_init(100000);
        i2c_set_address(0x50);
        // 5 more lines of I2C setup...
    }
}
The application code is buried in hardware-specific initialization details. Every new comm type means modifying the caller.

Good — Factory Function

// Common interface
typedef struct {
    int  (*send)(const uint8_t *data, int len);
    int  (*recv)(uint8_t *buf, int max_len);
    void (*close)(void);
} CommInterface;

// Factory function
CommInterface *comm_create(CommType type) {
    switch (type) {
    case COMM_UART:  return uart_create_interface();
    case COMM_SPI:   return spi_create_interface();
    case COMM_I2C:   return i2c_create_interface();
    default:         return NULL;
    }
}

// Each module handles its own setup internally
CommInterface *uart_create_interface(void) {
    static CommInterface iface;
    uart_init(9600, 8, 1);
    uart_set_flow_control(FLOW_NONE);
    iface.send  = uart_send;
    iface.recv  = uart_recv;
    iface.close = uart_close;
    return &iface;
}

// Application code — clean and simple
void app_init(CommType type) {
    CommInterface *comm = comm_create(type);
    comm->send(hello_msg, sizeof(hello_msg));
}
The caller does not know or care whether it is using UART, SPI, or I2C. The factory handles all construction details. Adding a new comm type means writing one xxx_create_interface() function and one case in the factory.

Example 2: Logger Factory

Bad — Scattered Logger Creation

void setup_logging(const char *target) {
    if (strcmp(target, "uart") == 0) {
        uart_init(115200, 8, 1);
        // global logger state for UART
        g_log_func = uart_log;
    } else if (strcmp(target, "file") == 0) {
        g_log_file = fopen("/var/log/app.log", "a");
        g_log_func = file_log;
    } else if (strcmp(target, "syslog") == 0) {
        openlog("myapp", LOG_PID, LOG_USER);
        g_log_func = syslog_log;
    }
}

Good — Logger Factory with Consistent Interface

typedef struct Logger {
    void (*log)(struct Logger *self, const char *level, const char *msg);
    void (*destroy)(struct Logger *self);
    void *ctx;  // backend-specific data
} Logger;

// UART logger
static void uart_log(Logger *self, const char *level, const char *msg) {
    char buf[256];
    int len = snprintf(buf, sizeof(buf), "[%s] %srn", level, msg);
    uart_send((uint8_t *)buf, len);
}

Logger *logger_create_uart(int baud) {
    Logger *l = malloc(sizeof(Logger));
    uart_init(baud, 8, 1);
    l->log = uart_log;
    l->destroy = uart_logger_destroy;
    l->ctx = NULL;
    return l;
}

// File logger
static void file_log(Logger *self, const char *level, const char *msg) {
    FILE *f = (FILE *)self->ctx;
    fprintf(f, "[%s] %s\n", level, msg);
    fflush(f);
}

Logger *logger_create_file(const char *path) {
    Logger *l = malloc(sizeof(Logger));
    l->ctx = fopen(path, "a");
    l->log = file_log;
    l->destroy = file_logger_destroy;
    return l;
}

// Factory
Logger *logger_create(const char *type) {
    if (strcmp(type, "uart") == 0)  return logger_create_uart(115200);
    if (strcmp(type, "file") == 0)  return logger_create_file("/var/log/app.log");
    return NULL;
}

// Usage — backend-agnostic
Logger *log = logger_create("file");
log->log(log, "INFO", "System started");
log->log(log, "ERROR", "Sensor timeout");
log->destroy(log);
The application code uses the Logger interface without knowing the backend. Switching from file logging to UART logging means changing one string. Each backend manages its own resources internally.

Factory Pattern Variations

  • Simple Factory — a function with a switch/if that returns the right type (shown above)
  • Registration Factory — modules register their factory functions at startup, and the factory looks them up by name. This combines Factory with Open/Closed Principle
  • Abstract Factory — a struct of factory function pointers, letting you swap entire families of related objects. Common in HAL (Hardware Abstraction Layer) design

When to Use the Factory Pattern

  • Multiple implementations of the same interface (UART vs SPI vs I2C)
  • Complex initialization that should be hidden from the caller
  • Runtime selection — choosing which implementation based on config, hardware detection, or user input
  • Testing — the factory can return mock implementations during unit tests

Key Takeaways

  • The Factory pattern centralizes object creation behind a common function
  • Callers use a uniform interface without knowing the concrete implementation
  • In C, factories return struct pointers with function pointers as the “interface”
  • Combine with the Strategy pattern for maximum flexibility

Leave a Reply

Your email address will not be published. Required fields are marked *