Skip to content
Home » Software Design » Refactoring Techniques in C — With Practical Examples

Refactoring Techniques in C — With Practical Examples

Refactoring Techniques featured image with purple background, Practical badge, Rf icon, 5 Essential Methods in C subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 21 of 31View Full Path →

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

Leave a Reply

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