4:Refactoring
Quick recap
1. Deep vs shallow modules
Agenda
- Refactoring
- Code smells
- Common refactoring techniques
Refactoring
Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
Refactoring (verb): to restructure software by applying a series of refactorings without changing its observable behavior.
Why refactor?
- improves the design of software
- makes software easier to understand
- helps find bugs
- helps program faster
Code smells
“If it stinks, change it.”
— Grandma Beck, discussing child-rearing philosophy
- It"s easier to explain how to do refactoring than when.
- No hard and fast rules
- Comes with practice and intuition
Long function
- Small functions live longer → easier to read, maintain, and reuse.
- Modern languages remove call overhead → no excuse for long functions.
- Good naming reduces need to read body.
- Rule of thumb: When you feel like adding a comment → Extract a Function.
- Not about length → about semantic distance between intent and implementation(what vs. how).
💬 Refactor for lower semantic distance
// says *how* — high semantic distance:
for (int i = 0; i < users.size(); i++)
if (users[i].age >= 18) eligible.push_back(users[i]);
Refactored — says what
vector<User> filter_adults(const vector<User>& users) {
vector<User> eligible;
for (const auto& user : users)
if (user.age >= 18) eligible.push_back(user);
return eligible;
}
auto eligible = filter_adults(users);
filter_adults) so the call site reads like English.
- The reader no longer needs to understand the loop body to know why it exists.
- Long functions = Bad smell → aggressively decompose into meaningful, named functions.
Long function — in your code
🔍 From your snake games
-
krishadoshi16/Reptile-rush—Snake_game_final_code.cpp:13-310main(), 298 lines — window creation, snake construction, food spawning, bonus-food timing, obstacle placement, font loading, button layout, direction state, the event loop, collision detection, scoring, the render pass and the game-over screen, all in one un-named scope. The author has already marked nine extraction boundaries with// ---------- Section ----------banners. That is the rule of thumb above — "when you feel like adding a comment → Extract a Function" — firing nine times in one function.
-
sagar-dot-bera/ByteHebi—source/game.cpp:315-738Game::render(), 424 lines — the longest function in the corpus. Board borders, gradient snake colouring, the HUD and three separate dialog modes in one body. Its mirror image,processInput, is another 205 lines.
Neither is a smell because it is long. Both are a smell because the semantic distance between "draw one frame" and 424 lines of terminal calls is more than a reader can hold in their head.
🔧 How would you fix these?
- Reptile-rush → Extract Function at each banner. The nine comments
become nine names:
createWindow(),initSnake(),spawnFood(),placeObstacles(),layoutButtons(),drawFrame().main()shrinks to ~15 lines that read like a table of contents. - ByteHebi → Split Phase first: compute what the frame should show, then emit the terminal calls. Only then Extract Function per region (border, snake, HUD, each dialog).
- Order matters: slide related statements together before extracting — you can only extract what is already adjacent.
- Stop when each function fits one screen and its name says what, not how.
Duplicate code
- Duplication = more reading, harder changes, higher bug risk.
- Every copy must be checked → wasted effort.
- Guiding principle: Short, meaningful, unified functions = healthier codebase.
Duplicate code — in your code
🔍 From your snake games
JagratJani/snake-game-cpp—src/Food.cpp:18-82- The copy that already drifted. The rejection-sampling loop that finds
a free cell was copied from
GenerateWithObstaclesintoGenerateSpecialFood— and the author documented the copy in a comment instead of removing it:
- The copy that already drifted. The rejection-sampling loop that finds
a free cell was copied from
void Food::GenerateSpecialFood(const Snake& snake) {
// Generate position first (same logic as regular food)
bool invalidPosition;
do {
invalidPosition = false;
x = rand() % maxX;
y = rand() % maxY;
...
} while (invalidPosition);
The copy dropped the obstacle check, so special food can spawn inside an obstacle and regular food cannot. Nobody decided that; the duplicate drifted.
Piyushtanwani/Snake-game—main.cpp:357-543- The same four lines, seven times. The head/body/food/empty glyph chain
is written out seven times inside one
render(), once per row that needed a different right-hand margin. Rendering logic was duplicated to accommodate a layout difference.
- The same four lines, seven times. The head/body/food/empty glyph chain
is written out seven times inside one
Both dissolve the same way: Extract Function (findFreeCell, cellGlyph),
then call it from every site.
A third kind — versioning by file copy.
202512057Meetsheth/Debug-Thugs
ships part1.cpp … part5.cpp plus part52.cpp: six standalone programs, each
an evolved copy of the last (122 → 223 → 290 → 322 → 436 lines, and part5.cpp
is headed //Part 5 (final)), with ~84 duplicated blocks between them. Three
other repos do the same. Branches and tags are how you keep versions — copying
the file is how you lose track of which one is real.
🔧 How would you fix these?
- snake-game-cpp → Extract Function
findFreeCell(snake, obstacles); both generators call it. Pass obstacles as a parameter, so "special food ignores obstacles" has to be a decision someone types, not an accident of copying. - Snake-game → Extract
cellGlyph(x, y), then one loop over all rows, with the margin text pulled from a lookup table (rowMargin[y]). The layout difference stops duplicating the rendering logic. - Do this first: make the copies identical before extracting. If you extract over a drifted copy, you silently ship a behaviour change — here, special food would suddenly start respecting obstacles.
- Then decide which behaviour was correct. Duplication hid the question; removing it forces the answer.
Divergent Change
- Goal of structure: Make change easy → one clear place to modify.
- Smell: When one module changes for different reasons (e.g., database vs. financial logic).
- Problem: Mixed contexts → every change touches unrelated code → harder to understand & maintain.
- Better design: Separate contexts into distinct modules/classes.
Divergent Change — in your code
🔍 From your snake games
Dazzling-Darshan/SnakeByte—game.h:51-138- One class, five reasons to change — and the history is annotated in
the source. Every wave of requirements left a
// New:marker behind:
- One class, five reasons to change — and the history is annotated in
the source. Every wave of requirements left a
pair<int,int> poisonFood; // New: Poison food
pair<int,int> shield; // New: Shield power-up
bool paused; // New: Pause state
const int SHIELD_SPAWN_INTERVAL = 45; // Changed: ...to 60 seconds
`class Game` changes for
- new collectibles,
- for rendering,
- for high-score,
- for file I/O,
- for pause semantics, and for difficulty tuning.
Adding one collectible means touching the member list, three declarations, spawn,
update, the draw chain **and** the pause-timer compensation. The
`// Changed: ...to 60 seconds` comment sitting above a literal `45` is
itself evidence of how hard the class is to keep coherent.
GarvModi18/SnakeGameProject—snake.cpp:1-1028- One file, six concerns, 1028 lines — ANSI console plumbing, Windows audio, high-score persistence, six UI screens, rendering and the game rules, all over shared mutable globals. Changing the audio backend, the save-file format or the collision rules all land in the same file.
Ask of any module: what kinds of change bring me here? More than one answer is the smell.
🔧 How would you fix these?
- Split by reason to change, not by size. A 1000-line file with one reason is fine; a 100-line class with five is not.
- SnakeByte → a
Collectibleinterface with one implementation per pickup (food, poison, shield, special) + aHighScoreStorefor the file I/O + aClockthat owns pause compensation. Adding a pickup becomes one new class, not seven edits. - SnakeGameProject → split
snake.cppintoconsole,audio,highscore,ui,render,rules. Replace the shared mutable globals with aGameStatepassed explicitly — otherwise the split is cosmetic and every module still reaches into everything. - Test you did it right: adding a new power-up should touch one file.
Shotgun Surgery
- Smell: Opposite of Divergent Change.
- Every change → many small edits across multiple classes/modules.
- Hard to track, easy to miss updates.
Shotgun Surgery — in your code
🔍 From your snake games
-
GarvModi18/SnakeGameProject—snake.cpp431-440·527-536·596-604- One new menu key, three edits. The arrow-key block —
kbhit(),getch(), the72/80scancodeswitch— is repeated verbatim inShowSettingsPage()(388),ShowMenu()(469) andShowPauseOverlay()(554). All three are live in the same running program, so binding one new key means finding and correctly editing all three.
- One new menu key, three edits. The arrow-key block —
-
Neel1585/Snake_Game—level.cpp:10-31·obstacle.cpp:9-29- Non-blocking input, copied per level.
getchNonBlocking()— 20 lines oftermiosplus aselect()— is defined identically in both levels the README ships (g++ level.cpp -o level,g++ obstacle.cpp -o obstacle). Fixing terminal restore on Ctrl-C is two edits, and the copies have already diverged cosmetically, which defeats diffing.
- Non-blocking input, copied per level.
snake.cpp appeared under Divergent Change as well: it changes for six
unrelated reasons and one menu change takes three edits inside it. The two
smells are opposite ends of one defect — the duplication is why the change is
scattered.
🔧 How would you fix these?
- SnakeGameProject → Extract Function
readMenuKey()returning an enum (Up,Down,Select,Back); all three screens call it. A new binding becomes one edit in one place. - Snake_Game → move
getchNonBlocking()intoinput.hand include it from both levels. Better still: the two levels are ~90% the same file — make them one program with a level parameter. - General recipe: Move Function / Move Field to pull the scattered pieces into one module, then Inline whatever is left over.
- Don't over-correct. Sweeping everything into one "utils" module just trades this smell for Divergent Change. Group by what changes together, not by what is left over.
Comments
- Comments are good → but often used as deodorant to hide bad code.
- Heavy comments usually signal underlying bad smells.
- First step: Refactor → often eliminates the need for comments. When comments help
- To explain why something is done (not what).
- To flag uncertainty or future concerns. Principle: Write clean code that explains itself — use comments for intent, not explanation.
Comments — in your code
🔍 From your snake games
-
mshaikh19/Snake-Game-Project—GameGrid.cpp:314-349- Commented-out code used as version control. An entire previous version
of the HUD banner — 36 lines, same box-drawing art, one column wider —
sits commented out directly below the live version. Four more dead blocks
live in the same file.
drawGameGridis 166 lines, 45 of them comments, most of that a commented-out duplicate of the code above it. The repo is under git; the history already holds the old version.
- Commented-out code used as version control. An entire previous version
of the HUD banner — 36 lines, same box-drawing art, one column wider —
sits commented out directly below the live version. Four more dead blocks
live in the same file.
-
krishadoshi16/Reptile-rush—Snake_game_final_code.cpp:24-234- Comments standing in for function names. Nine
// ---------- Section ----------banners inside a single 298-linemain— each one a function name that was never written:createWindow(),initSnake(),spawnFood(),placeObstacles(),layoutButtons(),drawFrame(). Refactor first, and the comments delete themselves.
- Comments standing in for function names. Nine
The contrasting good case, from Dazzling-Darshan/SnakeByte
game.h:10 —
#include <fstream> // Required for file I/O (High Score). That one explains
why. Compare with // New: Poison food a few lines below it: six months on,
everything is "new".
🔧 How would you fix these?
- Delete commented-out code.
git logis your version control. Dead code costs you a re-read every time and is never the version you want back. - Banner comments → function names (Extract Function). The comment becomes the signature and stops being able to go stale.
// New: X→ nothing.git blamealready answers "when". If the comment tracks a diff rather than an intent, it expires.- Keep comments that answer why: a non-obvious constraint, a workaround and its cause, a warning about what looks safe but isn't.
- Rule of thumb: a comment describing what the next lines do is a function waiting to be extracted.
Common refactoring techniques
Extract function

💬 Can you extract a function from this cluttered body?
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
//print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
After Extract Function
function printOwing(invoice) {
function printDetails(outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
printBanner();
let outstanding = calculateOutstanding();
printDetails(outstanding);
}
Inline function
💬 Is this helper function adding clarity — or indirection?
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
After Inline Function
function getRating(driver) {
return (driver.numberOfLateDeliveries > 5) ? 2 : 1;
}
-
Inverse of
Extract Function -
When to Use
- Function’s body = just as clear as its name.
- Too much delegation → hard to trace logic.
- Want to regroup/refactor functions.
-
Mechanics
- Ensure it’s not polymorphic (no subclass overrides).
- Find all callers.
- Replace call with body → test after each.
- Remove function definition.
- Inline gradually if tricky (multiple returns, recursion).
- Principle: Indirection is good only when it adds clarity — inline when it doesn’t.
Slide statements
💬 Are related statements grouped together?
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
After Slide Statements
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;
pricingPlan and its consumer (chargePerUnit) are now adjacent → easier to extract into a function later.
- Problem
- Related code is scattered, mixed with unrelated logic.
- Harder to understand, modify, or extract into functions.
- Solution
- Move related statements together so intent is clearer.
- Often a preparatory step for Extract Function. Principle: Group related logic together → clarity first, then refactor further.
Replace temp with query
💬 Can you eliminate the temp variable?
const basePrice = this._quantity * this._itemPrice;
return basePrice > 1000 ? basePrice * 0.95 : basePrice * 0.98;
After Replace Temp with Query
getBasePrice() {
return this._quantity * this._itemPrice;
}
// call function instead of temp variable
return this.getBasePrice() > 1000 ? this.getBasePrice() * 0.95 : this.basePrice * 0.98;
Motivation - Easier function extraction (no temps to pass around). - Stronger boundaries → fewer dependencies & side effects. - Eliminates duplicate calculation logic. - Best inside a class (shared context for queries).
Split Loop
💬 This loop does too many things at once — how do you split it?
let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
averageAge += p.age;
totalSalary += p.salary;
}
averageAge = averageAge / people.length;
After Split Loop
let totalSalary = 0;
for (const p of people) {
totalSalary += p.salary;
}
let averageAge = 0;
for (const p of people) {
averageAge += p.age;
}
averageAge = averageAge / people.length;
Problem
- One loop doing multiple things at once.
- Makes modifications harder → must understand all behaviors.
- Leads to cluttered code with multiple outputs/side effects.
Solution
- Split loop into separate loops, each doing one task.
- Improves clarity and maintainability.
- Often followed by Extract Function on each loop.
Split phase
💬 This code mixes parsing and pricing — can you separate the concerns?
const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
After Split Phase
function parseOrder(aString) {
const values = aString.split(/\s+/);
return ({productID: values[0].split("-")[1], quantity: parseInt(values[1])});
}
function price(order, priceList) {
return order.quantity * priceList[order.productID];
}
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);
Clues
- Different parts use different data/functions.
- Sequential steps doing significantly different work.
Mechanics
1. Extract second phase into its own function.
2. Add intermediate data structure (passed between phases).
3. Move relevant parameters/fields into the structure.
4. Extract first phase separately.
Principle: Separate concerns into phases → easier to reason about, test, and extend.
References
- Chapter 1, Refactoring, Second edition by Martin Fowler and Kent Beck
- Refactoring example in Javascript
About the examples
Every "in your code" link above is pinned to a commit SHA, not a branch, so it keeps pointing at the code as it was when this lecture was written — your later commits will not shift the line numbers out from under it. All examples were read and verified against the source, not inferred from metrics.
Nothing here is a mark against the author. Every one of these appears in code that works; they are the shapes that make the next change expensive, which is exactly what refactoring is for.