Part 01 · Low Level Design
Behavioral Patterns: Strategy, State, Command
একই কাজ, আলাদা নিয়ম: algorithm বদলানো, status অনুযায়ী behaviour চালানো, ধাপ একই রেখে details বদলানো, action queue আর undo করা, আর request টা একটা chain ধরে পাঠানো।
⏱ 2-3 ঘণ্টা · Station 8 / 80
যে fare function পুরো app খেয়ে ফেললো
একটা ride app এর CalculateFare শুরু হয়েছিল তিন লাইনে। তারপর এলো bike, তারপর CNG, তারপর car, তারপর রাতের surcharge, তারপর পূজার discount, তারপর "Gulshan এ premium car"। আজ এটা ১৪টা if/else branch ওয়ালা একটা function, আর car এর ভাড়ায় যেকোনো change এ bike এর ভাড়া ভাঙার একটা সম্ভাবনা থাকে।
একই team তাদের food section এর cart এ "undo" চায়, আর এমন একটা refund flow চায় যেখানে ছোট refund নিজে থেকেই হয়ে যাবে, মাঝারি গুলো supervisor দেখবে, বড় গুলো manager। দুইটার প্রথম draft ই আবার, বাড়তে থাকা একটা if/else।
Behavioral pattern হলো আচরণ যখন বদলায়, তখন কে কী করবে সেটা ঠিক করার চেনা কিছু উপায়। প্রতিটা pattern branch এর একটা স্তূপকে একটা কাজের ছোট ছোট object এ ভাগ করে দেয়।
সমস্যা: বদলাতে থাকা আচরণ, branch দিয়ে লেখা
| যা দেখা যায় | Pattern |
|---|---|
if rideType == ... else if ... যেটা প্রতিটা নতুন product এ বাড়ে | Strategy |
অনেক method এ বারবার if status == ... else if ... | State |
| কয়েকটা class একই ধাপ গুলো সামান্য পার্থক্য সহ বারবার করে | Template Method |
| Undo, redo, queue বা action এর log লাগবে | Command |
| কয়েকটা handler এর মধ্যে যে প্রথম পারে, request টা সে-ই সামলাবে | Chain of Responsibility |
একটা ride app, একটা খাতা আর একটা office এর কথা ভাবো
Ride app এ bike, CNG না car বাছলে ভাড়া হিসাবের নিয়ম বদলায়; trip নিজে এ নিয়ে মাথা ঘামায় না। এটা Strategy: algorithm বদলাও, caller একই থাকে।
পেন্সিল দিয়ে লেখা আর কী কী লিখলে তার একটা list রাখা, যাতে শেষেরটা মুছে ফেলা যায়: এটা Command। প্রতিটা কাজ এমন একটা জিনিস হিসেবে লিখে রাখা হয় যেটা ফিরিয়ে নেওয়া যায়।
Office এ একটা refund এর আবেদন যায় clerk এর কাছে; খুব বড় হলে supervisor এর কাছে; তারপর manager। প্রত্যেকে নিজের ক্ষমতার মধ্যে যা পড়ে সামলায়, বাকিটা উপরে পাঠায়। এটা Chain of Responsibility।
তবে একটা জায়গায় মেলে না। Office এ supervisor চাইলে নিয়ম একটু বাঁকাতে পারেন। Code এ প্রতিটা link ঠিক যা লেখা তাই করে, তাই chain এর ক্রম আর প্রতিটা link এর limit ই হলো নীতি; এগুলো বদলানো business এর সিদ্ধান্ত, refactor না।
Refund এর chain হলো auto (≤ ৫০০) → supervisor (≤ ৫,০০০) → manager (≤ ৫০,০০০)। Finance একটা নতুন ধাপ যোগ করলো: একজন 'regional head' যিনি ২,০০,০০০ পর্যন্ত approve করতে পারেন, manager আর শেষের মাঝখানে। কী বদলাবে?
বদলাতে থাকা আচরণের জন্য পাঁচটা pattern
- Strategy: একটা algorithm এর প্রতিটা রকম একই interface এর পেছনে রাখো (
FareStrategy.Fare(km)), আর caller কে তার দরকারি টা দাও। নতুন রকম = নতুন type। Caller এ কোনো branch নেই। Pricing, sorting, routing এর নিয়মের জন্য একদম ঠিক। - State: একটা object এর আচরণ যখন তার state এর উপর নির্ভর করে, আর অনেক method সেটা দেখে branch করতো, তখন প্রতিটা state কে একই interface সহ একটা class বানাও (
Placed.Cancel(),Shipped.Cancel())। Object টা নিজের এখনকার state object এর হাতে কাজটা তুলে দেয়। এটা chapter 4 এর transition table এর object-oriented রূপ; state গুলোর সত্যিই আলাদা আচরণ না হওয়া পর্যন্ত table ই ব্যবহার করো। - Template Method: একটা base একটা process এর নির্দিষ্ট ধাপ গুলো ঠিক করে দেয় (
validate → charge → notify), আর রকমভেদ গুলো নির্দিষ্ট কিছু ধাপ ভরে। কাজের, কিন্তু inheritance এর উপর ভর করে; Go, Rust আর আধুনিক TypeScript এ একই idea সাধারণত এমন একটা function যেটা বদলানো ধাপ গুলো parameter হিসেবে নেয় (composition)। - Command: একটা কাজকে
DoআরUndoসহ (হয়তো একটা নাম ও) একটা object বানাও। তাহলে undo র জন্য history রাখা যায়, কাজ পরে করার জন্য queue করা যায়, আবার চেষ্টা করা যায়, বা ঠিক কী হয়েছিল তার log রাখা যায়। - Chain of Responsibility: handler গুলোকে এমনভাবে জোড়া লাগাও যাতে প্রত্যেকে হয় request টা সামলায়, নয়তো পরের জনকে দেয়। ধাপ যোগ, বাদ বা আগে-পিছে করলে বাকিদের গায়ে হাত পড়ে না। আসল code এ সবচেয়ে চেনা chain হলো HTTP middleware (auth → rate limit → logging → handler)।
- মূল idea একটাই: যে অংশটা বদলায় সেটা খুঁজে বের করো, তাকে একটা interface দাও, আর প্রতিটা রকম নিজের ছোট object এ সরাও। যে code আচরণটা ব্যবহার করে, সেটা আর বদলায় না।
Undo, ধাপে ধাপে
দুইটা add আর তিনটা undo
ধাপ 1/5 Method না, একটা command চালাও
Caller AddItem{tea, 2} বানিয়ে cart.Run এর হাতে দেয়। Cart সেটা চালায় আর history তে push করে।
- cart
- 2 items, 60 tk
- history
- [add tea x2]
যখন branch জমতে থাকে
একই if/else বাড়তে দেখলে জিজ্ঞেস করো:
- Algorithm কি বদলায়? → Strategy।
- আচরণ কি object এর state এর উপর নির্ভর করে, অনেক method এ? → State (বা একটা transition table)।
- কাজ গুলো কি রাখতে, undo করতে বা queue করতে হবে? → Command।
- যে প্রথম পারে সে কি নেবে? → Chain of Responsibility।
- ধাপ একই, খুঁটিনাটি আলাদা? → Template Method, বা এমন একটা function যেটা খুঁটিনাটি গুলো parameter হিসেবে নেয়।
নিজে বলো: এটা কোন pattern?
Pattern এর নাম বলো
sorter := sorters[userChoice]; sorter.Sort(list)
Code এ ভাড়া, undo আর refund
ভাড়ার জন্য Strategy, undo করা যায় এমন cart এর জন্য Command, আর refund approval এর জন্য Chain of Responsibility।
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());
}ছয়টাই হুবহু এটা print করে:
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
Output টা পড়ি:
- ভাড়া: bike ৪০ + ১০×৮ = ১২০, car ১০০ + ৩০×৮ = ৩৪০, CNG ৪০ + ২০×৮ = ২০০। Loop শুধু হাতের strategy টার
Fare(8)ডাকে। - Cart: দুইটা command চলে, তারপর দুইটা undo সেগুলো উল্টো ক্রমে ফিরিয়ে নেয় (শেষে যেটা ঢুকেছে, আগে সেটা বের হয়)। তৃতীয় undo খালি history পায় আর সেটাই বলে।
- Refund: ৩০০ থামে auto তে, ৩,০০০ supervisor এ, ১২,০০০ manager এ। ৮০,০০০ chain এর শেষ পার হয়ে যায় আর finance review তে পাঠানো হয়।
- Language ভেদে কী আলাদা: Go আর Python ছোট ছোট value type ব্যবহার করে, আর Python এর
Protocolএর কারণেAddItemকোথাও ঘোষণা করে না যে সে একটাCommand। Rust এCommandএর method এর নামapply,doনা, কারণdoএকটা reserved word; chain হলোOption<Box<Limit>>, তাই chain এর শেষ মানেNone, null pointer না। Java তে command গুলো record, আর history একটাArrayDequeযেটা stack হিসেবে চলে। C++ প্রতিটা command এর মালিকunique_ptrএ, তাই history থেকে pop করলেUndoএর ঠিক পরেই command টা ধ্বংস হয়।
Real system এ behavioral pattern
- Strategy একবারই বাছো, একেবারে শুরুতে। Request আসার সময় ride type থেকে strategy বানাও (chapter 7 এর একটা factory দিয়ে), তারপর strategy টা বাইরে থেকে দাও। Business code আর কখনো ride type দেখে না।
- Middleware এমন একটা chain যেটা তুমি এমনিতেই ব্যবহার করো। Web framework এর auth, rate limiting, logging আর recovery হলো chain এর এক একটা link। প্রতিটা link ছোট রাখো, আর তাদের ক্রম এক জায়গায় পরিষ্কার লিখে রাখো।
- Audit log আর retry র জন্য command দারুণ। প্রতিটা কাজ যদি নাম আর data সহ একটা object হয়, তাহলে log করা, রাখা আর আবার চালানো যায় (event sourcing এই idea র উপরেই দাঁড়িয়ে)।
- আসল side effect থাকলে undo তে সাবধান। "Cart এ যোগ" undo করা সহজ; "SMS পাঠানো" undo করা যায় না। যে command এর ফল ফেরানো যায় না, তার জন্য সত্যিকারের undo না, একটা compensating কাজ লাগে (একটা সংশোধন পাঠাও)।
- একটা মাত্র
ifকে pattern এ মুড়িও না। দুইটা স্থির branchifহিসেবেই ঠিক আছে। নতুন নতুন রকম আসতে থাকলে তখন Strategy ধরো।
যে design গুলো পরে problem create করে
যে design টা পরে ভোগাবে
func (s Car) Fare() { if isBike { ... } }একটু থেমে ভাবো
Marketing রাত ১১টা থেকে সকাল ৬টা পর্যন্ত সব ride type এ 20% night surcharge চায়। তুমি কি এটা প্রতিটা fare strategy র ভেতরে যোগ করবে?
উত্তর দেখো
না। এটা সব strategy তে খাটে, তাই প্রতিটার ভেতরে copy করা উচিত না। তার বদলে বাছাই করা strategy টাকে মুড়ে দাও: একটা NightSurcharge{inner FareStrategy}, যেটা inner.Fare(km) ডাকে, আর সময় range এর মধ্যে থাকলে 20% যোগ করে (সময় বাইরে থেকে দেওয়া, ঘড়ি থেকে পড়া না)। এটাই পরের chapter এর Decorator pattern: কোনো strategy না বদলে তাদের চারপাশে আচরণ যোগ করে।
ছোট্ট quiz
১. Strategy আর State এর পার্থক্য কী?
উত্তর
Strategy বাইরে থেকে বাছা হয় আর কাজের সময় সাধারণত একই থাকে (কোন ভাড়ার নিয়ম)। State ভেতর থেকে বদলায়, object যত তার জীবনচক্রে এগোয়, আর এখনকার state ঠিক করে প্রতিটা method কীভাবে চলবে।
২. Undo চালাতে একটা Command object এর কী কী লাগে?
উত্তর
কাজটা করার আর উল্টানোর মতো যথেষ্ট data, সাথে Do আর Undo method। যে চালায়, সে চলা command গুলোর একটা history (stack) রাখে।
৩. বড় একটা if/else এর চেয়ে chain বদলানো সহজ কেন?
উত্তর
প্রতিটা link শুধু নিজের rule আর পরের link টা জানে। ধাপ যোগ, বাদ বা আগে-পিছে করলে বদলায় chain কীভাবে বানানো হয় শুধু সেটা, বাকি link গুলোর code না।
৪. অনেক Go আর Rust codebase Template Method এর বদলে function কেন পছন্দ করে?
উত্তর
Template Method inheritance এর উপর ভর করে। বদলানো ধাপ গুলো function (বা ছোট interface) হিসেবে দিলে composition দিয়েই একই "স্থির কাঠামো, বদলানো খুঁটিনাটি" পাওয়া যায়, যেটা এই language গুলো পছন্দ করে।
তোমার challenge: remove, আর একজন regional head
- একটা RemoveItem command যোগ করো, যার
Undoজিনিসটা ফেরত দেয়। দুইটা add এর পরেRemoveItem{tea, 1}চালাও আর cart print করো, তারপর তিনটা undo। - Manager এর পরে একটা regional head link বসাও (
approved by regional head, ২,০০,০০০ পর্যন্ত)।
চালানোর আগে আন্দাজ করো cart এর নতুন লাইন গুলো আর শেষ refund এর লাইন কী হবে।
হয়েছে বুঝবে যখন cart print করবে cart: remove tea x1 -> 2 items, 50 tk, তারপর undo গুলো দেখাবে 3 items, 80 tk, 2 items, 60 tk আর 0 items, 0 tk, শেষ refund এর লাইন হবে refund 80000 tk: approved by regional head, আর আগের কোনো command বা approver edit করতে হবে না।
এক কথায়
আচরণ বদলালে বদলানো অংশটাকে একটা interface দাও, আর প্রতিটা রকম নিজের ছোট object এ সরাও: Strategy algorithm বদলায়, State জীবনচক্রের সাথে আচরণ বদলায়, Command কাজকে undo করা যায় এমন record বানায়, আর Chain একটা request ততক্ষণ সামনে পাঠায় যতক্ষণ না কেউ সেটা সামলাতে পারে। যে code এগুলো ব্যবহার করে, তাতে আর branch জমে না।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →