Table of Contents
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

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







