Table of Contents
KEY TAKEAWAYS
- Refactoring improves code structure without changing behavior
- Make small, safe changes — one technique at a time
- Test after each refactoring step to ensure nothing broke
- The five most useful refactorings: Extract Function, Replace Magic Numbers, Guard Clauses, Explaining Variable, Consolidate Duplicates
What Is Refactoring?
Refactoring is the process of improving the internal structure of code without changing its external behavior. The code does the same thing before and after, but it becomes cleaner, more readable, and easier to maintain.Refactoring is not rewriting from scratch. It is small, safe, incremental transformations — each one simple enough that you can verify nothing broke.Technique 1: Extract Function
The most common refactoring. When a block of code inside a function does a distinct task, extract it into its own function with a descriptive name.Before
void process_reading(int channel) {
// Read and validate sensor
int raw = adc_read(channel);
if (raw 4095) {
log_error("Bad ADC reading");
return;
}
float voltage = raw * 3.3f / 4096.0f;
float temp = (voltage - 0.5f) * 100.0f;
// Check thresholds
if (temp > 85.0f) {
gpio_set(ALARM_PIN, HIGH);
uart_send_string("OVER TEMPrn");
} else if (temp < -10.0f) {
gpio_set(ALARM_PIN, HIGH);
uart_send_string("UNDER TEMPrn");
} else {
gpio_set(ALARM_PIN, LOW);
}
// Display
char buf[32];
snprintf(buf, sizeof(buf), "T=%.1f C", temp);
lcd_print(0, 0, buf);
}After
static float adc_to_temperature(int raw) {
float voltage = raw * 3.3f / 4096.0f;
return (voltage - 0.5f) * 100.0f;
}
static void check_temp_alarm(float temp) {
if (temp > 85.0f) {
gpio_set(ALARM_PIN, HIGH);
uart_send_string("OVER TEMPrn");
} else if (temp < -10.0f) {
gpio_set(ALARM_PIN, HIGH);
uart_send_string("UNDER TEMPrn");
} else {
gpio_set(ALARM_PIN, LOW);
}
}
static void display_temperature(float temp) {
char buf[32];
snprintf(buf, sizeof(buf), "T=%.1f C", temp);
lcd_print(0, 0, buf);
}
void process_reading(int channel) {
int raw = adc_read(channel);
if (raw 4095) {
log_error("Bad ADC reading");
return;
}
float temp = adc_to_temperature(raw);
check_temp_alarm(temp);
display_temperature(temp);
}The main function now reads like a summary. Each extracted function is independently testable and reusable.Technique 2: Replace Magic Numbers with Named Constants
Before
if (pressure > 1034) { // What is 1034?
set_valve(0, 255); // What is 0? What is 255?
delay_ms(500); // Why 500ms?
}After
#define MAX_SAFE_PRESSURE_KPA 1034
#define RELIEF_VALVE_ID 0
#define VALVE_FULLY_OPEN 255
#define VALVE_SETTLE_TIME_MS 500
if (pressure > MAX_SAFE_PRESSURE_KPA) {
set_valve(RELIEF_VALVE_ID, VALVE_FULLY_OPEN);
delay_ms(VALVE_SETTLE_TIME_MS);
}The code is now self-documenting. No comments needed because the constants explain the intent.Technique 3: Replace Nested Conditions with Guard Clauses
Before — Deep Nesting
int process_packet(const uint8_t *data, int len) {
if (data != NULL) {
if (len >= MIN_PACKET_SIZE) {
if (data[0] == HEADER_BYTE) {
if (verify_checksum(data, len)) {
// Finally, process the actual data
handle_payload(&data[4], len - 5);
return 0;
} else {
return -4;
}
} else {
return -3;
}
} else {
return -2;
}
} else {
return -1;
}
}After — Guard Clauses
int process_packet(const uint8_t *data, int len) {
if (data == NULL) return -1;
if (len < MIN_PACKET_SIZE) return -2;
if (data[0] != HEADER_BYTE) return -3;
if (!verify_checksum(data, len)) return -4;
handle_payload(&data[4], len - 5);
return 0;
}Guard clauses handle error cases early and return immediately. The main logic flows straight down without nesting. This is easier to read and less error-prone.Technique 4: Introduce Explaining Variable
Before
if ((data[0] & 0x80) && ((data[1] < 1000) && !(flags & 0x04)) {
trigger_alarm();
}After
bool is_critical = (data[0] & 0x80) != 0;
int pressure = (data[1] < MAX_PRESSURE && !alarm_disabled) {
trigger_alarm();
}Each sub-expression gets a name. The conditional reads like English.Technique 5: Consolidate Duplicate Code
Before
void send_temperature(float t) {
char buf[64];
snprintf(buf, sizeof(buf), "TEMP:%.1frn", t);
uart_send_string(buf);
}
void send_humidity(float h) {
char buf[64];
snprintf(buf, sizeof(buf), "HUM:%.1frn", h);
uart_send_string(buf);
}
void send_pressure(float p) {
char buf[64];
snprintf(buf, sizeof(buf), "PRES:%.1frn", p);
uart_send_string(buf);
}After
void send_reading(const char *label, float value) {
char buf[64];
snprintf(buf, sizeof(buf), "%s:%.1frn", label, value);
uart_send_string(buf);
}
// Usage
send_reading("TEMP", temperature);
send_reading("HUM", humidity);
send_reading("PRES", pressure);Refactoring Checklist
- Does the function do more than one thing? → Extract Function
- Are there unexplained numbers? → Replace Magic Numbers
- Is the code deeply nested? → Guard Clauses
- Is a complex expression hard to read? → Explaining Variable
- Are multiple functions nearly identical? → Consolidate
- Is a function longer than your screen? → Break it up
Key Takeaways
- Refactoring improves code structure without changing behavior
- Make small, safe changes — one technique at a time
- Test after each refactoring step to ensure nothing broke
- The five most useful refactorings: Extract Function, Replace Magic Numbers, Guard Clauses, Explaining Variable, Consolidate Duplicates

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.







