Skip to content

Dispensables — Practice Tasks

12 hands-on exercises across 6 Dispensables, with full solutions.


Task 1 — Comments → self-explanatory code (Java)

Problem: replace comments with extracted methods.

public BigDecimal computeFinalPrice(Item item, Customer c) {
    // Step 1: get base price
    BigDecimal price = item.getBasePrice();

    // Step 2: apply category discount
    if (item.getCategory() == Category.BOOK) {
        price = price.multiply(new BigDecimal("0.95"));
    }

    // Step 3: apply customer tier discount
    if (c.getTier() == Tier.GOLD) {
        price = price.multiply(new BigDecimal("0.90"));
    }

    // Step 4: apply tax
    price = price.multiply(new BigDecimal("1.0875"));

    return price;
}

Solution:

public BigDecimal computeFinalPrice(Item item, Customer c) {
    BigDecimal price = item.getBasePrice();
    price = applyCategoryDiscount(price, item);
    price = applyTierDiscount(price, c);
    price = applyTax(price);
    return price;
}

private BigDecimal applyCategoryDiscount(BigDecimal price, Item item) {
    return item.getCategory() == Category.BOOK
        ? price.multiply(new BigDecimal("0.95"))
        : price;
}

private BigDecimal applyTierDiscount(BigDecimal price, Customer c) {
    return c.getTier() == Tier.GOLD
        ? price.multiply(new BigDecimal("0.90"))
        : price;
}

private BigDecimal applyTax(BigDecimal price) {
    return price.multiply(new BigDecimal("1.0875"));
}

Task 2 — Extract Method for Duplicate Code (Python)

Problem: eliminate duplication.

def compute_order_totals(orders):
    total = 0
    for o in orders:
        line_total = o.unit_price * o.quantity
        if o.discount: line_total *= (1 - o.discount)
        total += line_total
    return total

def compute_quote_totals(quotes):
    total = 0
    for q in quotes:
        line_total = q.unit_price * q.quantity
        if q.discount: line_total *= (1 - q.discount)
        total += line_total
    return total

Solution:

def compute_total(items):
    return sum(line_total(i) for i in items)

def line_total(item):
    raw = item.unit_price * item.quantity
    return raw * (1 - item.discount) if item.discount else raw

# Usage:
total = compute_total(orders)  # or quotes

Task 3 — Inline Class (Java)

Problem: inline the trivial wrapper.

class TaxRate {
    private final BigDecimal rate;
    public TaxRate(BigDecimal rate) { this.rate = rate; }
    public BigDecimal applyTo(BigDecimal amount) {
        return amount.multiply(rate);
    }
}

class Order {
    public BigDecimal computeTax(BigDecimal subtotal) {
        return new TaxRate(new BigDecimal("0.0875")).applyTo(subtotal);
    }
}

Solution:

class Order {
    private static final BigDecimal TAX_RATE = new BigDecimal("0.0875");

    public BigDecimal computeTax(BigDecimal subtotal) {
        return subtotal.multiply(TAX_RATE);
    }
}

TaxRate is gone. The constant lives where it's used.


Task 4 — Move Method (Anemic to Rich Domain) (Java)

Problem: move logic from service onto domain class.

class Order {
    public OrderStatus status;
    public LocalDate placedAt;
    public BigDecimal total;
    // only getters/setters
}

class OrderService {
    public boolean canCancel(Order o) {
        return o.getStatus() == OrderStatus.PENDING ||
               o.getStatus() == OrderStatus.CONFIRMED;
    }
    public boolean isOverdue(Order o) {
        return o.getStatus() == OrderStatus.SHIPPED &&
               o.getPlacedAt().isBefore(LocalDate.now().minusDays(7));
    }
    public boolean isHighValue(Order o) {
        return o.getTotal().compareTo(new BigDecimal("1000")) > 0;
    }
}

Solution:

class Order {
    private OrderStatus status;
    private LocalDate placedAt;
    private BigDecimal total;
    // ... constructors, accessors

    public boolean canCancel() {
        return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED;
    }
    public boolean isOverdue() {
        return status == OrderStatus.SHIPPED &&
               placedAt.isBefore(LocalDate.now().minusDays(7));
    }
    public boolean isHighValue() {
        return total.compareTo(new BigDecimal("1000")) > 0;
    }
}

OrderService retains only operations that genuinely cross multiple domain objects.


Task 5 — Delete Dead Code (Java)

Problem: identify and delete dead code.

public class OrderProcessor {
    private static final boolean LEGACY_MODE = false;

    public void processOrder(Order o) {
        if (LEGACY_MODE) {
            legacyProcess(o);
            return;
        }
        modernProcess(o);
    }

    @Deprecated
    private void legacyProcess(Order o) {
        // 200 lines of pre-2020 logic
    }

    private void modernProcess(Order o) {
        // Current logic
    }

    // chargeViaPaypalOld(o);  // commented 2 years ago

    private boolean checkPaymentLegacy(Order o) {  // not called anywhere
        return o.getStatus() == OrderStatus.PAID;
    }
}

Solution:

public class OrderProcessor {
    public void processOrder(Order o) {
        modernProcess(o);
    }

    private void modernProcess(Order o) {
        // Current logic
    }
}

Deleted: LEGACY_MODE constant (always false), legacyProcess (unreachable), commented call (irrelevant), checkPaymentLegacy (no callers). Git remembers if needed.


Task 6 — Collapse Hierarchy (Speculative Generality) (Java)

Problem: the abstract class has only one subclass.

abstract class Notification {
    protected String to;
    protected String content;

    public abstract void send();
}

class EmailNotification extends Notification {
    public void send() {
        emailService.send(to, content);
    }
}

// No other subclass — used in only one place.

Solution:

class EmailNotification {
    private String to;
    private String content;

    public EmailNotification(String to, String content) {
        this.to = to;
        this.content = content;
    }

    public void send() {
        emailService.send(to, content);
    }
}

Hierarchy collapsed. If a second notification kind appears later, then extract a base class.


Task 7 — Form Template Method (Duplicate Code in Hierarchy) (Java)

Problem: two subclasses have similar logic with one varying step.

class CsvReport {
    public void generate(Data d) {
        validate(d);
        Output out = serializeCsv(d);
        save(out);
        notify(out);
    }
    private Output serializeCsv(Data d) { ... }
    // validate, save, notify identical to JsonReport
}

class JsonReport {
    public void generate(Data d) {
        validate(d);
        Output out = serializeJson(d);
        save(out);
        notify(out);
    }
    private Output serializeJson(Data d) { ... }
}

Solution (Form Template Method):

abstract class Report {
    public final void generate(Data d) {
        validate(d);
        Output out = serialize(d);
        save(out);
        notify(out);
    }

    protected abstract Output serialize(Data d);

    private void validate(Data d) { ... }
    private void save(Output out) { ... }
    private void notify(Output out) { ... }
}

class CsvReport extends Report {
    protected Output serialize(Data d) { ... }
}
class JsonReport extends Report {
    protected Output serialize(Data d) { ... }
}

The varying step is the abstract method; the template (in the parent) is shared. Cures Duplicate Code.


Task 8 — Encapsulate Field (Data Class) (Python)

Problem: convert from public-fields style to behavior-rich.

class Account:
    def __init__(self):
        self.balance = 0
        self.transactions = []

# Usage scattered:
account.balance += 100
account.transactions.append({"type": "deposit", "amount": 100})

account.balance -= 50
account.transactions.append({"type": "withdraw", "amount": 50})

Solution:

class Account:
    def __init__(self):
        self._balance = 0
        self._transactions = []

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        self._balance += amount
        self._transactions.append({"type": "deposit", "amount": amount})

    def withdraw(self, amount):
        if amount > self._balance:
            raise InsufficientFundsError()
        self._balance -= amount
        self._transactions.append({"type": "withdraw", "amount": amount})

    def history(self):
        return list(self._transactions)

The class enforces invariants (no overdraft) and keeps balance + transaction-log consistent.


Task 9 — Substitute Algorithm (Duplicate Code with Different Implementations) (Java)

Problem:

class StringUtils {
    // Two ways to check palindrome — pick one
    public static boolean isPalindrome1(String s) {
        for (int i = 0; i < s.length() / 2; i++) {
            if (s.charAt(i) != s.charAt(s.length() - 1 - i)) return false;
        }
        return true;
    }

    public static boolean isPalindrome2(String s) {
        return s.equals(new StringBuilder(s).reverse().toString());
    }
}

Solution:

class StringUtils {
    public static boolean isPalindrome(String s) {
        for (int i = 0; i < s.length() / 2; i++) {
            if (s.charAt(i) != s.charAt(s.length() - 1 - i)) return false;
        }
        return true;
    }
    // Faster (no allocation, early exit). Drop isPalindrome2.
}

Pick based on performance, readability, or both. Delete the loser.


Task 10 — Remove Speculative Hooks (Java)

Problem: the workflow has unused hooks.

abstract class Workflow {
    protected void preProcess() {}      // 0 subclasses override
    protected void postProcess() {}     // 0 subclasses override
    protected void onError(Exception e) {}  // 0 subclasses override
    protected void onCancel() {}        // 0 subclasses override

    public final void run() {
        preProcess();
        try {
            execute();
            postProcess();
        } catch (Exception e) {
            onError(e);
            throw e;
        }
    }

    protected abstract void execute();
}

Solution:

abstract class Workflow {
    public final void run() {
        execute();  // simple
    }

    protected abstract void execute();
}

If a hook is needed in the future, add it then. Speculative hooks accumulate cognitive load without payback.


Task 11 — Eliminate Lazy package (Go)

Problem:

package taxcalc

import "math/big"

type Calculator struct {
    rate *big.Float
}

func NewCalculator(rate *big.Float) *Calculator {
    return &Calculator{rate: rate}
}

func (c *Calculator) Apply(amount *big.Float) *big.Float {
    out := new(big.Float)
    return out.Mul(amount, c.rate)
}

// One function, used in one place.

Solution:

package main

func ApplyTax(amount, rate *big.Float) *big.Float {
    out := new(big.Float)
    return out.Mul(amount, rate)
}

// Or just inline at the call site:
// out := new(big.Float).Mul(amount, taxRate)

The package was overhead. Promote the function to where it's used or just inline.


Task 12 — Audit a method for all Dispensables (Java)

Problem: find every Dispensable.

class CustomerProcessor {

    // CustomerProcessor processes customers. (1)

    private static final boolean USE_LEGACY = false;  // (2)

    /**
     * Process the customer. Step 1: validate. Step 2: enrich. Step 3: save.
     */ // (3)
    public void process(Customer c) {
        // validate
        if (c == null) throw new IllegalArgumentException();
        if (c.getName() == null) throw new IllegalArgumentException();

        // enrich
        c.setProcessedAt(LocalDateTime.now());

        // save
        if (USE_LEGACY) {
            legacySave(c);  // (2 cont)
        } else {
            modernSave(c);
        }
    }

    private void legacySave(Customer c) { /* dead */ }  // (4)

    /** @deprecated */ public void processOld(Customer c) {}  // (5)

    // Maybe future: a hook for processing
    protected void preProcess(Customer c) {}  // (6)
}

Solution:

Issue Smell Cure
(1) Useless class-purpose comment Comments Delete
(2) USE_LEGACY = false Dead Code Delete the constant + the legacy branch
(3) Method-level Javadoc as step list Comments (what-comments) Delete; extract validate, enrich, save methods
(4) legacySave Dead Code Delete
(5) processOld deprecated method Dead Code (likely) Delete after deprecation period
(6) preProcess empty hook Speculative Generality Delete

After cleanup:

class CustomerProcessor {
    public void process(Customer c) {
        validate(c);
        enrich(c);
        save(c);
    }

    private void validate(Customer c) {
        if (c == null) throw new IllegalArgumentException();
        if (c.getName() == null) throw new IllegalArgumentException();
    }

    private void enrich(Customer c) {
        c.setProcessedAt(LocalDateTime.now());
    }

    private void save(Customer c) {
        modernSave(c);
    }

    private void modernSave(Customer c) { ... }
}

Or further, if modernSave is the only save: rename to save and delete the indirection.


Next: find-bug.md — bugs hiding in dispensable code.