Part 01 · Low Level Design

Behavioral Patterns: Strategy, State, Command

StrategyStateTemplate MethodCommandChain of Responsibility

একই কাজ, আলাদা নিয়ম: 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।

autoup to 500supervisorup to 5,000managerup to 50,000end of chainfinance review3003,00012,00080,000each refund stops at the first approver whose limit covers it
Play চাপো। চারটা refund একই chain ধরে যায়। ৩০০ থামে auto-approval এ, ৩,০০০ supervisor এর কাছে, ১২,০০০ manager এর কাছে, আর ৮০,০০০ chain এর শেষ পার হয়ে finance review তে চলে যায়।

তবে একটা জায়গায় মেলে না। Office এ supervisor চাইলে নিয়ম একটু বাঁকাতে পারেন। Code এ প্রতিটা link ঠিক যা লেখা তাই করে, তাই chain এর ক্রম আর প্রতিটা link এর limit ই হলো নীতি; এগুলো বদলানো business এর সিদ্ধান্ত, refactor না।

আগে অনুমান করো

Refund এর chain হলো auto (≤ ৫০০) → supervisor (≤ ৫,০০০) → manager (≤ ৫০,০০০)। Finance একটা নতুন ধাপ যোগ করলো: একজন 'regional head' যিনি ২,০০,০০০ পর্যন্ত approve করতে পারেন, manager আর শেষের মাঝখানে। কী বদলাবে?

বদলাতে থাকা আচরণের জন্য পাঁচটা pattern

  1. Strategy: একটা algorithm এর প্রতিটা রকম একই interface এর পেছনে রাখো (FareStrategy.Fare(km)), আর caller কে তার দরকারি টা দাও। নতুন রকম = নতুন type। Caller এ কোনো branch নেই। Pricing, sorting, routing এর নিয়মের জন্য একদম ঠিক।
  2. State: একটা object এর আচরণ যখন তার state এর উপর নির্ভর করে, আর অনেক method সেটা দেখে branch করতো, তখন প্রতিটা state কে একই interface সহ একটা class বানাও (Placed.Cancel(), Shipped.Cancel())। Object টা নিজের এখনকার state object এর হাতে কাজটা তুলে দেয়। এটা chapter 4 এর transition table এর object-oriented রূপ; state গুলোর সত্যিই আলাদা আচরণ না হওয়া পর্যন্ত table ই ব্যবহার করো।
  3. Template Method: একটা base একটা process এর নির্দিষ্ট ধাপ গুলো ঠিক করে দেয় (validate → charge → notify), আর রকমভেদ গুলো নির্দিষ্ট কিছু ধাপ ভরে। কাজের, কিন্তু inheritance এর উপর ভর করে; Go, Rust আর আধুনিক TypeScript এ একই idea সাধারণত এমন একটা function যেটা বদলানো ধাপ গুলো parameter হিসেবে নেয় (composition)।
  4. Command: একটা কাজকে Do আর Undo সহ (হয়তো একটা নাম ও) একটা object বানাও। তাহলে undo র জন্য history রাখা যায়, কাজ পরে করার জন্য queue করা যায়, আবার চেষ্টা করা যায়, বা ঠিক কী হয়েছিল তার log রাখা যায়।
  5. Chain of Responsibility: handler গুলোকে এমনভাবে জোড়া লাগাও যাতে প্রত্যেকে হয় request টা সামলায়, নয়তো পরের জনকে দেয়। ধাপ যোগ, বাদ বা আগে-পিছে করলে বাকিদের গায়ে হাত পড়ে না। আসল code এ সবচেয়ে চেনা chain হলো HTTP middleware (auth → rate limit → logging → handler)।
  6. মূল idea একটাই: যে অংশটা বদলায় সেটা খুঁজে বের করো, তাকে একটা interface দাও, আর প্রতিটা রকম নিজের ছোট object এ সরাও। যে code আচরণটা ব্যবহার করে, সেটা আর বদলায় না।

Undo, ধাপে ধাপে

ধাপে ধাপে দেখো

দুইটা add আর তিনটা undo

CallerCartHistory stackAddItem

ধাপ 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?

প্রতিটা case চালিয়ে দেখো

Pattern এর নাম বলো

sorter := sorters[userChoice]; sorter.Sort(list)
কাজ করে: Strategy। কাজ একই, algorithm বদলানো যায়।

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))
	}
}

ছয়টাই হুবহু এটা 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 এ মুড়িও না। দুইটা স্থির branch if হিসেবেই ঠিক আছে। নতুন নতুন রকম আসতে থাকলে তখন Strategy ধরো।

যে design গুলো পরে problem create করে

প্রতিটা case চালিয়ে দেখো

যে design টা পরে ভোগাবে

func (s Car) Fare() { if isBike { ... } }
ভাঙে: Branch টা শুধু strategy র ভেতরে সরে গেছে। প্রতিটা strategy শুধু নিজের rule জানবে।

একটু থেমে ভাবো

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 পাও →