Skip to content
Home » Software Design » Composition Over Inheritance in C

Composition Over Inheritance in C

Composition Over Inheritance featured image with purple background, Clean Code badge, C icon, Inheritance in C subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 10 of 31View Full Path →

KEY TAKEAWAYS

  • Composition: build complex objects by combining simple, independent parts
  • In C, use struct embedding (putting one struct inside another) for data composition
  • Use function pointers for behavior composition
  • Each component can be developed, tested, and reused independently
  • Composition is more flexible than a rigid hierarchy — you can mix and match components freely

What Is Composition Over Inheritance?

Composition Over Inheritance means: build complex behavior by combining simple, independent components rather than creating deep hierarchies.In C there is no class inheritance, but the same pitfall exists: creating rigid struct hierarchies where changing one “base” struct breaks everything. Composition gives you flexibility — you assemble what you need from independent parts.

Example 1: Building a Device from Components

Bad: Deep struct nesting that mimics inheritance

/* "Base" struct - all devices have this */
typedef struct {
    char name[32];
    int  id;
    int  status;
    /* Sensor-specific fields mixed in */
    int  raw_value;
    float calibration;
    /* Communication fields mixed in */
    int  baud_rate;
    char protocol[16];
    /* Storage fields mixed in */
    int  log_interval;
    char filename[64];
} device_t;

/* Every device carries ALL fields, even if unused */
device_t temp_sensor;  /* Doesn't need baud_rate or filename */
device_t gps_module;   /* Doesn't need calibration or log_interval */
Problem: Every device carries fields it does not use. Adding a field for one device type bloats all devices. Changing the communication fields risks breaking sensor code.

Good: Compose from independent components

/* Independent components */
typedef struct {
    char name[32];
    int  id;
    int  status;
} device_info_t;

typedef struct {
    int   raw_value;
    float calibration;
    int   (*read)(void);
} sensor_t;

typedef struct {
    int  baud_rate;
    char protocol[16];
    int  (*send)(const uint8_t *data, int len);
} comm_t;

typedef struct {
    int  log_interval;
    char filename[64];
    int  (*write)(const uint8_t *data, int len);
} storage_t;

/* Temperature sensor: device info + sensor (no comm, no storage) */
typedef struct {
    device_info_t info;
    sensor_t      sensor;
} temp_device_t;

/* IoT gateway: device info + comm + storage (no sensor) */
typedef struct {
    device_info_t info;
    comm_t        comm;
    storage_t     storage;
} gateway_device_t;

/* Weather station: all components */
typedef struct {
    device_info_t info;
    sensor_t      sensor;
    comm_t        comm;
    storage_t     storage;
} weather_station_t;
Each device type only includes the components it actually needs. Components are independent — changing comm_t does not affect sensor_t.

Example 2: Behavior Composition with Function Pointers

Bad: One monolithic “do everything” function

/* One function handles all data processing steps */
void process_data(int raw_data, int device_type) {
    int filtered;
    int result;

    /* Filter step - different per device type */
    if (device_type == 1) {
        filtered = raw_data / 4;  /* Average of 4 */
    } else if (device_type == 2) {
        filtered = (raw_data + last_value) / 2;  /* Moving average */
    } else {
        filtered = raw_data;  /* No filter */
    }

    /* Transform step - also different per type */
    if (device_type == 1) {
        result = (filtered * 330) / 1024;  /* ADC to celsius */
    } else if (device_type == 2) {
        result = filtered * 10;  /* Raw to mm */
    }

    /* Output - also different */
    if (device_type == 1) {
        uart_send_int(result);
    } else {
        lcd_print_int(result);
    }
}

Good: Compose the pipeline from interchangeable steps

/* Each step is a function pointer - composable */
typedef int (*filter_fn)(int raw);
typedef int (*transform_fn)(int filtered);
typedef void (*output_fn)(int result);

typedef struct {
    filter_fn    filter;
    transform_fn transform;
    output_fn    output;
} data_pipeline_t;

/* Reusable filter functions */
int filter_average4(int raw) { return raw / 4; }
int filter_none(int raw)     { return raw; }

/* Reusable transform functions */
int transform_adc_to_celsius(int val) { return (val * 330) / 1024; }
int transform_raw_to_mm(int val)      { return val * 10; }

/* Reusable output functions */
void output_uart(int val) { uart_send_int(val); }
void output_lcd(int val)  { lcd_print_int(val); }

/* Compose pipelines */
data_pipeline_t temp_pipeline = {
    .filter    = filter_average4,
    .transform = transform_adc_to_celsius,
    .output    = output_uart,
};

data_pipeline_t distance_pipeline = {
    .filter    = filter_none,
    .transform = transform_raw_to_mm,
    .output    = output_lcd,
};

/* One generic processor */
void process_data(data_pipeline_t *pipeline, int raw_data) {
    int filtered = pipeline->filter(raw_data);
    int result   = pipeline->transform(filtered);
    pipeline->output(result);
}
Adding a new device means creating a new pipeline from existing (or new) components. No if-else chains, no modification of existing code.

Key Takeaways

  • Composition: build complex objects by combining simple, independent parts
  • In C, use struct embedding (putting one struct inside another) for data composition
  • Use function pointers for behavior composition
  • Each component can be developed, tested, and reused independently
  • Composition is more flexible than a rigid hierarchy — you can mix and match components freely

Leave a Reply

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