Skip to main content

Code Katas

Six before/after refactor katas. Each targets one canonical move from the smell catalog. For every kata, the workflow is:

  1. Read the "before" snippet and name the smell(s) present.
  2. Predict the refactoring move before looking at the "after."
  3. Implement the move in the language you prefer.
  4. 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.