Skip to content

Couplers — Practice Tasks

12 hands-on exercises across the four Couplers, with full solutions.


Task 1 — Feature Envy (Java)

Problem: move the method to where it belongs.

class Order {
    private List<LineItem> items;
    public List<LineItem> getItems() { return items; }
}

class LineItem {
    private BigDecimal unitPrice;
    private int quantity;
    public BigDecimal getUnitPrice() { return unitPrice; }
    public int getQuantity() { return quantity; }
}

class ReportGenerator {
    public BigDecimal totalForLine(LineItem item) {
        return item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity()));
    }
}

Solution:

class LineItem {
    private BigDecimal unitPrice;
    private int quantity;

    public BigDecimal subtotal() {
        return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
}

// ReportGenerator no longer needs totalForLine — callers use item.subtotal().

Task 2 — Hide Delegate for Message Chain (Java)

Problem: hide the chain.

String city = customer.getCurrentOrder().getShippingAddress().getCity();

Solution:

class Customer {
    public String shippingCity() {
        return currentOrder.shippingCity();
    }
}

class Order {
    public String shippingCity() {
        return shippingAddress.city();
    }
}

// Caller:
String city = customer.shippingCity();

Task 3 — Remove Middle Man (Java)

Problem: the wrapper does pure forwarding.

class CustomerWrapper {
    private final Customer customer;

    public String getName() { return customer.getName(); }
    public String getEmail() { return customer.getEmail(); }
    public String getPhone() { return customer.getPhone(); }
    public LocalDate getDob() { return customer.getDob(); }
    public String getCountry() { return customer.getCountry(); }
}

Solution: delete CustomerWrapper. Use Customer directly.

If the wrapper was meant to add behavior in the future, defer creation until that future arrives.


Task 4 — Tell, Don't Ask (Python)

Problem: caller does the work.

def withdraw(account, amount, audit_log):
    if account.balance >= amount:
        account.balance -= amount
        audit_log.record("withdraw", account.id, amount)
        return True
    return False

Solution:

class Account:
    def withdraw(self, amount, audit_log) -> bool:
        if self.balance < amount:
            return False
        self.balance -= amount
        audit_log.record("withdraw", self.id, amount)
        return True

# Caller:
account.withdraw(100, audit_log)

Task 5 — Inappropriate Intimacy via subclass (Java)

Problem: subclass reaches into parent's protected fields.

abstract class Form {
    protected List<Field> fields = new ArrayList<>();
    protected Map<String, String> data = new HashMap<>();
    protected ValidationError currentError;
}

class LoginForm extends Form {
    public void submit() {
        // Direct field manipulation
        data.put("username", currentUser());
        data.put("token", generateToken());
        if (data.get("token") == null) {
            currentError = new ValidationError("token failed");
        }
    }
}

Solution: composition + accessors.

class FormState {
    private final List<Field> fields = new ArrayList<>();
    private final Map<String, String> data = new HashMap<>();
    private ValidationError error;

    public void setField(String name, String value) { data.put(name, value); }
    public String getField(String name) { return data.get(name); }
    public void recordError(ValidationError e) { this.error = e; }
}

class LoginForm {
    private final FormState state;

    public LoginForm() { this.state = new FormState(); }

    public void submit() {
        state.setField("username", currentUser());
        state.setField("token", generateToken());
        if (state.getField("token") == null) {
            state.recordError(new ValidationError("token failed"));
        }
    }
}

The intimacy is gone — LoginForm uses FormState via methods, not direct fields.


Task 6 — Move Method (Go)

Problem:

type Order struct {
    Items []LineItem
}

type Calculator struct{}

func (c *Calculator) ComputeTotal(o *Order) float64 {
    total := 0.0
    for _, item := range o.Items {
        total += item.UnitPrice * float64(item.Quantity)
    }
    return total
}

Solution:

type Order struct {
    Items []LineItem
}

func (o *Order) Total() float64 {
    total := 0.0
    for _, item := range o.Items {
        total += item.UnitPrice * float64(item.Quantity)
    }
    return total
}

// Calculator removed. Caller: order.Total()

Task 7 — Demeter compliance (Java)

Problem: rewrite to comply with Demeter's law.

class TaxCalculator {
    public BigDecimal calculate(Order order) {
        return order.getCustomer().getAddress().getCountry().getTaxRate().multiply(order.getSubtotal());
    }
}

Solution:

class TaxCalculator {
    public BigDecimal calculate(Order order) {
        return order.taxRate().multiply(order.subtotal());
    }
}

class Order {
    public BigDecimal taxRate() { return customer.taxRate(); }
}

class Customer {
    public BigDecimal taxRate() { return address.taxRate(); }
}

class Address {
    public BigDecimal taxRate() { return country.getTaxRate(); }
}

Each level talks to its immediate neighbor. The chain is hidden behind the methods.


Task 8 — Eliminate bidirectional intimacy (Python)

Problem:

class Department:
    def __init__(self):
        self.employees = []

    def add(self, employee):
        self.employees.append(employee)
        employee._department = self  # ! reaches into employee

    def remove(self, employee):
        self.employees.remove(employee)
        employee._department = None

class Employee:
    def __init__(self):
        self._department = None

    def transfer_to(self, new_department):
        if self._department:
            self._department.employees.remove(self)  # ! reaches into department
        new_department.employees.append(self)
        self._department = new_department

Solution: unidirectional + service.

class Department:
    def __init__(self):
        self.employees = []

class Employee:
    def __init__(self):
        pass  # no back-reference

class HRService:
    def transfer(self, employee, from_dept, to_dept):
        from_dept.employees.remove(employee)
        to_dept.employees.append(employee)

    def department_of(self, employee, all_departments):
        for d in all_departments:
            if employee in d.employees:
                return d
        return None

Each class has a clean responsibility. The HRService coordinates without intimate access.


Task 9 — Refactor away Middle Man (Go)

Problem:

type ClientWrapper struct {
    real *http.Client
}

func (w *ClientWrapper) Get(url string) (*http.Response, error) {
    return w.real.Get(url)
}

func (w *ClientWrapper) Post(url string, body io.Reader) (*http.Response, error) {
    return w.real.Post(url, "application/json", body)
}

func (w *ClientWrapper) Put(url string, body io.Reader) (*http.Response, error) {
    return w.real.Do(...)
}
// ... 10 more straight forwards

Solution: delete the wrapper. If you need to add behavior (like default headers), make a constructor:

func NewClient() *http.Client {
    return &http.Client{
        Timeout: 30 * time.Second,
        Transport: &headerInjectingTransport{
            base: http.DefaultTransport,
            headers: map[string]string{"User-Agent": "MyApp/1.0"},
        },
    }
}

The "wrapper" is now configuration on *http.Client itself. No new type.


Task 10 — Convert ask to tell (Java)

Problem:

public boolean canSubmit(Order order) {
    if (order.getItems().isEmpty()) return false;
    if (order.getCustomer() == null) return false;
    if (order.getCustomer().getEmail() == null) return false;
    if (order.getTotal().compareTo(BigDecimal.ZERO) <= 0) return false;
    return true;
}

Solution:

class Order {
    public boolean canBeSubmitted() {
        return !items.isEmpty()
            && customer != null
            && customer.hasEmail()
            && total.signum() > 0;
    }
}

class Customer {
    public boolean hasEmail() {
        return email != null && !email.isBlank();
    }
}

Each class encapsulates its own readiness check. Caller asks one question, not five.


Task 11 — Identify the Couplers (Java)

Problem: find all Couplers in this code.

class Cart {
    public List<Item> items;        // public!
    public Customer customer;

    public BigDecimal total;
}

class CheckoutHelper {
    public BigDecimal computeTotal(Cart cart) {
        BigDecimal total = BigDecimal.ZERO;
        for (Item item : cart.items) {
            total = total.add(item.price.multiply(BigDecimal.valueOf(item.qty)));
        }
        return total;
    }

    public String getCustomerCity(Cart cart) {
        return cart.customer.profile.address.city;
    }

    public boolean isVip(Cart cart) {
        return cart.customer.tier.value > 1000;
    }
}

Solution:

Smell Where
Inappropriate Intimacy All fields on Cart are public; CheckoutHelper reaches in directly
Feature Envy computeTotal, isVip operate on Cart's data — should be methods on Cart
Message Chain cart.customer.profile.address.city

Cures combined:

class Cart {
    private final List<Item> items;
    private final Customer customer;

    public BigDecimal total() {
        return items.stream()
            .map(Item::subtotal)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public String shippingCity() {
        return customer.shippingCity();
    }

    public boolean isVip() {
        return customer.isVip();
    }
}

class Item {
    public BigDecimal subtotal() {
        return price.multiply(BigDecimal.valueOf(qty));
    }
}

class Customer {
    public String shippingCity() {
        return profile.shippingCity();
    }
    public boolean isVip() {
        return tier.isVip();
    }
}

// CheckoutHelper deleted (or reduced to genuine cross-class coordination).

Task 12 — Architectural Coupler (System Design)

Problem: describe a service architecture and its Coupler smells.

[Web App] → [API Gateway] → [User Service] → [Profile Service] → [Database]
                                   [Notification Service] → [Database]
                                                    [Audit Service]

The User Service makes 3 synchronous downstream calls per request. The Notification Service writes to its own DB and also calls Audit Service.

Smells:

  • Message Chain at architectural level: API Gateway → User → Profile → DB is a 4-hop chain.
  • Inappropriate Intimacy if Notification & User share a database (not stated, but a common anti-pattern).
  • Middle Man if API Gateway only forwards without auth, rate-limit, or transformation.

Cures:

  • Reduce chain depth: can the User Service include Profile data in its response by querying the DB directly? (If they're in the same bounded context.) Or, expose Profile data via events that User Service subscribes to.
  • Async the Audit: Notification publishes an event; Audit subscribes. Notification's response time is no longer dependent on Audit.
  • Verify Gateway value-add: if it does auth + rate-limiting, keep. If pure forwarding, cut.

After:

[Web App] → [API Gateway (auth, rate-limit)] → [User Service]
                                          ↓ events
                                   [Notification Service]
                                          ↓ events  
                                    [Audit Service]

Sync chain depth: 2. The rest is asynchronous.


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