How to Refactor C Code: From Simple Scripts to Object‑Oriented and Functional Styles
This article walks through a step‑by‑step refactoring of a small C program that reads employee data, adjusts salaries, and writes results, illustrating how to identify repeated code, extract reusable abstractions, apply object‑oriented and functional techniques, and even explore dynamic scope and data‑structure replacement for cleaner, more maintainable software.
1. Introduction
Software development has evolved from functions and sub‑routines to object‑oriented classes, dynamic libraries, and now cloud‑native micro‑services. The essence of all these advances is reuse at different granularities. The goal of writing code should be to turn repetitive work—both business scenarios and code patterns—into reusable solutions.
2. Problem Statement
The task is to write a program that reads a file work.txt containing employee records (name, age, salary), increases the salary of anyone earning less than 30,000 by 3,000, prints the results before and after the adjustment, and writes the updated data back to the file.
William 35 25000
Kishore 41 35000
Wallace 37 30000
Bruce 39 29999Read the file and output the salaries.
Increase salaries below 30,000 by 3,000.
Print the adjusted salaries.
Save the new data back to work.txt.
3. Testing
A shell script check.sh is provided to initialize work.txt, run the compiled program, and compare both standard output and the file contents with the expected results. The script prints PASS if everything matches.
#!/bin/sh
if [ $# -eq 0 ]; then
echo "usage: $0 <c-source-file>" >&2
exit -1
fi
input=$(cat <<EOF
William 35 25000
Kishore 41 35000
Wallace 37 30000
Bruce 39 29999
EOF)
output=$(cat <<EOF
William 35 28000
Kishore 41 35000
Wallace 37 30000
Bruce 39 32999
EOF)
echo "$input" > work.txt
... (compile and diff) ...4. Maintainable Code – First Version (It works)
The initial implementation uses plain ANSI C99. It reads the file into an array of structs, adjusts salaries, prints before and after, and writes the file.
#include <stdio.h>
int main(void) {
struct { char name[8]; int age; int salary; } e[4];
FILE *istream = fopen("work.txt", "r");
for (int i = 0; i < 4; i++) {
fscanf(istream, "%s%d%d", e[i].name, &e[i].age, &e[i].salary);
printf("%s %d %d
", e[i].name, e[i].age, e[i].salary);
if (e[i].salary < 30000) e[i].salary += 3000;
}
fclose(istream);
FILE *ostream = fopen("work.txt", "w");
for (int i = 0; i < 4; i++) {
printf("%s %d %d
", e[i].name, e[i].age, e[i].salary);
fprintf(ostream, "%s %d %d
", e[i].name, e[i].age, e[i].salary);
}
fclose(ostream);
return 0;
}Issues identified:
Magic constants (e.g., the number 4).
Repeated file name literals.
Poor variable names ( e, i).
Lack of error handling for file operations.
5. Object‑Oriented Style – Second to Fifth Versions
To improve clarity, the code is refactored into an employee object and a list object. Functions such as employee_read, employee_print, employee_adjust_salary, and their list counterparts ( employees_read, employees_print, etc.) are introduced. Visibility macros public and private (mapped to static) simulate encapsulation.
typedef struct _Employee { char name[8]; int age; int salary; } *Employee;
public void employee_print(Employee e, FILE *out) {
fprintf(out, "%s %d %d
", e->name, e->age, e->salary);
}
public Employee employee_read(FILE *in) {
Employee e = calloc(1, sizeof(struct _Employee));
if (fscanf(in, "%s%d%d", e->name, &e->age, &e->salary) != 3) {
free(e);
return NULL;
}
return e;
}The main function now orchestrates the workflow by calling high‑level list functions, keeping the application layer clean.
6. Functional Programming – Sixth to Seventh Versions
Repeated loops are abstracted into higher‑order functions. An EmployeeFn type is defined ( typedef void (*EmployeeFn)(Employee)) and used by employees_each to apply a function to every employee.
private void employees_each(Employees list, EmployeeFn fn) {
for (int i = 0; i < RECORD_COUNT; i++) {
fn(list[i]);
}
}To handle functions with different signatures, a more generic function pointer type is introduced:
typedef Employee (*EmployeeFn)(Employee, FILE *);and a employees_map function that can return a new list element after applying the supplied function.
7. Dynamic Scope and Context Wrappers – Eighth Version
Parameter explosion is mitigated by using a wrapper that temporarily redirects stdin / stdout with freopen, dup, and fdopen. The wrapper restores the original streams after the operation, allowing functions like employees_input and employees_output to omit explicit FILE * parameters.
private Employees file_with(String name, String mode, Employees list, EmployeesFn fn) {
int saved = dup(mode[0] == 'r' ? 0 : 1);
FILE *stream = freopen(name, mode, mode[0] == 'r' ? stdin : stdout);
if (!stream) { perror("open"); exit(EXIT_FAILURE); }
list = fn(list);
fclose(stream);
fdopen(saved, mode);
return list;
}8. Data‑Structure Replacement – Ninth Version
The fixed‑size array is replaced with a singly linked list, demonstrating that a change in the underlying data structure should not affect the application layer if proper abstractions are in place.
typedef struct _Employees {
Employee employee;
struct _Employees *next;
} *Employees;
private Employees employees_map(Employees list, EmployeeFn fn) {
for (Employees p = list; p; p = p->next) {
p->employee = fn(p->employee);
}
return list;
}All higher‑level functions ( employees_print, employees_adjust_salary, etc.) continue to work unchanged.
9. Summary
The article demonstrates iterative refactoring from a monolithic C program to a clean, layered design using object‑oriented concepts, functional higher‑order functions, dynamic scope wrappers, and flexible data structures. It shows that both OO and functional paradigms can coexist to improve code reuse and maintainability.
10. Tips
Coding standards: Consistent style makes hidden duplication visible.
Refactoring order: Start with the smallest duplication (constants, variables) and work up to whole modules.
Pass the baton: Use macros or code generation to avoid repetitive boiler‑plate when similar functions differ only by naming.
11. Appendix I – Common Lisp Solution
In Lisp, the same task can be expressed in a few lines because the language treats code as data and provides built‑in list handling.
;; Read the file
(defparameter employees
(with-open-file (f #P"work.lisp")
(read f)))
;; Print original data
(print employees)
;; Adjust salaries
(dolist (e employees)
(when (< (third e) 30000)
(incf (third e) 3000)))
;; Print adjusted data
(print employees)
;; Save back to file
(with-open-file (f #P"work.lisp" :direction :output :if-exists :overwrite)
(print employees f))The accompanying work.lisp file simply contains a list of employee records:
((William 35 25000)
(Kishore 41 35000)
(Wallace 37 30000)
(Bruce 39 29999))This demonstrates how higher‑level languages can eliminate much of the boiler‑plate required in C.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
