Skip to content
Home » Software Design » Separation of Concerns in C

Separation of Concerns in C

Separation of Concerns featured image with purple background, Clean Code badge, S icon, in C Programming subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 11 of 31View Full Path →

KEY TAKEAWAYS

  • SoC: one module, one concern — hardware, logic, display, and storage should not be in the same function
  • The main function should be an orchestrator that calls into focused modules
  • Separated concerns are independently testable, reusable, and replaceable
  • SoC is closely related to SRP but applies at a broader architectural level

What Is Separation of Concerns?

Separation of Concerns (SoC) means: divide your code into distinct sections, where each section addresses a specific concern. A “concern” is a distinct piece of functionality: reading input, processing data, displaying output, logging, error handling, etc.When concerns are mixed together, changing one thing affects everything else. When they are separated, each piece can be understood, modified, and tested independently.

Example 1: Mixed Business Logic and Hardware I/O

Bad: Everything in one function

/* main.c - reading hardware, processing, outputting, all mixed together */
void monitor_temperature(void) {
    /* Hardware concern: read ADC */
    ADC_START(CHANNEL_0);
    while (!ADC_DONE());
    int raw = ADC_RESULT;

    /* Processing concern: convert */
    float celsius = (raw * 3.3f / 4096.0f - 0.5f) * 100.0f;

    /* Business logic concern: check threshold */
    static float max_temp = 0;
    if (celsius > max_temp) max_temp = celsius;

    int alarm = (celsius > 75.0f) ? 1 : 0;

    /* Display concern: format and send */
    char buf[64];
    sprintf(buf, "T=%.1f C (max=%.1f) %srn",
            celsius, max_temp, alarm ? "ALARM!" : "OK");
    UART_TX_STRING(buf);

    /* Actuator concern: control fan */
    if (alarm) {
        GPIO_SET(FAN_PIN);
    } else {
        GPIO_CLEAR(FAN_PIN);
    }
}
Problems:
  • Cannot test the conversion formula without real ADC hardware
  • Cannot change the display format without risking the alarm logic
  • Cannot reuse the temperature conversion in another project

Good: Each concern in its own module

/* adc_driver.c - hardware concern */
int adc_read(int channel) {
    ADC_START(channel);
    while (!ADC_DONE());
    return ADC_RESULT;
}

/* temperature.c - processing concern */
float temperature_convert(int raw_adc) {
    return (raw_adc * 3.3f / 4096.0f - 0.5f) * 100.0f;
}

/* alarm.c - business logic concern */
typedef struct {
    float max_temp;
    int   is_active;
} alarm_state_t;

void alarm_update(alarm_state_t *state, float temperature) {
    if (temperature > state->max_temp)
        state->max_temp = temperature;
    state->is_active = (temperature > 75.0f) ? 1 : 0;
}

/* display.c - presentation concern */
void display_temperature(float temp, float max_temp, int alarm) {
    char buf[64];
    sprintf(buf, "T=%.1f C (max=%.1f) %srn",
            temp, max_temp, alarm ? "ALARM!" : "OK");
    uart_send_string(buf);
}

/* fan.c - actuator concern */
void fan_set(int on) {
    if (on) GPIO_SET(FAN_PIN);
    else    GPIO_CLEAR(FAN_PIN);
}

/* main.c - orchestration only */
void monitor_temperature(void) {
    int raw = adc_read(CHANNEL_0);
    float celsius = temperature_convert(raw);
    alarm_update(&alarm_state, celsius);
    display_temperature(celsius, alarm_state.max_temp, alarm_state.is_active);
    fan_set(alarm_state.is_active);
}
Each module can be tested independently. temperature_convert can be unit tested with known values. The display format can change without touching alarm logic.

Example 2: Monolithic main.c Split Into Modules

Bad: 500-line main.c with everything

/* main.c - 500+ lines doing everything */
/* Line 1-50: GPIO initialization */
/* Line 51-120: UART communication functions */
/* Line 121-200: Sensor reading and calibration */
/* Line 201-280: Data filtering and averaging */
/* Line 281-350: Command parser for serial commands */
/* Line 351-420: LED status indicator logic */
/* Line 421-500: Main loop tying it all together */

Good: Each concern gets its own file

project/
├── main.c           /* 30 lines: init + main loop */
├── gpio.c / gpio.h          /* Pin setup and control */
├── uart.c / uart.h          /* Serial communication */
├── sensor.c / sensor.h      /* Sensor reading + calibration */
├── filter.c / filter.h      /* Data filtering algorithms */
├── command.c / command.h    /* Serial command parser */
└── status_led.c / status_led.h  /* LED indicator logic */
/* main.c - just orchestration */
#include "gpio.h"
#include "uart.h"
#include "sensor.h"
#include "filter.h"
#include "command.h"
#include "status_led.h"

int main(void) {
    gpio_init();
    uart_init(9600);
    sensor_init();
    status_led_init();

    while (1) {
        int raw = sensor_read();
        int filtered = filter_apply(raw);
        command_process_pending();
        status_led_update(filtered);
    }
}
main.c is now 20 lines and reads like a table of contents. Each module is small, focused, and independently testable.

Common Concerns to Separate

ConcernDescriptionExample Module
InputReading data from hardware or useradc.c, uart_rx.c, button.c
ProcessingTransforming or computingfilter.c, calculator.c
OutputDisplaying or sending resultsdisplay.c, uart_tx.c
StorageSaving/loading persistent dataeeprom.c, flash.c
ConfigurationSettings and parametersconfig.c
Error handlingDetecting and reporting errorserror.c, fault.c
LoggingRecording events for debugginglogger.c

Key Takeaways

  • SoC: one module, one concern — hardware, logic, display, and storage should not be in the same function
  • The main function should be an orchestrator that calls into focused modules
  • Separated concerns are independently testable, reusable, and replaceable
  • SoC is closely related to SRP but applies at a broader architectural level

Leave a Reply

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