Skip to content
Home » Software Design » Code Smells in C: Change Preventers (Duplicate Code, Tight Coupling, and More)

Code Smells in C: Change Preventers (Duplicate Code, Tight Coupling, and More)

Code Smells Change Preventers featured image with dark purple background, Code Smells badge, exclamation icon, Change Preventers in C subtitle in the Software Design in C series
Software Design Principles in C
Part 23 of 31View Full Path →

KEY TAKEAWAYS

  • Change preventers are code patterns that make modifications difficult and error-prone
  • Duplicate code means every fix must be applied in multiple places — violating the DRY principle
  • Tight coupling between modules means changing one module forces changes in others
  • Refactor change preventers early to keep the codebase adaptable to new requirements

What Are Change Preventers?

Change preventers are code smells that make it painful or risky to modify the code. When you need to make one small change but must edit ten different files, that is a change preventer.

Smell 1: Duplicate Code

What it is: The same (or nearly identical) code appears in multiple places. Fixing a bug in one copy means finding and fixing all copies.

Smelly Code

/* In sensor.c */
void sensor_log(const char *msg) {
    char buf[64];
    snprintf(buf, sizeof(buf), "[%lu] SENSOR: %srn", get_tick(), msg);
    uart_send(buf);
}

/* In motor.c - nearly identical! */
void motor_log(const char *msg) {
    char buf[64];
    snprintf(buf, sizeof(buf), "[%lu] MOTOR: %srn", get_tick(), msg);
    uart_send(buf);
}

/* In comm.c - same pattern again! */
void comm_log(const char *msg) {
    char buf[64];
    snprintf(buf, sizeof(buf), "[%lu] COMM: %srn", get_tick(), msg);
    uart_send(buf);
}

Fix: One shared function

/* logger.c */
void log_message(const char *module, const char *msg) {
    char buf[64];
    snprintf(buf, sizeof(buf), "[%lu] %s: %srn", get_tick(), module, msg);
    uart_send(buf);
}

/* Usage: */
log_message("SENSOR", "Temperature read");
log_message("MOTOR", "Speed changed");
log_message("COMM", "Packet received");

Smell 2: Shotgun Surgery

What it is: A single change requires editing many files across the codebase. The opposite of having things in one place.

Smelly Code

/* Adding a new sensor type "pressure" requires changes in: */

/* sensor.h - add enum value */
typedef enum { TEMP, HUMIDITY, PRESSURE } sensor_type_t;  /* 1 */

/* sensor.c - add read function */
int read_pressure(void) { ... }                            /* 2 */

/* display.c - add display case */
case PRESSURE: lcd_print("P:"); break;                     /* 3 */

/* logger.c - add log format */
case PRESSURE: log("Pressure: %d", val); break;            /* 4 */

/* alarm.c - add threshold check */
case PRESSURE: if (val > 1100) alarm(); break;             /* 5 */

/* config.c - add config parsing */
case PRESSURE: cfg.pressure_interval = atoi(val); break;   /* 6 */

/* 6 files changed for one new sensor! */

Fix: Encapsulate sensor behavior in one place

/* sensor.h */
typedef struct {
    const char *name;
    int  (*read)(void);
    void (*display)(int value);
    int  alarm_threshold;
} sensor_def_t;

/* pressure_sensor.c - everything about pressure in ONE file */
int pressure_read(void) { return adc_read(PRESSURE_CH); }
void pressure_display(int val) { lcd_printf("P: %d hPa", val); }

sensor_def_t pressure_sensor = {
    .name            = "Pressure",
    .read            = pressure_read,
    .display         = pressure_display,
    .alarm_threshold = 1100,
};

/* sensor_manager.c - generic, does not change when adding sensors */
void sensor_manager_register(sensor_def_t *sensor);

/* Adding a new sensor = adding ONE file + one register call */

Smell 3: Tight Coupling

What it is: Modules directly reference each other’s internal variables or implementation details, creating a chain of dependencies.

Smelly Code

/* motor.c reaches into sensor.c internals */
extern int sensor_raw_value;          /* From sensor.c */
extern int sensor_calibration_offset; /* Internal detail! */
extern int sensor_filter_buffer[16];  /* Very internal! */

void motor_adjust(void) {
    int temp = sensor_raw_value - sensor_calibration_offset;
    int avg = 0;
    for (int i = 0; i  50 ? 100 : 50);
}
Problem: If sensor.c changes its buffer size from 16 to 32, motor.c breaks. motor.c knows too much about sensor.c internals.

Fix: Use public interfaces only

/* sensor.h - clean public API */
int sensor_get_temperature(void);    /* Returns calibrated, filtered value */
int sensor_get_average(void);        /* Returns running average */

/* motor.c - uses only the public API */
void motor_adjust(void) {
    int avg_temp = sensor_get_average();
    motor_set_speed(avg_temp > 50 ? 100 : 50);
}

/* sensor.c - free to change internals without breaking motor.c */
static int raw_value;
static int calibration_offset;
static int filter_buffer[32];  /* Changed from 16 to 32 - no problem */
static int filter_size = 32;

int sensor_get_average(void) {
    long sum = 0;
    for (int i = 0; i < filter_size; i++) sum += filter_buffer[i];
    return (int)(sum / filter_size);
}

Smell 4: Hardcoded Values

What it is: Paths, addresses, pin numbers, or configuration values embedded directly in the code instead of being configurable.

Smelly Code

void save_log(const char *msg) {
    FILE *f = fopen("/var/log/sensor.txt", "a");  /* Hardcoded path */
    fprintf(f, "%s\n", msg);
    fclose(f);
}

void init_gpio(void) {
    gpio_set_output(13);   /* What is pin 13? */
    gpio_set_input(7);     /* What is pin 7? */
    gpio_set_output(4);    /* What is pin 4? */
}

Fix: Centralize in config or header

/* board_config.h */
#define LOG_FILE_PATH    "/var/log/sensor.txt"
#define PIN_LED          13
#define PIN_BUTTON        7
#define PIN_RELAY         4

void save_log(const char *msg) {
    FILE *f = fopen(LOG_FILE_PATH, "a");
    fprintf(f, "%s\n", msg);
    fclose(f);
}

void init_gpio(void) {
    gpio_set_output(PIN_LED);
    gpio_set_input(PIN_BUTTON);
    gpio_set_output(PIN_RELAY);
}

Summary

SmellSignFix
Duplicate CodeCopy-pasted logic in multiple placesExtract into a shared function
Shotgun SurgeryOne change requires editing many filesEncapsulate related behavior in one module
Tight CouplingModules use each other’s extern internalsExpose clean public APIs in header files
Hardcoded ValuesPaths, pins, addresses embedded in codeCentralize in config headers

Leave a Reply

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