Table of Contents
KEY TAKEAWAYS
- Feature Envy: move logic to the module that owns the data
- Inappropriate Intimacy: use public APIs instead of extern-ing internals
- Middle Man: remove wrappers that add no value; keep those that add real responsibility
- Message Chains: wrap deep struct access in helper functions at the right level
What Are Coupler Code Smells?
Coupler code smells indicate excessive coupling between modules. Two modules are too tightly connected when one cannot function, be tested, or be understood without the other. In C, this often shows up as modules reaching into each other’s internal data, extern-ing private variables, or having functions that “know too much” about other modules.Smell 1: Feature Envy
A function that spends more time working with another module’s data than its own. It “envies” the features of a different module and should probably live there instead.Bad — Motor Module Envies Sensor Module
// motor.c — reaches deep into sensor internals
#include "sensor.h"
void motor_adjust_speed(Motor *m) {
// This function mostly works with sensor data, not motor data
float temp = sensor_get_raw_adc() * 3.3f / 4096.0f;
float corrected = (temp - sensor_get_offset()) * sensor_get_gain();
if (corrected > sensor_get_threshold()) {
m->speed = m->speed / 2;
}
}The motor module is doing the sensor’s calibration work. If sensor calibration changes, motor.c must change too.Good — Move Logic to Where the Data Lives
// sensor.c — owns its own calibration logic
float sensor_read_calibrated(void) {
float raw = sensor_get_raw_adc() * 3.3f / 4096.0f;
return (raw - offset) * gain;
}
bool sensor_is_over_threshold(void) {
return sensor_read_calibrated() > threshold;
}
// motor.c — asks sensor for what it needs
void motor_adjust_speed(Motor *m) {
if (sensor_is_over_threshold()) {
m->speed = m->speed / 2;
}
}Smell 2: Inappropriate Intimacy
Two modules access each other’s internal variables or private functions. They are so intertwined that changing one always requires changing the other.Bad — Modules Sharing Internals via extern
// display.c
extern int sensor_raw_buffer[16]; // Reaches into sensor internals
extern int sensor_buffer_index; // Knows about sensor's private index
extern float comm_last_rssi; // Reaches into comm internals
void display_update(void) {
int latest = sensor_raw_buffer[sensor_buffer_index];
lcd_print(0, 0, "Raw: %d", latest);
lcd_print(1, 0, "RSSI: %.1f", comm_last_rssi);
}Good — Access Through Clean Interfaces
// sensor.h — public API
int sensor_get_latest(void);
// comm.h — public API
float comm_get_rssi(void);
// display.c — only uses public APIs
void display_update(void) {
lcd_print(0, 0, "Raw: %d", sensor_get_latest());
lcd_print(1, 0, "RSSI: %.1f", comm_get_rssi());
}Smell 3: Middle Man
A module that does nothing but forward calls to another module. It adds a layer of indirection without adding value.Bad — Pointless Wrapper
// sensor_manager.c — just forwards to sensor.c
float sensor_manager_read(int ch) {
return sensor_read(ch);
}
void sensor_manager_init(void) {
sensor_init();
}
void sensor_manager_calibrate(void) {
sensor_calibrate();
}If every function is just a pass-through, the middle man adds complexity without benefit. Remove it and call sensor.c directly.Good — Middle Man Earns Its Keep
// sensor_manager.c — adds actual value
float sensor_manager_read(int ch) {
float val = sensor_read(ch);
filter_apply(&moving_avg[ch], val);
log_reading(ch, val);
return filter_get(&moving_avg[ch]);
}
void sensor_manager_init(void) {
sensor_init();
filter_init_all(moving_avg, NUM_CHANNELS);
log_init();
}Now the manager adds filtering and logging — it has a real responsibility.Smell 4: Message Chains (Long Struct Chains)
Calling through a chain of structs:system->subsys->device->sensor->read(). This is also a Law of Demeter violation.Bad
float t = system->thermal->sensors[0]->driver->read_raw(system->thermal->sensors[0]->driver->ctx);
Good
float t = thermal_read_sensor(system->thermal, 0);Wrap the chain in a function at the appropriate level.
How to Detect Couplers
- Count the #includes — if a .c file includes many unrelated headers, it may be too tightly coupled
- Search for extern — extern variables are a strong signal of inappropriate intimacy
- Check function parameter types — if a function takes a large parent struct but only uses one field, it may have feature envy
- Look for pass-through functions — if a module adds no logic, it may be a needless middle man
Key Takeaways
- Feature Envy: move logic to the module that owns the data
- Inappropriate Intimacy: use public APIs instead of extern-ing internals
- Middle Man: remove wrappers that add no value; keep those that add real responsibility
- Message Chains: wrap deep struct access in helper functions at the right level

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.







