Table of Contents
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
| Smell | Sign | Fix |
|---|---|---|
| Duplicate Code | Copy-pasted logic in multiple places | Extract into a shared function |
| Shotgun Surgery | One change requires editing many files | Encapsulate related behavior in one module |
| Tight Coupling | Modules use each other’s extern internals | Expose clean public APIs in header files |
| Hardcoded Values | Paths, pins, addresses embedded in code | Centralize in config headers |

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.







