Code Katas
Six before/after refactor katas. Each targets one canonical move from the smell catalog. For every kata, the workflow is:
- Read the "before" snippet and name the smell(s) present.
- Predict the refactoring move before looking at the "after."
- Implement the move in the language you prefer.
- Write one sentence of justification: which principle or smell is addressed, and why this move is the right answer.
Examples below use Python and Java for readability; translate freely.
Kata 1: Replace Conditional with Polymorphism
Time limit: 20 minutes
Target smell: Repeated Switch / Primitive Obsession on a type code.
Before:
def charge(amount, kind, card=None, bank=None, wallet=None):
if kind == "credit":
return card.process(amount)
elif kind == "bank":
return bank.transfer(amount)
elif kind == "wallet":
return wallet.pay(amount)
raise ValueError(kind)
After (sketch):
class PaymentMethod:
def charge(self, amount): raise NotImplementedError
class CreditCard(PaymentMethod):
def __init__(self, card): self.card = card
def charge(self, amount): return self.card.process(amount)
class BankTransfer(PaymentMethod):
def __init__(self, bank): self.bank = bank
def charge(self, amount): return self.bank.transfer(amount)
class WalletPayment(PaymentMethod):
def __init__(self, wallet): self.wallet = wallet
def charge(self, amount): return self.wallet.pay(amount)
def charge(method: PaymentMethod, amount): return method.charge(amount)
Repeat until: You can translate any switch-on-kind into a polymorphic family in under 10 minutes and justify when it is the wrong move.
Kata 2: Introduce Parameter Object
Time limit: 15 minutes
Target smell: Data Clumps.
Before:
void schedule(String firstName, String lastName, int year, int month, int day, String city, String zip) { ... }
After:
record Person(String firstName, String lastName) {}
record BirthDate(int year, int month, int day) {}
record Address(String city, String zip) {}
void schedule(Person person, BirthDate birth, Address address) { ... }
Repeat until: You automatically flag groups of three-or-more parameters that appear together twice as a Data Clump candidate.
Kata 3: Extract Class
Time limit: 20 minutes
Target smell: Large Class / Divergent Change.
Before:
class Employee:
def __init__(self, name, street, city, zip_code):
self.name = name
self.street = street
self.city = city
self.zip_code = zip_code
def label(self): return f"{self.name}, {self.street}, {self.city} {self.zip_code}"
def zip_prefix(self): return self.zip_code[:3]
After:
class Address:
def __init__(self, street, city, zip_code):
self.street, self.city, self.zip_code = street, city, zip_code
def __str__(self): return f"{self.street}, {self.city} {self.zip_code}"
def zip_prefix(self): return self.zip_code[:3]
class Employee:
def __init__(self, name, address: Address):
self.name, self.address = name, address
def label(self): return f"{self.name}, {self.address}"
Repeat until: You can extract a class when you see three or more fields that belong together and the extraction reduces Employee's method count.
Kata 4: Move Function (Feature Envy)
Time limit: 15 minutes
Target smell: Feature Envy.
Before:
class Invoice {
double totalWithTax(Customer c) {
double rate = (c.country().equals("US") ? TaxRules.rateFor(c.state(), c.zip()) : TaxRules.defaultRate());
return subtotal * (1 + rate);
}
}
After:
class Customer {
double taxRate() {
return country().equals("US") ? TaxRules.rateFor(state(), zip()) : TaxRules.defaultRate();
}
}
class Invoice {
double totalWithTax(Customer c) { return subtotal * (1 + c.taxRate()); }
}
Repeat until: You can diagnose a Feature Envy method (more getters on C than fields of self) and move the behavior in one pass.
Kata 5: Replace Primitive with Object
Time limit: 20 minutes
Target smell: Primitive Obsession.
Before:
def charge(card_number: str, amount_cents: int, currency: str): ...
After:
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class CardNumber:
digits: str
def __post_init__(self):
if len(self.digits) not in (15, 16) or not self.digits.isdigit():
raise ValueError("invalid card number")
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def plus(self, other):
if self.currency != other.currency: raise ValueError("currency mismatch")
return Money(self.amount + other.amount, self.currency)
def charge(card: CardNumber, total: Money): ...
Repeat until: Validation, arithmetic, and formatting all live on the domain type, not spread across call sites.
Kata 6: Inline Speculative Abstraction (YAGNI)
Time limit: 15 minutes
Target smell: Speculative Generality.
Before:
public interface UserNameFormatter { String format(User u); }
public class DefaultUserNameFormatter implements UserNameFormatter {
public String format(User u) { return u.firstName() + " " + u.lastName(); }
}
// Exactly one implementation, one caller.
After:
public final class UserNameFormatter {
public static String format(User u) { return u.firstName() + " " + u.lastName(); }
}
Repeat until: You reflexively look for "interface with exactly one implementation and no roadmap item for a second" and inline it.
Kata 7: Encapsulate Collection
Time limit: 15 minutes
Target smell: Encapsulation leak.
Before:
class Team {
public List<Player> players = new ArrayList<>();
}
// elsewhere: team.players.add(newPlayer); team.players.sort(...);
After:
class Team {
private final List<Player> players = new ArrayList<>();
public void add(Player p) { validate(p); players.add(p); }
public List<Player> roster() { return List.copyOf(players); }
}
Repeat until: You reflexively wrap any mutable collection exposed through a public field.
Kata 8: Replace Conditional with Guard Clauses + Extract
Time limit: 15 minutes
Target smell: Long Method with nested conditionals.
Before:
def discount(order):
if order.customer is not None:
if order.customer.is_member:
if order.total > 100:
return order.total * 0.10
else:
return order.total * 0.05
else:
return 0
else:
return 0
After:
def discount(order):
if order.customer is None or not order.customer.is_member:
return 0
return member_discount(order)
def member_discount(order):
return order.total * (0.10 if order.total > 100 else 0.05)
Repeat until: You write guard clauses before nested if, and you extract the happy path into a named function whenever indentation exceeds two levels.
Completion Standard
- Each kata completed within its time limit.
- For each kata, you named the target smell before looking at the "after."
- You can explain the move in terms of a specific principle (SRP / OCP / LSP / DIP / encapsulation / YAGNI).
- You can describe at least one scenario in which the kata's move would be the wrong answer.