OO Abusers — Practice Tasks¶
12 hands-on exercises across the four OO Abusers, with full solutions.
Task 1 — Switch Statements (Java)¶
Problem: Replace this type-code switch with polymorphism.
class Notification {
String type; // "email", "sms", "push"
String to;
String content;
}
class NotificationSender {
void send(Notification n) {
switch (n.type) {
case "email": emailGateway.send(n.to, n.content); break;
case "sms": smsGateway.send(n.to, n.content); break;
case "push": pushGateway.send(n.to, n.content); break;
default: throw new IllegalStateException();
}
}
}
Solution:
sealed interface Notification permits EmailNotification, SmsNotification, PushNotification {
void send();
}
record EmailNotification(String to, String content, EmailGateway gateway) implements Notification {
public void send() { gateway.send(to, content); }
}
record SmsNotification(String to, String content, SmsGateway gateway) implements Notification {
public void send() { gateway.send(to, content); }
}
record PushNotification(String to, String content, PushGateway gateway) implements Notification {
public void send() { gateway.send(to, content); }
}
class NotificationSender {
void send(Notification n) { n.send(); }
}
Task 2 — Switch with State (Java)¶
Problem: Apply Replace Type Code with State to model an order's status with per-state behavior.
class Order {
String status; // "DRAFT", "PAID", "SHIPPED", "DELIVERED"
public List<String> allowedActions() {
switch (status) {
case "DRAFT": return List.of("ADD_ITEM", "REMOVE_ITEM", "PAY", "CANCEL");
case "PAID": return List.of("SHIP", "REFUND");
case "SHIPPED": return List.of("MARK_DELIVERED");
case "DELIVERED": return List.of("REVIEW");
default: return List.of();
}
}
}
Solution:
sealed interface OrderState permits Draft, Paid, Shipped, Delivered {
List<String> allowedActions();
}
final class Draft implements OrderState {
public List<String> allowedActions() {
return List.of("ADD_ITEM", "REMOVE_ITEM", "PAY", "CANCEL");
}
}
final class Paid implements OrderState {
public List<String> allowedActions() { return List.of("SHIP", "REFUND"); }
}
final class Shipped implements OrderState {
public List<String> allowedActions() { return List.of("MARK_DELIVERED"); }
}
final class Delivered implements OrderState {
public List<String> allowedActions() { return List.of("REVIEW"); }
}
class Order {
private OrderState state = new Draft();
public List<String> allowedActions() { return state.allowedActions(); }
}
Task 3 — Refused Bequest (Java)¶
Problem: Penguin extends Bird is a textbook Refused Bequest. Refactor with Replace Inheritance with Delegation.
abstract class Bird {
String name;
public abstract void fly();
}
class Eagle extends Bird {
public void fly() { System.out.println(name + " soars."); }
}
class Penguin extends Bird {
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
Solution:
abstract class Bird {
protected final String name;
protected Bird(String name) { this.name = name; }
}
interface FlyBehavior { void fly(String name); }
class Soars implements FlyBehavior {
public void fly(String name) { System.out.println(name + " soars."); }
}
class CannotFly implements FlyBehavior {
public void fly(String name) { throw new UnsupportedOperationException(name + " can't fly"); }
}
class Eagle extends Bird {
private final FlyBehavior flying = new Soars();
public Eagle(String name) { super(name); }
public void fly() { flying.fly(name); }
}
class Penguin extends Bird {
// No fly() method at all — can't be called by mistake.
public Penguin(String name) { super(name); }
public void swim() { System.out.println(name + " swims."); }
}
Task 4 — Temporary Field (Java)¶
Problem: Refactor to remove the temporary fields.
class GraphSearch {
private Graph graph;
private Map<Node, Node> previous; // only set during shortestPath
private Map<Node, Integer> distance; // ditto
public List<Node> shortestPath(Node from, Node to) {
previous = new HashMap<>();
distance = new HashMap<>();
// dijkstra implementation using previous and distance
return reconstructPath(to);
}
private List<Node> reconstructPath(Node target) {
// uses previous
...
}
}
Solution:
class GraphSearch {
private final Graph graph;
public List<Node> shortestPath(Node from, Node to) {
return new SearchOperation(graph, from, to).execute();
}
}
class SearchOperation {
private final Graph graph;
private final Node from, to;
private final Map<Node, Node> previous = new HashMap<>();
private final Map<Node, Integer> distance = new HashMap<>();
SearchOperation(Graph graph, Node from, Node to) {
this.graph = graph;
this.from = from;
this.to = to;
}
List<Node> execute() {
// dijkstra
return reconstructPath();
}
private List<Node> reconstructPath() {
// uses previous
...
}
}
Task 5 — Switch + Type Code (Python)¶
Problem: Replace the type-code dispatch with polymorphism.
class Animal:
def __init__(self, kind):
self.kind = kind
def speak(animal):
if animal.kind == "dog":
return "Woof"
elif animal.kind == "cat":
return "Meow"
elif animal.kind == "cow":
return "Moo"
Solution:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self) -> str: ...
class Dog(Animal):
def speak(self): return "Woof"
class Cat(Animal):
def speak(self): return "Meow"
class Cow(Animal):
def speak(self): return "Moo"
# usage:
def speak(animal: Animal) -> str:
return animal.speak()
Task 6 — Alternative Classes (Java)¶
Problem: These two classes do similar work with unrelated APIs. Extract a common interface.
class FileLogger {
public void writeMessage(String msg) { ... }
public void flushAll() { ... }
}
class CloudLogger {
public void uploadEvent(String event) { ... }
public void sync() { ... }
}
Solution:
interface Logger {
void log(String message);
void flush();
}
class FileLogger implements Logger {
public void log(String message) { /* was writeMessage */ }
public void flush() { /* was flushAll */ }
}
class CloudLogger implements Logger {
public void log(String message) { /* was uploadEvent */ }
public void flush() { /* was sync */ }
}
// Callers can now polymorphically use either:
void process(Logger logger) {
logger.log("Started");
// ...
logger.flush();
}
Task 7 — Replace Conditional with Polymorphism in Go¶
Problem: Replace the type-switch with proper interface dispatch.
type Employee struct {
Type string // "salaried", "hourly", "commission"
Salary float64
Hours int
Rate float64
Sales float64
Comm float64
}
func PayAmount(e Employee) float64 {
switch e.Type {
case "salaried":
return e.Salary
case "hourly":
return float64(e.Hours) * e.Rate
case "commission":
return e.Sales * e.Comm
}
return 0
}
Solution:
type PayCalculator interface {
PayAmount() float64
}
type Salaried struct{ Salary float64 }
func (s Salaried) PayAmount() float64 { return s.Salary }
type Hourly struct {
Hours int
Rate float64
}
func (h Hourly) PayAmount() float64 { return float64(h.Hours) * h.Rate }
type Commission struct {
Sales, Rate float64
}
func (c Commission) PayAmount() float64 { return c.Sales * c.Rate }
func PayAmount(e PayCalculator) float64 { return e.PayAmount() }
Task 8 — Introduce Null Object (Java)¶
Problem: Eliminate the null checks by introducing a null object.
class Customer {
private DiscountPlan plan; // sometimes null
public BigDecimal apply(BigDecimal amount) {
if (plan == null) return amount;
return plan.apply(amount);
}
}
interface DiscountPlan {
BigDecimal apply(BigDecimal amount);
}
Solution:
interface DiscountPlan {
BigDecimal apply(BigDecimal amount);
static DiscountPlan none() {
return amount -> amount; // identity
}
}
class Customer {
private DiscountPlan plan = DiscountPlan.none();
public BigDecimal apply(BigDecimal amount) {
return plan.apply(amount); // no null check
}
}
Task 9 — Refused Bequest in Python¶
Problem: Refactor.
class Stream:
def read(self): ...
def write(self, data): ...
class ReadOnlyFile(Stream):
def __init__(self, path): self.path = path
def read(self): return open(self.path).read()
def write(self, data):
raise NotImplementedError("read-only")
Solution:
from typing import Protocol
class Readable(Protocol):
def read(self) -> str: ...
class Writable(Protocol):
def write(self, data: str) -> None: ...
class ReadOnlyFile:
def __init__(self, path):
self.path = path
def read(self) -> str:
return open(self.path).read()
# No write() at all — can't be misused.
class ReadWriteFile:
def __init__(self, path):
self.path = path
def read(self) -> str: ...
def write(self, data: str) -> None: ...
# Functions that need only reading take a Readable:
def consume(source: Readable):
print(source.read())
mypy --strict rejects passing a ReadOnlyFile to a function expecting a Writable.
Task 10 — Pattern matching with sealed types (Java 21+)¶
Problem: Convert this nested instanceof chain to a sealed-type pattern match.
double processPayment(Payment p) {
if (p instanceof CashPayment cp) {
return cp.getAmount();
} else if (p instanceof CreditCardPayment ccp) {
return ccp.getAmount() - ccp.getProcessingFee();
} else if (p instanceof CryptoPayment crp) {
return crp.getAmount() * (1 - crp.getNetworkFee());
}
throw new IllegalStateException();
}
Solution:
sealed interface Payment permits CashPayment, CreditCardPayment, CryptoPayment {}
record CashPayment(BigDecimal amount) implements Payment {}
record CreditCardPayment(BigDecimal amount, BigDecimal processingFee) implements Payment {}
record CryptoPayment(BigDecimal amount, BigDecimal networkFee) implements Payment {}
double processPayment(Payment p) {
return switch (p) {
case CashPayment(var amount) -> amount.doubleValue();
case CreditCardPayment(var amount, var fee) -> amount.subtract(fee).doubleValue();
case CryptoPayment(var amount, var fee) -> amount.multiply(BigDecimal.ONE.subtract(fee)).doubleValue();
};
} // exhaustive — compiler enforces
Task 11 — Find the OO Abusers (Java)¶
Problem: Identify all OO Abusers in this code.
class Notification {
String type;
String content;
String to;
String emailSubject; // only when type="email"
String emailBody; // only when type="email"
String smsCarrier; // only when type="sms"
String pushDeviceToken; // only when type="push"
void send() {
switch (type) {
case "email":
emailService.send(to, emailSubject, emailBody);
break;
case "sms":
smsService.send(to, content, smsCarrier);
break;
case "push":
pushService.send(pushDeviceToken, content);
break;
}
}
}
Solution:
| Smell | Where |
|---|---|
| Switch Statements | send() switches on type |
| Temporary Field | emailSubject, emailBody, smsCarrier, pushDeviceToken are null for most types |
| (related: Primitive Obsession) | String type is a type code |
Cure (combined):
sealed interface Notification permits Email, Sms, Push {
void send();
}
record Email(String to, String subject, String body) implements Notification {
public void send() { emailService.send(to, subject, body); }
}
record Sms(String to, String content, String carrier) implements Notification {
public void send() { smsService.send(to, content, carrier); }
}
record Push(String deviceToken, String content) implements Notification {
public void send() { pushService.send(deviceToken, content); }
}
Each notification type now has only its relevant fields. No temporary fields. No type-code switch.
Task 12 — Capability-based design (Go)¶
Problem: This struct embeds *os.File to get IO methods, but exposes ALL of them. Restructure to expose only Read and Close.
Solution:
type ReadOnlyFile struct {
f *os.File // not embedded — private
}
func Open(path string) (*ReadOnlyFile, error) {
f, err := os.Open(path)
if err != nil { return nil, err }
return &ReadOnlyFile{f: f}, nil
}
func (r *ReadOnlyFile) Read(p []byte) (int, error) { return r.f.Read(p) }
func (r *ReadOnlyFile) Close() error { return r.f.Close() }
// No Write, Seek, Sync — caller can't access them.
This is Go's idiomatic answer to Refused Bequest: replace embedding with explicit forwarding for only the methods you want to expose.
Next: find-bug.md — buggy snippets where OO Abusers hide subtle bugs.