Skip to content
Home » Software Design » Interface Segregation Principle (ISP) in C

Interface Segregation Principle (ISP) in C

Pipelining in Embedded Systems featured image with dark green background, HARDWARE badge, FDE icon in green circle, and Stages Hazards and Performance subtitle by nerdyelectronics.com
Software Design Principles in C
Part 5 of 31View Full Path →

KEY TAKEAWAYS

  • ISP: keep interfaces small and focused; do not force clients to depend on things they do not use
  • In C, this means splitting large headers and large function-pointer structs into smaller, cohesive groups
  • A module should only #include what it directly uses
  • ISP reduces compilation dependencies and makes code easier to understand and test

What Is the Interface Segregation Principle?

The Interface Segregation Principle (ISP) states: no client should be forced to depend on methods it does not use.In C terms: do not put everything into one giant header file or one massive struct of function pointers. Split interfaces into small, focused groups so that each module only includes what it actually needs.

Example 1: Bloated Sensor Driver Interface

Bad: One huge interface forces all sensors to implement everything

/* sensor_driver.h - one interface for everything */
typedef struct {
    int  (*init)(void);
    int  (*read_raw)(void);
    float(*read_calibrated)(void);
    void (*set_sample_rate)(int hz);
    void (*set_resolution)(int bits);
    void (*self_test)(void);
    void (*enter_low_power)(void);
    void (*exit_low_power)(void);
    int  (*read_register)(uint8_t reg);
    void (*write_register)(uint8_t reg, uint8_t val);
    void (*set_interrupt_pin)(int pin);
    void (*calibrate)(float offset, float gain);
} sensor_driver_t;
Problem: A simple temperature sensor (like an NTC thermistor read via ADC) does not have registers, interrupt pins, low-power modes, or self-test capability. Yet it must implement (or stub out with NULL) all 12 functions.
/* ntc_thermistor.c - forced to set unused functions to NULL */
sensor_driver_t ntc_driver = {
    .init            = ntc_init,
    .read_raw        = ntc_read_raw,
    .read_calibrated = ntc_read_calibrated,
    .set_sample_rate = NULL,    /* Not supported */
    .set_resolution  = NULL,    /* Not supported */
    .self_test       = NULL,    /* Not supported */
    .enter_low_power = NULL,    /* Not supported */
    .exit_low_power  = NULL,    /* Not supported */
    .read_register   = NULL,    /* No registers */
    .write_register  = NULL,    /* No registers */
    .set_interrupt_pin = NULL,  /* No interrupt */
    .calibrate       = ntc_calibrate,
};
And every caller must check for NULL before calling any function — messy and error-prone.

Good: Split into focused interfaces

/* sensor_read.h - basic reading (all sensors support this) */
typedef struct {
    int  (*init)(void);
    int  (*read_raw)(void);
    float(*read_calibrated)(void);
} sensor_read_t;

/* sensor_config.h - for configurable sensors */
typedef struct {
    void (*set_sample_rate)(int hz);
    void (*set_resolution)(int bits);
} sensor_config_t;

/* sensor_power.h - for sensors with power management */
typedef struct {
    void (*enter_low_power)(void);
    void (*exit_low_power)(void);
} sensor_power_t;

/* sensor_diag.h - for sensors with diagnostics */
typedef struct {
    void (*self_test)(void);
    void (*calibrate)(float offset, float gain);
} sensor_diag_t;
/* ntc_thermistor.c - only implements what it supports */
sensor_read_t ntc_reader = {
    .init            = ntc_init,
    .read_raw        = ntc_read_raw,
    .read_calibrated = ntc_read_calibrated,
};

/* imu_sensor.c - implements multiple interfaces */
sensor_read_t   imu_reader = { imu_init, imu_read_raw, imu_read_calibrated };
sensor_config_t imu_config = { imu_set_sample_rate, imu_set_resolution };
sensor_power_t  imu_power  = { imu_enter_low_power, imu_exit_low_power };
sensor_diag_t   imu_diag   = { imu_self_test, imu_calibrate };
Now a module that only reads temperature only needs to #include "sensor_read.h". It has no dependency on power management or diagnostics.

Example 2: Splitting a Large Header File

Bad: One header that everything includes

/* system.h - everything in one place */
void system_init(void);
void system_reset(void);
int  system_get_uptime(void);
void system_set_clock(int mhz);

void gpio_set(int pin);
void gpio_clear(int pin);
int  gpio_read(int pin);

void uart_send(const char *str);
int  uart_receive(char *buf, int max);

void spi_transfer(uint8_t *tx, uint8_t *rx, int len);

void i2c_write(uint8_t addr, uint8_t *data, int len);
int  i2c_read(uint8_t addr, uint8_t *buf, int len);

void adc_start(int channel);
int  adc_read(void);

void timer_start(int period_ms);
void timer_stop(void);
Problem: A module that only needs GPIO must include this entire header, gaining visibility of UART, SPI, I2C, ADC, and timer functions it does not use. Any change to the timer declarations causes the GPIO module to recompile.

Good: Separate headers per concern

/* gpio.h */
void gpio_set(int pin);
void gpio_clear(int pin);
int  gpio_read(int pin);

/* uart.h */
void uart_send(const char *str);
int  uart_receive(char *buf, int max);

/* spi.h */
void spi_transfer(uint8_t *tx, uint8_t *rx, int len);

/* i2c.h */
void i2c_write(uint8_t addr, uint8_t *data, int len);
int  i2c_read(uint8_t addr, uint8_t *buf, int len);

/* adc.h */
void adc_start(int channel);
int  adc_read(void);

/* timer.h */
void timer_start(int period_ms);
void timer_stop(void);
/* led_control.c - only includes what it needs */
#include "gpio.h"

void led_on(void)  { gpio_set(LED_PIN); }
void led_off(void) { gpio_clear(LED_PIN); }
/* Does NOT depend on uart.h, spi.h, adc.h, or timer.h */

When to Apply ISP

  • When you have a struct of function pointers where most implementations set half of them to NULL
  • When a header file is included everywhere but each file only uses a small fraction of its declarations
  • When changing one part of an interface causes unrelated modules to recompile

When NOT to Over-Apply

  • Do not create a separate header for every single function — group related functions together
  • If all consumers genuinely use all functions, one interface is fine
  • 3-5 functions in an interface is usually a good size

Key Takeaways

  • ISP: keep interfaces small and focused; do not force clients to depend on things they do not use
  • In C, this means splitting large headers and large function-pointer structs into smaller, cohesive groups
  • A module should only #include what it directly uses
  • ISP reduces compilation dependencies and makes code easier to understand and test

📖 Related: Building a Driver Interface in C — A Complete Example

Leave a Reply

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