added base examples and drills

This commit is contained in:
2026-05-20 17:42:01 -06:00
parent 9bf788d9ef
commit 4fc0d851e7
40 changed files with 802 additions and 1 deletions
+41 -1
View File
@@ -1,3 +1,43 @@
# refactor-kata
These are various practice drills for practicing refactoring. Using "Working With Legacy Code" by Feathers and "Refactoring" by Fowler
Practice drills for refactoring, especially with ideas from *Working Effectively with Legacy Code* by Michael Feathers and *Refactoring* by Martin Fowler.
## Repository layout
- `kata/<example-name>/original/`
- the frozen messy starting point
- copy from here when creating a new drill scenario
- `kata/<example-name>/scenarios/<scenario-name>/code/`
- the working copy for a specific drill attempt
- `kata/<example-name>/scenarios/<scenario-name>/drill-prompt.md`
- instructions and success conditions for that drill
- `kata/<example-name>/scenarios/<scenario-name>/notes.md`
- notes on seams found, tests added, techniques used, and lessons learned
- `notes/`
- cross-kata notes and recurring patterns
## Intended workflow
1. Start with code in `original/`.
2. Copy that code into a scenario's `code/` folder.
3. Do the refactor only inside that scenario copy.
4. Record what you observed and learned in `notes.md`.
5. Reset by making a fresh scenario from `original/`.
## Current starter kata
- `kata/example-seam-drill/`
- a placeholder kata for practicing:
- characterization tests
- breaking one dependency with a seam
- extracting one pure decision from mixed logic
## Goal
The point is not to build a production app. The point is to practice:
- finding seams in messy code
- adding characterization tests safely
- making small reviewable refactors
- separating decisions from mechanics
- gradually moving code toward stronger boundaries
+3
View File
@@ -0,0 +1,3 @@
# auth original
Messy login logic mixing lockout policy, clock handling, token generation, mutation, logging, and file I/O.
+36
View File
@@ -0,0 +1,36 @@
const fs = require("fs");
function login(filePath, username, password) {
const raw = fs.readFileSync(filePath, "utf8");
const state = JSON.parse(raw);
const user = state.users.find((item) => item.username === username);
if (!user) {
throw new Error("User not found");
}
if (user.lockedUntil && new Date(user.lockedUntil).getTime() > Date.now()) {
return { ok: false, reason: "locked" };
}
if (user.password !== password) {
user.failedAttempts = user.failedAttempts + 1;
if (user.failedAttempts >= 3) {
user.lockedUntil = new Date(Date.now() + 15 * 60 * 1000).toISOString();
}
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
return { ok: false, reason: "invalid-password" };
}
user.failedAttempts = 0;
user.lockedUntil = null;
user.lastLoginAt = new Date().toISOString();
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
console.log("Logged in", username);
return { ok: true, token: "token-" + username + "-" + Date.now() };
}
module.exports = { login };
+11
View File
@@ -0,0 +1,11 @@
{
"users": [
{
"username": "ada",
"password": "secret",
"failedAttempts": 0,
"lockedUntil": null,
"lastLoginAt": null
}
]
}
+3
View File
@@ -0,0 +1,3 @@
# billing original
Messy invoice finalization logic mixing pricing rules, time, logging, mutation, and file I/O.
+39
View File
@@ -0,0 +1,39 @@
const fs = require("fs");
function finalizeInvoice(filePath, invoiceId, customerType) {
const raw = fs.readFileSync(filePath, "utf8");
const state = JSON.parse(raw);
const invoice = state.invoices.find((item) => item.id === invoiceId);
if (!invoice) {
throw new Error("Invoice not found");
}
let subtotal = 0;
for (const line of invoice.lines) {
subtotal = subtotal + line.quantity * line.unitPrice;
}
let discount = 0;
if (customerType === "vip") {
discount = subtotal * 0.15;
}
if (subtotal > 200) {
discount = discount + 10;
}
const taxed = (subtotal - discount) * 1.1;
invoice.status = "finalized";
invoice.total = Math.round(taxed * 100) / 100;
invoice.finalizedAt = new Date().toISOString();
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
console.log("Invoice finalized", invoiceId);
return invoice;
}
module.exports = { finalizeInvoice };
+14
View File
@@ -0,0 +1,14 @@
{
"invoices": [
{
"id": "inv-1",
"status": "draft",
"total": 0,
"finalizedAt": null,
"lines": [
{ "description": "Hosting", "quantity": 2, "unitPrice": 60 },
{ "description": "Support", "quantity": 1, "unitPrice": 95 }
]
}
]
}
+18
View File
@@ -0,0 +1,18 @@
# example-seam-drill
This kata is a placeholder example showing the intended practice layout.
## Structure
- `original/`: the frozen messy source you copy from for each drill
- `scenarios/<scenario-name>/code/`: a working copy for one drill attempt
- `scenarios/<scenario-name>/drill-prompt.md`: the drill instructions
- `scenarios/<scenario-name>/notes.md`: what you observed, tried, and learned
## Suggested workflow
1. Put the messy starting code in `original/`.
2. Copy it into a scenario's `code/` directory.
3. Do the drill only inside that scenario copy.
4. Record seam choices, tests added, and refactor steps in `notes.md`.
5. Reset by creating a new scenario from `original/`.
@@ -0,0 +1,17 @@
# original
This is intentionally messy starter code.
## Why it is messy
- mixes file I/O, business rules, time, and logging in one method
- uses raw strings and generic objects instead of stronger primitives
- hard-codes reward logic inline
- depends directly on `new Date()` and `fs`
- mutates loaded state directly
## Suggested drill targets
- characterize current completion behavior
- break the file-system or clock dependency with a seam
- extract the points or badge decision into a pure function
@@ -0,0 +1,12 @@
{
"name": "example-seam-drill",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
@@ -0,0 +1,62 @@
const fs = require("fs");
class TaskService {
constructor(filePath) {
this.filePath = filePath;
}
completeTask(taskName, userName) {
const raw = fs.readFileSync(this.filePath, "utf8");
const state = JSON.parse(raw);
const task = state.tasks.find((item) => item.name === taskName);
if (!task) {
throw new Error("Task not found");
}
if (task.completed) {
return {
message: "Task already complete",
pointsAwarded: 0,
badge: null
};
}
task.completed = true;
task.completedAt = new Date().toISOString();
let pointsAwarded = 10;
if (userName && userName.toLowerCase() === "ada") {
pointsAwarded = pointsAwarded + 5;
}
if (new Date().getDay() === 5) {
pointsAwarded = pointsAwarded * 2;
}
state.points = state.points + pointsAwarded;
let badge = null;
if (state.points > 50) {
badge = "consistency-star";
}
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2));
if (badge) {
console.log("Awarded badge:", badge);
}
return {
message: "Task completed",
pointsAwarded,
badge
};
}
}
module.exports = {
TaskService
};
@@ -0,0 +1,15 @@
{
"points": 45,
"tasks": [
{
"name": "Write daily summary",
"completed": false,
"completedAt": null
},
{
"name": "Review inbox",
"completed": true,
"completedAt": "2026-05-18T08:00:00.000Z"
}
]
}
@@ -0,0 +1,17 @@
# original
This is intentionally messy starter code.
## Why it is messy
- mixes file I/O, business rules, time, and logging in one method
- uses raw strings and generic objects instead of stronger primitives
- hard-codes reward logic inline
- depends directly on `new Date()` and `fs`
- mutates loaded state directly
## Suggested drill targets
- characterize current completion behavior
- break the file-system or clock dependency with a seam
- extract the points or badge decision into a pure function
@@ -0,0 +1,12 @@
{
"name": "example-seam-drill",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
@@ -0,0 +1,62 @@
const fs = require("fs");
class TaskService {
constructor(filePath) {
this.filePath = filePath;
}
completeTask(taskName, userName) {
const raw = fs.readFileSync(this.filePath, "utf8");
const state = JSON.parse(raw);
const task = state.tasks.find((item) => item.name === taskName);
if (!task) {
throw new Error("Task not found");
}
if (task.completed) {
return {
message: "Task already complete",
pointsAwarded: 0,
badge: null
};
}
task.completed = true;
task.completedAt = new Date().toISOString();
let pointsAwarded = 10;
if (userName && userName.toLowerCase() === "ada") {
pointsAwarded = pointsAwarded + 5;
}
if (new Date().getDay() === 5) {
pointsAwarded = pointsAwarded * 2;
}
state.points = state.points + pointsAwarded;
let badge = null;
if (state.points > 50) {
badge = "consistency-star";
}
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2));
if (badge) {
console.log("Awarded badge:", badge);
}
return {
message: "Task completed",
pointsAwarded,
badge
};
}
}
module.exports = {
TaskService
};
@@ -0,0 +1,15 @@
{
"points": 45,
"tasks": [
{
"name": "Write daily summary",
"completed": false,
"completedAt": null
},
{
"name": "Review inbox",
"completed": true,
"completedAt": "2026-05-18T08:00:00.000Z"
}
]
}
@@ -0,0 +1,12 @@
# Drill: break-dependency
## Goal
Introduce one seam that reduces coupling to a hard dependency.
## Prompt
Find one hard dependency in `code/` such as time, randomness, persistence, network, UI, or global state. Introduce the smallest seam you can so that behavior can be controlled more easily in tests.
## Success conditions
- One dependency is isolated behind a seam
- The change is small and reviewable
- You can explain the enabling point and why it is safer to change now
@@ -0,0 +1,9 @@
# Notes
## Dependency chosen
## Seam introduced
## Why this seam
## What I learned
@@ -0,0 +1,17 @@
# original
This is intentionally messy starter code.
## Why it is messy
- mixes file I/O, business rules, time, and logging in one method
- uses raw strings and generic objects instead of stronger primitives
- hard-codes reward logic inline
- depends directly on `new Date()` and `fs`
- mutates loaded state directly
## Suggested drill targets
- characterize current completion behavior
- break the file-system or clock dependency with a seam
- extract the points or badge decision into a pure function
@@ -0,0 +1,12 @@
{
"name": "example-seam-drill",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
@@ -0,0 +1,62 @@
const fs = require("fs");
class TaskService {
constructor(filePath) {
this.filePath = filePath;
}
completeTask(taskName, userName) {
const raw = fs.readFileSync(this.filePath, "utf8");
const state = JSON.parse(raw);
const task = state.tasks.find((item) => item.name === taskName);
if (!task) {
throw new Error("Task not found");
}
if (task.completed) {
return {
message: "Task already complete",
pointsAwarded: 0,
badge: null
};
}
task.completed = true;
task.completedAt = new Date().toISOString();
let pointsAwarded = 10;
if (userName && userName.toLowerCase() === "ada") {
pointsAwarded = pointsAwarded + 5;
}
if (new Date().getDay() === 5) {
pointsAwarded = pointsAwarded * 2;
}
state.points = state.points + pointsAwarded;
let badge = null;
if (state.points > 50) {
badge = "consistency-star";
}
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2));
if (badge) {
console.log("Awarded badge:", badge);
}
return {
message: "Task completed",
pointsAwarded,
badge
};
}
}
module.exports = {
TaskService
};
@@ -0,0 +1,15 @@
{
"points": 45,
"tasks": [
{
"name": "Write daily summary",
"completed": false,
"completedAt": null
},
{
"name": "Review inbox",
"completed": true,
"completedAt": "2026-05-18T08:00:00.000Z"
}
]
}
@@ -0,0 +1,12 @@
# Drill: characterization
## Goal
Capture current behavior before changing the code.
## Prompt
Add characterization tests for the current behavior of the code in `code/` without intentionally changing behavior.
## Success conditions
- At least one important behavior is captured in tests
- You can describe what the code currently does, even if the behavior is awkward
- No production behavior changes are introduced on purpose
@@ -0,0 +1,9 @@
# Notes
## Observed behavior
## Seams discovered
## Tests added
## What I learned
@@ -0,0 +1,17 @@
# original
This is intentionally messy starter code.
## Why it is messy
- mixes file I/O, business rules, time, and logging in one method
- uses raw strings and generic objects instead of stronger primitives
- hard-codes reward logic inline
- depends directly on `new Date()` and `fs`
- mutates loaded state directly
## Suggested drill targets
- characterize current completion behavior
- break the file-system or clock dependency with a seam
- extract the points or badge decision into a pure function
@@ -0,0 +1,12 @@
{
"name": "example-seam-drill",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
@@ -0,0 +1,62 @@
const fs = require("fs");
class TaskService {
constructor(filePath) {
this.filePath = filePath;
}
completeTask(taskName, userName) {
const raw = fs.readFileSync(this.filePath, "utf8");
const state = JSON.parse(raw);
const task = state.tasks.find((item) => item.name === taskName);
if (!task) {
throw new Error("Task not found");
}
if (task.completed) {
return {
message: "Task already complete",
pointsAwarded: 0,
badge: null
};
}
task.completed = true;
task.completedAt = new Date().toISOString();
let pointsAwarded = 10;
if (userName && userName.toLowerCase() === "ada") {
pointsAwarded = pointsAwarded + 5;
}
if (new Date().getDay() === 5) {
pointsAwarded = pointsAwarded * 2;
}
state.points = state.points + pointsAwarded;
let badge = null;
if (state.points > 50) {
badge = "consistency-star";
}
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2));
if (badge) {
console.log("Awarded badge:", badge);
}
return {
message: "Task completed",
pointsAwarded,
badge
};
}
}
module.exports = {
TaskService
};
@@ -0,0 +1,15 @@
{
"points": 45,
"tasks": [
{
"name": "Write daily summary",
"completed": false,
"completedAt": null
},
{
"name": "Review inbox",
"completed": true,
"completedAt": "2026-05-18T08:00:00.000Z"
}
]
}
@@ -0,0 +1,12 @@
# Drill: extract-policy
## Goal
Move one mixed rule out of orchestration code into a pure decision.
## Prompt
Find logic in `code/` that mixes decision-making with mechanics. Extract the decision into a pure function or policy with explicit inputs and outputs.
## Success conditions
- The decision is separated from mechanics
- Inputs and outputs are explicit
- You can describe the command, event, and workflow role of the affected code
@@ -0,0 +1,9 @@
# Notes
## Mixed logic found
## Pure decision extracted
## Command / event / workflow mapping
## What I learned
+3
View File
@@ -0,0 +1,3 @@
# moderation original
Messy moderation logic mixing policy rules, time, logging, mutation, and file I/O.
@@ -0,0 +1,30 @@
const fs = require("fs");
function reviewPost(filePath, postId) {
const raw = fs.readFileSync(filePath, "utf8");
const state = JSON.parse(raw);
const post = state.posts.find((item) => item.id === postId);
if (!post) {
throw new Error("Post not found");
}
let action = "approve";
if (post.text.includes("buy now") || post.text.includes("free money")) {
action = "reject";
}
if (post.reports > 3) {
action = "escalate";
}
post.status = action;
post.reviewedAt = new Date().toISOString();
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
console.log("Moderation action", postId, action);
return { action };
}
module.exports = { reviewPost };
+11
View File
@@ -0,0 +1,11 @@
{
"posts": [
{
"id": "post-1",
"text": "free money if you buy now",
"reports": 1,
"status": "pending",
"reviewedAt": null
}
]
}
+3
View File
@@ -0,0 +1,3 @@
# notifications original
Messy reminder logic mixing quiet-hours rules, channel checks, time, logging, mutation, and file I/O.
@@ -0,0 +1,33 @@
const fs = require("fs");
function sendReminder(filePath, userId, channel) {
const raw = fs.readFileSync(filePath, "utf8");
const state = JSON.parse(raw);
const user = state.users.find((item) => item.id === userId);
if (!user) {
throw new Error("User not found");
}
if (!user.remindersEnabled) {
return { sent: false, reason: "disabled" };
}
const hour = new Date().getHours();
if (hour < 8 || hour > 20) {
return { sent: false, reason: "quiet-hours" };
}
if (channel === "sms" && !user.phoneNumber) {
return { sent: false, reason: "missing-phone" };
}
user.lastReminderAt = new Date().toISOString();
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
console.log("Sent reminder", userId, channel);
return { sent: true, channel };
}
module.exports = { sendReminder };
+10
View File
@@ -0,0 +1,10 @@
{
"users": [
{
"id": "user-1",
"remindersEnabled": true,
"phoneNumber": "+15555555555",
"lastReminderAt": null
}
]
}
+3
View File
@@ -0,0 +1,3 @@
# shipping original
Messy shipping logic mixing eligibility, cost calculation, randomness, time, logging, and file I/O.
+37
View File
@@ -0,0 +1,37 @@
const fs = require("fs");
function shipOrder(filePath, orderId, destinationCountry) {
const raw = fs.readFileSync(filePath, "utf8");
const state = JSON.parse(raw);
const order = state.orders.find((item) => item.id === orderId);
if (!order) {
throw new Error("Order not found");
}
if (order.status === "shipped") {
return { message: "Already shipped", trackingNumber: order.trackingNumber };
}
let shippingCost = 5;
if (destinationCountry !== "US") {
shippingCost = shippingCost + 15;
}
if (order.weightKg > 10) {
shippingCost = shippingCost + 20;
}
order.status = "shipped";
order.shippingCost = shippingCost;
order.trackingNumber = "trk-" + Math.floor(Math.random() * 100000);
order.shippedAt = new Date().toISOString();
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
console.log("Order shipped", orderId);
return { message: "Shipped", trackingNumber: order.trackingNumber, shippingCost };
}
module.exports = { shipOrder };
+12
View File
@@ -0,0 +1,12 @@
{
"orders": [
{
"id": "ord-1",
"status": "paid",
"weightKg": 12,
"shippingCost": 0,
"trackingNumber": null,
"shippedAt": null
}
]
}
+8
View File
@@ -0,0 +1,8 @@
# Practice Notes
Use this directory for cross-kata notes:
- patterns you keep noticing
- seams that worked well
- refactor techniques that felt natural or hard
- mapping from legacy code moves to effect-template ideas