Part 01 · Low Level Design
Behavioral Patterns: Strategy, State, Command
Same job, different rules: swap an algorithm, let status drive behaviour, reuse the steps but change the details, queue and undo actions, and pass a request down a chain.
⏱ 2-3 hours · Station 8 of 80
The fare function that ate the app
A ride app's CalculateFare started as three lines. Then came bikes, then CNGs, then cars, then a night surcharge, then a Puja discount, then "premium cars in Gulshan". Today it's one function with 14 if/else branches, and every change to car fares has a chance of breaking bike fares.
The same team wants "undo" in the shopping cart of their food section, and a refund flow where small refunds are automatic, medium ones need a supervisor and large ones a manager. The first draft of both is, again, a growing if/else.
Behavioral patterns are standard ways to decide who does what when behaviour varies. Each one turns a pile of branches into small objects with one job.
The problem: behaviour that varies, written as branches
| What you see | Pattern |
|---|---|
if rideType == ... else if ... that grows with every product | Strategy |
if status == ... else if ... repeated in many methods | State |
| Several classes repeat the same steps with small differences | Template Method |
| You need undo, redo, queueing or logging of actions | Command |
| A request should be handled by the first of several handlers that can | Chain of Responsibility |
Think of a ride app, a notebook and an office
Choosing bike, CNG or car in a ride app changes how the fare is worked out; the trip itself doesn't care. That's Strategy: swap the algorithm, keep the caller.
Writing in pencil and keeping a list of what you wrote, so you can rub out the last thing: that's Command. Each action is written down as a thing that can be undone.
A refund request at an office goes to the clerk; if it's too big, to the supervisor; then to the manager. Each person handles what they're allowed to and passes the rest up. That's a Chain of Responsibility.
Where the comparison breaks down. In an office, the supervisor might bend a rule. In code, each link does exactly what it says, so the order of the chain and each link's limit are the policy; changing them is a business decision, not a refactor.
The refund chain is auto (≤ 500) → supervisor (≤ 5,000) → manager (≤ 50,000). Finance adds a new step: a 'regional head' who can approve up to 200,000, between the manager and the end. What changes?
Five patterns for varying behaviour
- Strategy: put each variant of an algorithm behind one interface (
FareStrategy.Fare(km)), and give the caller the one it needs. New variant = new type. The caller has no branches. Perfect for pricing rules, sorting rules, routing rules. - State: when an object's behaviour depends on its state and many methods would branch on it, make each state a class with the same interface (
Placed.Cancel(),Shipped.Cancel()). The object delegates to its current state object. It's the object-oriented version of the transition table from chapter 4; use the table until the states really have different behaviour. - Template Method: a base defines the fixed steps of a process (
validate → charge → notify), and variants fill in specific steps. Useful, but it relies on inheritance; in Go, Rust and modern TypeScript the same idea is usually a function that takes the variable steps as parameters (composition). - Command: turn an action into an object with
DoandUndo(and maybe a name). Then you can keep a history for undo, queue actions for later, retry them, or log exactly what happened. - Chain of Responsibility: link handlers so each one either handles a request or passes it to the next. Adding, removing or reordering steps doesn't touch the others. HTTP middleware (auth → rate limit → logging → handler) is the most common chain in real code.
- The common idea: find the part that varies, give it an interface, and move each variant into its own small object. The code that uses the behaviour stops changing.
Undo, step by step
Two adds and three undos
Step 1/5 Run a command, not a method
The caller makes AddItem{tea, 2} and hands it to cart.Run. The cart applies it and pushes it on the history.
- cart
- 2 items, 60 tk
- history
- [add tea x2]
When branches pile up
When you see the same if/else growing, ask:
- Does the algorithm vary? → Strategy.
- Does behaviour depend on the object's state, in many methods? → State (or a transition table).
- Do I need to store, undo or queue actions? → Command.
- Should the first capable handler take it? → Chain of Responsibility.
- Same steps, different details? → Template Method, or a function that takes the details as parameters.
Try it: which pattern is this?
Name the pattern
sorter := sorters[userChoice]; sorter.Sort(list)
Fares, undo and refunds in code
Strategy for fares, Command for an undoable cart, and Chain of Responsibility for refund approval.
package main
import "fmt"
// ---- Strategy: the fare rule is a swappable object; the ride doesn't care which.
type FareStrategy interface{ Fare(km int) int }
type PerKm struct{ Base, PerKm int }
func (p PerKm) Fare(km int) int { return p.Base + p.PerKm*km }
// ---- Command: each change to the cart is an object that knows how to undo itself.
var prices = map[string]int{"tea": 30, "biscuit": 20}
type Cart struct {
qty map[string]int
history []Command
}
type Command interface {
Do(c *Cart)
Undo(c *Cart)
Name() string
}
type AddItem struct {
Item string
Qty int
}
func (a AddItem) Do(c *Cart) { c.qty[a.Item] += a.Qty }
func (a AddItem) Undo(c *Cart) { c.qty[a.Item] -= a.Qty }
func (a AddItem) Name() string { return fmt.Sprintf("add %s x%d", a.Item, a.Qty) }
func (c *Cart) Run(cmd Command) { cmd.Do(c); c.history = append(c.history, cmd) }
func (c *Cart) UndoLast() bool {
if len(c.history) == 0 {
return false
}
last := c.history[len(c.history)-1]
c.history = c.history[:len(c.history)-1]
last.Undo(c)
return true
}
func (c *Cart) Summary() string {
items, total := 0, 0
for name, q := range c.qty {
items += q
total += q * prices[name]
}
return fmt.Sprintf("%d items, %d tk", items, total)
}
// ---- Chain of Responsibility: each approver handles what it can, or passes it on.
type Approver interface{ Approve(amount int) string }
type Limit struct {
Who string
Max int
Next Approver
}
func (l Limit) Approve(amount int) string {
if amount <= l.Max {
return l.Who
}
if l.Next == nil {
return "rejected, needs finance review"
}
return l.Next.Approve(amount)
}
func main() {
strategies := []struct {
name string
s FareStrategy
}{{"bike", PerKm{40, 10}}, {"car", PerKm{100, 30}}, {"cng", PerKm{40, 20}}}
for _, st := range strategies {
fmt.Printf("fare %-5s 8 km: %d tk\n", st.name, st.s.Fare(8))
}
cart := &Cart{qty: map[string]int{}}
for _, cmd := range []Command{AddItem{"tea", 2}, AddItem{"biscuit", 1}} {
cart.Run(cmd)
fmt.Printf("cart: %s -> %s\n", cmd.Name(), cart.Summary())
}
for range 3 {
if cart.UndoLast() {
fmt.Println("cart: undo ->", cart.Summary())
} else {
fmt.Println("cart: undo -> nothing to undo")
}
}
chain := Limit{"auto-approved", 500,
Limit{"approved by supervisor", 5000,
Limit{"approved by manager", 50000, nil}}}
for _, amount := range []int{300, 3000, 12000, 80000} {
fmt.Printf("refund %d tk: %s\n", amount, chain.Approve(amount))
}
}from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional, Protocol
# ---- Strategy: the fare rule is a swappable object; the ride doesn't care which.
class FareStrategy(Protocol):
def fare(self, km: int) -> int: ...
@dataclass(frozen=True)
class PerKm:
base: int
per_km: int
def fare(self, km: int) -> int:
return self.base + self.per_km * km
# ---- Command: each change to the cart is an object that knows how to undo itself.
PRICES = {"tea": 30, "biscuit": 20}
class Command(Protocol):
name: str
def do(self, cart: Cart) -> None: ...
def undo(self, cart: Cart) -> None: ...
@dataclass(frozen=True)
class AddItem:
item: str
qty: int
@property
def name(self) -> str:
return f"add {self.item} x{self.qty}"
def do(self, cart: Cart) -> None:
cart.qty[self.item] = cart.qty.get(self.item, 0) + self.qty
def undo(self, cart: Cart) -> None:
cart.qty[self.item] -= self.qty
@dataclass
class Cart:
qty: dict[str, int] = field(default_factory=dict)
history: list[Command] = field(default_factory=list)
def run(self, cmd: Command) -> None:
cmd.do(self)
self.history.append(cmd)
def undo_last(self) -> bool:
if not self.history:
return False
self.history.pop().undo(self)
return True
def summary(self) -> str:
items = sum(self.qty.values())
total = sum(q * PRICES[n] for n, q in self.qty.items())
return f"{items} items, {total} tk"
# ---- Chain of Responsibility: each approver handles what it can, or passes it on.
@dataclass(frozen=True)
class Limit:
who: str
max: int
next: Optional[Limit] = None
def approve(self, amount: int) -> str:
if amount <= self.max:
return self.who
if self.next is None:
return "rejected, needs finance review"
return self.next.approve(amount)
for name, s in (("bike", PerKm(40, 10)), ("car", PerKm(100, 30)), ("cng", PerKm(40, 20))):
print(f"fare {name:<5} 8 km: {s.fare(8)} tk")
cart = Cart()
for cmd in (AddItem("tea", 2), AddItem("biscuit", 1)):
cart.run(cmd)
print(f"cart: {cmd.name} -> {cart.summary()}")
for _ in range(3):
print("cart: undo ->", cart.summary() if cart.undo_last() else "nothing to undo")
chain = Limit("auto-approved", 500,
Limit("approved by supervisor", 5000,
Limit("approved by manager", 50000)))
for amount in (300, 3000, 12000, 80000):
print(f"refund {amount} tk: {chain.approve(amount)}")// ---- Strategy: the fare rule is a swappable object; the ride doesn't care which.
interface FareStrategy {
fare(km: number): number;
}
class PerKm implements FareStrategy {
readonly base: number;
readonly perKm: number;
constructor(base: number, perKm: number) {
this.base = base;
this.perKm = perKm;
}
fare(km: number): number {
return this.base + this.perKm * km;
}
}
// ---- Command: each change to the cart is an object that knows how to undo itself.
const PRICES: Record<string, number> = { tea: 30, biscuit: 20 };
interface Command {
readonly name: string;
do(cart: Cart): void;
undo(cart: Cart): void;
}
class AddItem implements Command {
readonly item: string;
readonly qty: number;
constructor(item: string, qty: number) {
this.item = item;
this.qty = qty;
}
get name(): string {
return `add ${this.item} x${this.qty}`;
}
do(cart: Cart): void {
cart.qty.set(this.item, (cart.qty.get(this.item) ?? 0) + this.qty);
}
undo(cart: Cart): void {
cart.qty.set(this.item, cart.qty.get(this.item)! - this.qty);
}
}
class Cart {
readonly qty = new Map<string, number>();
readonly #history: Command[] = [];
run(cmd: Command): void {
cmd.do(this);
this.#history.push(cmd);
}
undoLast(): boolean {
const last = this.#history.pop();
if (!last) return false;
last.undo(this);
return true;
}
summary(): string {
let items = 0;
let total = 0;
for (const [name, q] of this.qty) {
items += q;
total += q * PRICES[name]!;
}
return `${items} items, ${total} tk`;
}
}
// ---- Chain of Responsibility: each approver handles what it can, or passes it on.
class Limit {
readonly who: string;
readonly max: number;
readonly next: Limit | null;
constructor(who: string, max: number, next: Limit | null = null) {
this.who = who;
this.max = max;
this.next = next;
}
approve(amount: number): string {
if (amount <= this.max) return this.who;
return this.next ? this.next.approve(amount) : "rejected, needs finance review";
}
}
const strategies: [string, FareStrategy][] = [
["bike", new PerKm(40, 10)],
["car", new PerKm(100, 30)],
["cng", new PerKm(40, 20)],
];
for (const [name, s] of strategies) console.log(`fare ${name.padEnd(5)} 8 km: ${s.fare(8)} tk`);
const cart = new Cart();
for (const cmd of [new AddItem("tea", 2), new AddItem("biscuit", 1)]) {
cart.run(cmd);
console.log(`cart: ${cmd.name} -> ${cart.summary()}`);
}
for (let i = 0; i < 3; i++) console.log("cart: undo ->", cart.undoLast() ? cart.summary() : "nothing to undo");
const chain = new Limit("auto-approved", 500,
new Limit("approved by supervisor", 5000,
new Limit("approved by manager", 50000)));
for (const amount of [300, 3000, 12000, 80000]) console.log(`refund ${amount} tk: ${chain.approve(amount)}`);use std::collections::BTreeMap;
// ---- Strategy: the fare rule is a swappable object; the ride doesn't care which.
trait FareStrategy { fn fare(&self, km: u32) -> u32; }
struct PerKm { base: u32, per_km: u32 }
impl FareStrategy for PerKm { fn fare(&self, km: u32) -> u32 { self.base + self.per_km * km } }
// ---- Command: each change to the cart is an object that knows how to undo itself.
fn price(item: &str) -> u32 { match item { "tea" => 30, "biscuit" => 20, _ => 0 } }
#[derive(Default)]
struct Cart { qty: BTreeMap<String, u32>, history: Vec<Box<dyn Command>> }
trait Command {
fn apply(&self, cart: &mut Cart);
fn undo(&self, cart: &mut Cart);
fn name(&self) -> String;
}
struct AddItem { item: String, qty: u32 }
impl Command for AddItem {
fn apply(&self, c: &mut Cart) { *c.qty.entry(self.item.clone()).or_insert(0) += self.qty; }
fn undo(&self, c: &mut Cart) { *c.qty.get_mut(&self.item).unwrap() -= self.qty; }
fn name(&self) -> String { format!("add {} x{}", self.item, self.qty) }
}
impl Cart {
fn run(&mut self, cmd: Box<dyn Command>) { cmd.apply(self); self.history.push(cmd); }
fn undo_last(&mut self) -> bool {
match self.history.pop() {
Some(cmd) => { cmd.undo(self); true }
None => false,
}
}
fn summary(&self) -> String {
let items: u32 = self.qty.values().sum();
let total: u32 = self.qty.iter().map(|(n, q)| q * price(n)).sum();
format!("{items} items, {total} tk")
}
}
// ---- Chain of Responsibility: each approver handles what it can, or passes it on.
struct Limit { who: &'static str, max: u32, next: Option<Box<Limit>> }
impl Limit {
fn approve(&self, amount: u32) -> &'static str {
if amount <= self.max { return self.who; }
match &self.next {
Some(n) => n.approve(amount),
None => "rejected, needs finance review",
}
}
}
fn main() {
let strategies: [(&str, Box<dyn FareStrategy>); 3] = [
("bike", Box::new(PerKm { base: 40, per_km: 10 })),
("car", Box::new(PerKm { base: 100, per_km: 30 })),
("cng", Box::new(PerKm { base: 40, per_km: 20 })),
];
for (name, s) in &strategies { println!("fare {name:<5} 8 km: {} tk", s.fare(8)); }
let mut cart = Cart::default();
for (item, qty) in [("tea", 2), ("biscuit", 1)] {
let cmd = Box::new(AddItem { item: item.into(), qty });
let name = cmd.name();
cart.run(cmd);
println!("cart: {name} -> {}", cart.summary());
}
for _ in 0..3 {
if cart.undo_last() { println!("cart: undo -> {}", cart.summary()) } else { println!("cart: undo -> nothing to undo") }
}
let chain = Limit { who: "auto-approved", max: 500, next: Some(Box::new(
Limit { who: "approved by supervisor", max: 5000, next: Some(Box::new(
Limit { who: "approved by manager", max: 50000, next: None })) })) };
for amount in [300, 3000, 12000, 80000] { println!("refund {amount} tk: {}", chain.approve(amount)); }
}import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class Main {
// ---- Strategy: the fare rule is a swappable object; the ride doesn't care which.
interface FareStrategy { int fare(int km); }
record PerKm(int base, int perKm) implements FareStrategy {
public int fare(int km) { return base + perKm * km; }
}
// ---- Command: each change to the cart is an object that knows how to undo itself.
static final Map<String, Integer> PRICES = Map.of("tea", 30, "biscuit", 20);
interface Command {
void apply(Cart c);
void undo(Cart c);
String name();
}
record AddItem(String item, int qty) implements Command {
public void apply(Cart c) { c.qty.merge(item, qty, Integer::sum); }
public void undo(Cart c) { c.qty.merge(item, -qty, Integer::sum); }
public String name() { return "add " + item + " x" + qty; }
}
static final class Cart {
final Map<String, Integer> qty = new LinkedHashMap<>();
private final Deque<Command> history = new ArrayDeque<>();
void run(Command cmd) { cmd.apply(this); history.push(cmd); }
boolean undoLast() {
if (history.isEmpty()) return false;
history.pop().undo(this);
return true;
}
String summary() {
int items = 0, total = 0;
for (var e : qty.entrySet()) { items += e.getValue(); total += e.getValue() * PRICES.get(e.getKey()); }
return items + " items, " + total + " tk";
}
}
// ---- Chain of Responsibility: each approver handles what it can, or passes it on.
record Limit(String who, int max, Limit next) {
String approve(int amount) {
if (amount <= max) return who;
return next == null ? "rejected, needs finance review" : next.approve(amount);
}
}
public static void main(String[] args) {
Map<String, FareStrategy> strategies = new LinkedHashMap<>();
strategies.put("bike", new PerKm(40, 10));
strategies.put("car", new PerKm(100, 30));
strategies.put("cng", new PerKm(40, 20));
strategies.forEach((name, s) -> System.out.printf("fare %-5s 8 km: %d tk%n", name, s.fare(8)));
Cart cart = new Cart();
for (Command cmd : List.of(new AddItem("tea", 2), new AddItem("biscuit", 1))) {
cart.run(cmd);
System.out.printf("cart: %s -> %s%n", cmd.name(), cart.summary());
}
for (int i = 0; i < 3; i++)
System.out.println("cart: undo -> " + (cart.undoLast() ? cart.summary() : "nothing to undo"));
Limit chain = new Limit("auto-approved", 500,
new Limit("approved by supervisor", 5000,
new Limit("approved by manager", 50000, null)));
for (int amount : List.of(300, 3000, 12000, 80000))
System.out.printf("refund %d tk: %s%n", amount, chain.approve(amount));
}
}#include <cstdio>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
// ---- Strategy: the fare rule is a swappable object; the ride doesn't care which.
class FareStrategy {
public:
virtual ~FareStrategy() = default;
virtual int fare(int km) const = 0;
};
class PerKm : public FareStrategy {
public:
PerKm(int base, int perKm) : base_(base), perKm_(perKm) {}
int fare(int km) const override { return base_ + perKm_ * km; }
private:
int base_, perKm_;
};
// ---- Command: each change to the cart is an object that knows how to undo itself.
const std::map<std::string, int> kPrices{{"tea", 30}, {"biscuit", 20}};
struct Cart;
class Command {
public:
virtual ~Command() = default;
virtual void apply(Cart& c) const = 0;
virtual void undo(Cart& c) const = 0;
virtual std::string name() const = 0;
};
struct Cart {
std::map<std::string, int> qty;
std::vector<std::unique_ptr<Command>> history;
void run(std::unique_ptr<Command> cmd) {
cmd->apply(*this);
history.push_back(std::move(cmd));
}
bool undoLast() {
if (history.empty()) return false;
history.back()->undo(*this);
history.pop_back();
return true;
}
std::string summary() const {
int items = 0, total = 0;
for (const auto& [n, q] : qty) {
items += q;
total += q * kPrices.at(n);
}
return std::to_string(items) + " items, " + std::to_string(total) + " tk";
}
};
class AddItem : public Command {
public:
AddItem(std::string item, int qty) : item_(std::move(item)), qty_(qty) {}
void apply(Cart& c) const override { c.qty[item_] += qty_; }
void undo(Cart& c) const override { c.qty[item_] -= qty_; }
std::string name() const override { return "add " + item_ + " x" + std::to_string(qty_); }
private:
std::string item_;
int qty_;
};
// ---- Chain of Responsibility: each approver handles what it can, or passes it on.
struct Limit {
std::string who;
int max;
std::unique_ptr<Limit> next;
std::string approve(int amount) const {
if (amount <= max) return who;
return next ? next->approve(amount) : "rejected, needs finance review";
}
};
int main() {
std::vector<std::pair<std::string, std::unique_ptr<FareStrategy>>> strategies;
strategies.emplace_back("bike", std::make_unique<PerKm>(40, 10));
strategies.emplace_back("car", std::make_unique<PerKm>(100, 30));
strategies.emplace_back("cng", std::make_unique<PerKm>(40, 20));
for (const auto& [name, s] : strategies) std::printf("fare %-5s 8 km: %d tk\n", name.c_str(), s->fare(8));
Cart cart;
for (auto [item, q] : std::vector<std::pair<std::string, int>>{{"tea", 2}, {"biscuit", 1}}) {
auto cmd = std::make_unique<AddItem>(item, q);
std::string name = cmd->name();
cart.run(std::move(cmd));
std::printf("cart: %s -> %s\n", name.c_str(), cart.summary().c_str());
}
for (int i = 0; i < 3; i++)
std::printf("cart: undo -> %s\n", cart.undoLast() ? cart.summary().c_str() : "nothing to undo");
Limit chain{"auto-approved", 500,
std::make_unique<Limit>(Limit{"approved by supervisor", 5000,
std::make_unique<Limit>(Limit{"approved by manager", 50000, nullptr})})};
for (int amount : {300, 3000, 12000, 80000}) std::printf("refund %d tk: %s\n", amount, chain.approve(amount).c_str());
}All six print exactly this:
fare bike 8 km: 120 tk
fare car 8 km: 340 tk
fare cng 8 km: 200 tk
cart: add tea x2 -> 2 items, 60 tk
cart: add biscuit x1 -> 3 items, 80 tk
cart: undo -> 2 items, 60 tk
cart: undo -> 0 items, 0 tk
cart: undo -> nothing to undo
refund 300 tk: auto-approved
refund 3000 tk: approved by supervisor
refund 12000 tk: approved by manager
refund 80000 tk: rejected, needs finance review
Reading the output:
- Fares: bike 40 + 10×8 = 120, car 100 + 30×8 = 340, CNG 40 + 20×8 = 200. The loop just calls
Fare(8)on whichever strategy it holds. - Cart: two commands run, then two undos reverse them in last-in-first-out order. The third undo finds an empty history and says so.
- Refunds: 300 stops at auto, 3,000 at the supervisor, 12,000 at the manager. 80,000 falls off the end of the chain and is rejected for finance review.
- What each language changed: Go and Python use tiny value types, and Python's
ProtocolmeansAddItemnever declares that it's aCommand. Rust'sCommandmethod is calledapply, notdo, becausedois a reserved word; the chain isOption<Box<Limit>>, so the end of the chain isNonerather than a null pointer. Java's commands are records and the history is anArrayDequeused as a stack. C++ owns each command in aunique_ptr, so popping the history destroys the command right after itsUndo.
Behaviour patterns in real systems
- Choose the strategy once, at the edge. Map the ride type to a strategy when the request arrives (a factory, from chapter 7), then pass the strategy in. Business code never sees the ride type again.
- Middleware is a chain you already use. Auth, rate limiting, logging and recovery in web frameworks are links in a chain. Keep each link small and its order explicit in one place.
- Commands are great for audit logs and retries. If every action is an object with a name and data, you can log it, store it and replay it (event sourcing builds on this idea).
- Undo needs care with real side effects. Undoing "add to cart" is easy; undoing "send SMS" isn't. Commands with irreversible effects need a compensating action (send a correction), not a true undo.
- Don't wrap a single
ifin a pattern. Two stable branches are fine as anif. Reach for Strategy when variants keep arriving.
Designs that cause problems later
Pick the design that will hurt
func (s Car) Fare() { if isBike { ... } }Pause and think
Marketing wants a night surcharge of 20% on every ride type between 11 p.m. and 6 a.m. Do you add it inside each fare strategy?
Reveal
No. It applies to every strategy, so it shouldn't be copied into each one. Wrap the chosen strategy instead: a NightSurcharge{inner FareStrategy} that calls inner.Fare(km) and adds 20% when the time is in range (with the time passed in, not read from a clock). That's the Decorator pattern from the next chapter: it adds behaviour around a strategy without changing any of them.
Quick quiz
1. What's the difference between Strategy and State?
Answer
Strategy is chosen from outside and usually stays fixed for the job (which fare rule). State changes from inside as the object moves through its lifecycle, and the current state decides how each method behaves.
2. What does a Command object need so that undo works?
Answer
Enough data to perform the action and to reverse it, plus Do and Undo methods. The invoker keeps a history (a stack) of commands that ran.
3. Why is a chain easier to change than one big if/else?
Answer
Each link knows only its own rule and the next link. Adding, removing or reordering steps changes how the chain is built, not the code of the other links.
4. Why do many Go and Rust codebases prefer functions over Template Method?
Answer
Template Method relies on inheritance. Passing the varying steps as functions (or small interfaces) gives the same "fixed outline, variable details" with composition, which those languages favour.
Your challenge: remove, and a regional head
- Add a RemoveItem command whose
Undoputs the item back. After the two adds, runRemoveItem{tea, 1}and print the cart, then do the three undos. - Insert a regional head link (
approved by regional head, up to 200,000) after the manager.
Before you run it, predict the new cart lines and the last refund line.
You've succeeded when the cart prints cart: remove tea x1 -> 2 items, 50 tk, the undos then print 3 items, 80 tk, 2 items, 60 tk and 0 items, 0 tk, the last refund line is refund 80000 tk: approved by regional head, and no existing command or approver was edited.
In one line
When behaviour varies, give the varying part an interface and move each variant into its own small object: Strategy swaps algorithms, State swaps behaviour with the lifecycle, Command turns actions into undoable records, and a Chain passes a request along until someone can handle it. The code that uses them stops growing branches.
Station stamp
Finish the chapter
Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.
Go Pro to earn XP →