Part 01 · Low Level Design
Enums and State Machines
Order, ticket, elevator সবাই এক state থেকে আরেক state এ যায়। Enum আর পরিষ্কার transition দিয়ে model করো, যাতে অসম্ভব একটা state code এ লেখাই না যায়।
⏱ 1-2 ঘণ্টা · Station 4 / 80
Refund হলো, তারপরেও delivered
একটা online shop প্রতিটা order এর status রাখে string হিসেবে: "placed", "paid", "shipped"। আলাদা আলাদা screen সরাসরি এটা বসায়: payment callback বসায় "paid", courier এর app বসায় "shipped", support tool বসায় "cancelled"।
একদিন support একটা order cancel করে customer কে টাকা ফেরত দিলো। দশ মিনিট পর courier এর app, যেখানে order টা তখনো খোলা ছিল, সেটাকে "delivered" করে দিলো। এখন order টা delivered আবার refunded ও। আরেকবার একটা typo তে status হয়ে গেল "shiped", আর "shipped" দিয়ে filter করা সব list থেকে order টা হাওয়া।
কোনো ভুলই আটকানো যায়নি, কারণ code কোথাও লিখে রাখেনি কোন কোন status আছে, আর কোনটা থেকে কোনটায় যাওয়া যাবে। এটা লিখে রাখার নামই state machine।
সমস্যা: সব জায়গা থেকে status বদলানো
প্রায় প্রতিটা business object এর একটা জীবনচক্র আছে: order, ticket, ride, loan এর আবেদন, job post। Status যখন একটা খোলা string, আর অনেক জায়গা থেকে বদলানো হয়:
| যা ভুল হয় | কেন হয় |
|---|---|
| Typo থেকে এমন state তৈরি হয় যেটা আসলে নেই | যেকোনো string ই চলে যায় |
| Order cancelled থেকে আবার shipped হয়ে যায় | কোন move legal, কোনো rule সেটা বলে না |
একই check (if status == "paid" or status == "placed") অনেক screen এ copy | Rule গুলো এক জায়গায় নেই |
| মোট কয়টা state আছে, কেউ বলতে পারে না | এরা পুরো code এ ছড়ানো |
Stadium এর একটা ticket এর কথা ভাবো
একটা ticket শুরুতে unused। Gate এ scan হলে সেটা used। Match বাতিল হলে সেটা refunded। Used ticket এর refund হয় না, আর refunded ticket দিয়ে ঢোকা যায় না। Gate এর guard কে পুরো ইতিহাস জানতে হয় না; সে শুধু দেখে: ticket টা এখন যেখানে আছে, সেখান থেকে এই move টা কি চলে?
তবে একটা জায়গায় মেলে না। Stadium এর guard চাইলে ব্যতিক্রম করতে পারে। Code পারে না, আর পারা উচিতও না: business এর যদি ব্যতিক্রম লাগে ("shipping এর পরেও cancel, return হিসেবে"), সেটা table এ নতুন একটা নাম দেওয়া transition হবে, কোনো একটা screen এ লুকানো special case না।
Transition table বলছে: placed → (pay → paid, cancel → cancelled), paid → (ship → shipped, cancel → cancelled), shipped → (deliver → delivered)। একটা order shipped অবস্থায় আছে। cancel আসলে কী হবে?
Enum, state আর transition
- Enum হলো নাম দেওয়া কয়েকটা value এর একটা বন্ধ set:
Placed, Paid, Shipped, Delivered, Cancelled। Compiler (বা type checker) এর বাইরে কিছু মানে না, তাই"shiped"ঢুকতে পারে না। Go তে typed constant, Rust আর Java তে আসল enum, TypeScript এ string literal এর union, C++ এenum class, Python এEnum। - State মানে object টা তার জীবনের এই মুহূর্তে কোথায় আছে। এটাই ঠিক করে এরপর সে কী করতে পারবে।
- Transition মানে এক state থেকে আরেক state এ যাওয়া, যেটা ঘটে একটা event এর কারণে (pay, ship, cancel)। Legal transition গুলোর পুরো list ই হলো state machine।
- Transition গুলো এক জায়গায় লেখো, পারলে data হিসেবে: একটা table বা একটা
match। তাহলে পুরো জীবনচক্র এক screen এ পড়া, review করা আর test করা যায়। - State private রাখো, আর বদলাও শুধু একটা method দিয়ে (
Apply(event)), যেটা table দেখে নেয়। এটা আগের chapter এর encapsulation, এবার জীবনচক্রের উপর। - Final state থেকে বের হওয়ার কোনো transition নেই (delivered, cancelled)। এটা পরিষ্কার করে বলে দিলে "zombie" object হয় না, মানে শেষ হয়ে যাওয়ার পরেও বদলাতে থাকা object।
- State diagram state গুলোকে box আর transition গুলোকে নাম লেখা তীর হিসেবে আঁকে। Product manager কে দেখানোর জন্য এর চেয়ে পরিষ্কার ছবি নেই, আর এটা table এর সাথে লাইন ধরে মিলতে হবে।
একটা cancel, table এর সাথে মিলিয়ে দেখা
shipped অবস্থায় order.Apply(cancel)
ধাপ 1/5 Support screen cancel চায়
Screen নিজে status বসায় না। সে একটা event পাঠায়: cancel।
- event
- cancel
পুরো জীবনচক্র এক পাতায়
জীবনচক্রটাকে এমন একটা table হিসেবে ভাবো যেটা print করে দেয়ালে লাগিয়ে রাখা যায়:
| কোথা থেকে | Event | কোথায় |
|---|---|---|
| placed | pay | paid |
| placed | cancel | cancelled |
| paid | ship | shipped |
| paid | cancel | cancelled |
| shipped | deliver | delivered |
প্রতিটা legal move একটা সারি। Table এ যা নেই, তা illegal। Business rule বদলালে একটা সারি যোগ বা বাদ দাও, আর diagram, code আর test একসাথে বদলায়।
নিজে বলো: এটা কি চলবে?
Legal transition কিনা?
উপরের order table টা ধরে।
placed -> paid
Code এ order এর জীবনচক্র
State আর event হলো enum, transition গুলো একটা table এ (বা একটা match এ), আর state বদলানোর একমাত্র রাস্তা Apply। দুইটা order কে legal আর illegal দুই রকম event দিয়ে চালানো হয়।
package main
import (
"fmt"
"strings"
)
// State and Event are enums: a closed set of names instead of free-form strings.
type State int
const (
Placed State = iota
Paid
Shipped
Delivered
Cancelled
)
func (s State) String() string {
return [...]string{"placed", "paid", "shipped", "delivered", "cancelled"}[s]
}
type Event string
const (
Pay Event = "pay"
Ship Event = "ship"
Deliver Event = "deliver"
Cancel Event = "cancel"
)
// The whole lifecycle in one table: from a state, which events are allowed, and where they lead.
// Anything not listed is an invalid transition.
var transitions = map[State]map[Event]State{
Placed: {Pay: Paid, Cancel: Cancelled},
Paid: {Ship: Shipped, Cancel: Cancelled},
Shipped: {Deliver: Delivered},
// Delivered and Cancelled are final: no way out.
}
type Order struct {
ID int
state State
history []State
}
func NewOrder(id int) *Order {
return &Order{ID: id, state: Placed, history: []State{Placed}}
}
// Apply is the only way to change the state, so an impossible state can't be reached.
func (o *Order) Apply(e Event) error {
next, ok := transitions[o.state][e]
if !ok {
return fmt.Errorf("cannot %s when %s", e, o.state)
}
o.state = next
o.history = append(o.history, next)
return nil
}
func (o *Order) History() string {
parts := []string{}
for _, s := range o.history {
parts = append(parts, s.String())
}
return strings.Join(parts, " > ")
}
func run(o *Order, events ...Event) {
for _, e := range events {
from := o.state
if err := o.Apply(e); err != nil {
fmt.Printf("%-8s %-9s : %v\n", e, from, err)
} else {
fmt.Printf("%-8s %-9s -> %s\n", e, from, o.state)
}
}
}
func main() {
a := NewOrder(1001)
fmt.Printf("order %d %s\n", a.ID, a.state)
run(a, Pay, Ship, Cancel, Deliver, Deliver)
b := NewOrder(1002)
fmt.Printf("order %d %s\n", b.ID, b.state)
run(b, Cancel, Pay)
fmt.Printf("history %d: %s\n", a.ID, a.History())
fmt.Printf("history %d: %s\n", b.ID, b.History())
}from enum import Enum
class State(Enum):
"""A closed set of names instead of free-form strings."""
PLACED = "placed"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Event(Enum):
PAY = "pay"
SHIP = "ship"
DELIVER = "deliver"
CANCEL = "cancel"
# The whole lifecycle in one table. Anything not listed is an invalid transition.
TRANSITIONS: dict[State, dict[Event, State]] = {
State.PLACED: {Event.PAY: State.PAID, Event.CANCEL: State.CANCELLED},
State.PAID: {Event.SHIP: State.SHIPPED, Event.CANCEL: State.CANCELLED},
State.SHIPPED: {Event.DELIVER: State.DELIVERED},
# DELIVERED and CANCELLED are final: no way out.
}
class InvalidTransition(Exception):
pass
class Order:
def __init__(self, id: int):
self.id = id
self._state = State.PLACED
self._history = [State.PLACED]
@property
def state(self) -> State:
return self._state
def apply(self, e: Event) -> None:
"""The only way to change the state, so an impossible state can't be reached."""
nxt = TRANSITIONS.get(self._state, {}).get(e)
if nxt is None:
raise InvalidTransition(f"cannot {e.value} when {self._state.value}")
self._state = nxt
self._history.append(nxt)
def history(self) -> str:
return " > ".join(s.value for s in self._history)
def run(o: Order, *events: Event) -> None:
for e in events:
frm = o.state
try:
o.apply(e)
print(f"{e.value:<8} {frm.value:<9} -> {o.state.value}")
except InvalidTransition as err:
print(f"{e.value:<8} {frm.value:<9} : {err}")
a = Order(1001)
print(f"order {a.id} {a.state.value}")
run(a, Event.PAY, Event.SHIP, Event.CANCEL, Event.DELIVER, Event.DELIVER)
b = Order(1002)
print(f"order {b.id} {b.state.value}")
run(b, Event.CANCEL, Event.PAY)
print(f"history {a.id}: {a.history()}")
print(f"history {b.id}: {b.history()}")// String-literal unions are TypeScript's enums: a closed set of names, checked by the compiler.
// (OrderEvent, because the browser already has a global named Event.)
type State = "placed" | "paid" | "shipped" | "delivered" | "cancelled";
type OrderEvent = "pay" | "ship" | "deliver" | "cancel";
// The whole lifecycle in one table. Anything not listed is an invalid transition.
const TRANSITIONS: Partial<Record<State, Partial<Record<OrderEvent, State>>>> = {
placed: { pay: "paid", cancel: "cancelled" },
paid: { ship: "shipped", cancel: "cancelled" },
shipped: { deliver: "delivered" },
// delivered and cancelled are final: no way out.
};
class Order {
readonly id: number;
#state: State = "placed";
#history: State[] = ["placed"];
constructor(id: number) {
this.id = id;
}
get state(): State {
return this.#state;
}
// The only way to change the state, so an impossible state can't be reached.
apply(e: OrderEvent): void {
const next = TRANSITIONS[this.#state]?.[e];
if (!next) throw new Error(`cannot ${e} when ${this.#state}`);
this.#state = next;
this.#history.push(next);
}
history(): string {
return this.#history.join(" > ");
}
}
function run(o: Order, ...events: OrderEvent[]): void {
for (const e of events) {
const from = o.state;
try {
o.apply(e);
console.log(`${e.padEnd(8)} ${from.padEnd(9)} -> ${o.state}`);
} catch (err) {
console.log(`${e.padEnd(8)} ${from.padEnd(9)} : ${(err as Error).message}`);
}
}
}
const a = new Order(1001);
console.log(`order ${a.id} ${a.state}`);
run(a, "pay", "ship", "cancel", "deliver", "deliver");
const b = new Order(1002);
console.log(`order ${b.id} ${b.state}`);
run(b, "cancel", "pay");
console.log(`history ${a.id}: ${a.history()}`);
console.log(`history ${b.id}: ${b.history()}`);use std::fmt;
// Enums: a closed set of states. `match` below must cover every case, or it won't compile.
#[derive(Clone, Copy, PartialEq, Debug)]
enum State { Placed, Paid, Shipped, Delivered, Cancelled }
#[derive(Clone, Copy, Debug)]
enum Event { Pay, Ship, Deliver, Cancel }
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(match self {
State::Placed => "placed", State::Paid => "paid", State::Shipped => "shipped",
State::Delivered => "delivered", State::Cancelled => "cancelled",
})
}
}
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(match self { Event::Pay => "pay", Event::Ship => "ship", Event::Deliver => "deliver", Event::Cancel => "cancel" })
}
}
// The whole lifecycle in one match. Anything not listed is an invalid transition.
fn transition(s: State, e: Event) -> Option<State> {
use {Event::*, State::*};
match (s, e) {
(Placed, Pay) => Some(Paid),
(Placed, Cancel) | (Paid, Cancel) => Some(Cancelled),
(Paid, Ship) => Some(Shipped),
(Shipped, Deliver) => Some(Delivered),
_ => None, // Delivered and Cancelled are final: no way out.
}
}
struct Order { id: u32, state: State, history: Vec<State> }
impl Order {
fn new(id: u32) -> Order { Order { id, state: State::Placed, history: vec![State::Placed] } }
// The only way to change the state, so an impossible state can't be reached.
fn apply(&mut self, e: Event) -> Result<(), String> {
let next = transition(self.state, e).ok_or(format!("cannot {e} when {}", self.state))?;
self.state = next;
self.history.push(next);
Ok(())
}
fn history(&self) -> String {
self.history.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(" > ")
}
}
fn run(o: &mut Order, events: &[Event]) {
for &e in events {
let from = o.state;
match o.apply(e) {
Ok(()) => println!("{e:<8} {from:<9} -> {}", o.state),
Err(err) => println!("{e:<8} {from:<9} : {err}"),
}
}
}
fn main() {
use Event::*;
let mut a = Order::new(1001);
println!("order {} {}", a.id, a.state);
run(&mut a, &[Pay, Ship, Cancel, Deliver, Deliver]);
let mut b = Order::new(1002);
println!("order {} {}", b.id, b.state);
run(&mut b, &[Cancel, Pay]);
println!("history {}: {}", a.id, a.history());
println!("history {}: {}", b.id, b.history());
}import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
// Enums: a closed set of names instead of free-form strings.
enum State {
PLACED, PAID, SHIPPED, DELIVERED, CANCELLED;
@Override public String toString() { return name().toLowerCase(); }
}
enum Event {
PAY, SHIP, DELIVER, CANCEL;
@Override public String toString() { return name().toLowerCase(); }
}
// The whole lifecycle in one table. Anything not listed is an invalid transition.
static final Map<State, Map<Event, State>> TRANSITIONS = new EnumMap<>(Map.of(
State.PLACED, Map.of(Event.PAY, State.PAID, Event.CANCEL, State.CANCELLED),
State.PAID, Map.of(Event.SHIP, State.SHIPPED, Event.CANCEL, State.CANCELLED),
State.SHIPPED, Map.of(Event.DELIVER, State.DELIVERED)
// DELIVERED and CANCELLED are final: no way out.
));
static final class InvalidTransition extends Exception {
private static final long serialVersionUID = 1L;
InvalidTransition(String msg) { super(msg); }
}
static final class Order {
final int id;
private State state = State.PLACED;
private final List<State> history = new ArrayList<>(List.of(State.PLACED));
Order(int id) { this.id = id; }
State state() { return state; }
// The only way to change the state, so an impossible state can't be reached.
void apply(Event e) throws InvalidTransition {
State next = TRANSITIONS.getOrDefault(state, Map.of()).get(e);
if (next == null) throw new InvalidTransition("cannot " + e + " when " + state);
state = next;
history.add(next);
}
String history() { return history.stream().map(State::toString).collect(Collectors.joining(" > ")); }
}
static void run(Order o, Event... events) {
for (Event e : events) {
State from = o.state();
try {
o.apply(e);
System.out.printf("%-8s %-9s -> %s%n", e, from, o.state());
} catch (InvalidTransition err) {
System.out.printf("%-8s %-9s : %s%n", e, from, err.getMessage());
}
}
}
public static void main(String[] args) {
Order a = new Order(1001);
System.out.printf("order %d %s%n", a.id, a.state());
run(a, Event.PAY, Event.SHIP, Event.CANCEL, Event.DELIVER, Event.DELIVER);
Order b = new Order(1002);
System.out.printf("order %d %s%n", b.id, b.state());
run(b, Event.CANCEL, Event.PAY);
System.out.printf("history %d: %s%n", a.id, a.history());
System.out.printf("history %d: %s%n", b.id, b.history());
}
}#include <cstdio>
#include <map>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
// enum class: a closed set of names, and no silent conversion to int.
enum class State { Placed, Paid, Shipped, Delivered, Cancelled };
enum class Event { Pay, Ship, Deliver, Cancel };
const char* name(State s) {
switch (s) {
case State::Placed: return "placed";
case State::Paid: return "paid";
case State::Shipped: return "shipped";
case State::Delivered: return "delivered";
case State::Cancelled: return "cancelled";
}
return "?";
}
const char* name(Event e) {
switch (e) {
case Event::Pay: return "pay";
case Event::Ship: return "ship";
case Event::Deliver: return "deliver";
case Event::Cancel: return "cancel";
}
return "?";
}
// The whole lifecycle in one table. Anything not listed is an invalid transition.
const std::map<std::pair<State, Event>, State> kTransitions = {
{{State::Placed, Event::Pay}, State::Paid},
{{State::Placed, Event::Cancel}, State::Cancelled},
{{State::Paid, Event::Ship}, State::Shipped},
{{State::Paid, Event::Cancel}, State::Cancelled},
{{State::Shipped, Event::Deliver}, State::Delivered},
// Delivered and Cancelled are final: no way out.
};
class Order {
public:
explicit Order(int id) : id(id) {}
const int id;
State state() const { return state_; }
// The only way to change the state, so an impossible state can't be reached.
void apply(Event e) {
auto it = kTransitions.find({state_, e});
if (it == kTransitions.end())
throw std::runtime_error(std::string("cannot ") + name(e) + " when " + name(state_));
state_ = it->second;
history_.push_back(state_);
}
std::string history() const {
std::string out;
for (State s : history_) out += (out.empty() ? "" : " > ") + std::string(name(s));
return out;
}
private:
State state_ = State::Placed;
std::vector<State> history_{State::Placed};
};
void run(Order& o, std::vector<Event> events) {
for (Event e : events) {
State from = o.state();
try {
o.apply(e);
std::printf("%-8s %-9s -> %s\n", name(e), name(from), name(o.state()));
} catch (const std::exception& err) {
std::printf("%-8s %-9s : %s\n", name(e), name(from), err.what());
}
}
}
int main() {
Order a(1001);
std::printf("order %d %s\n", a.id, name(a.state()));
run(a, {Event::Pay, Event::Ship, Event::Cancel, Event::Deliver, Event::Deliver});
Order b(1002);
std::printf("order %d %s\n", b.id, name(b.state()));
run(b, {Event::Cancel, Event::Pay});
std::printf("history %d: %s\n", a.id, a.history().c_str());
std::printf("history %d: %s\n", b.id, b.history().c_str());
}ছয়টাই হুবহু এটা print করে:
order 1001 placed
pay placed -> paid
ship paid -> shipped
cancel shipped : cannot cancel when shipped
deliver shipped -> delivered
deliver delivered : cannot deliver when delivered
order 1002 placed
cancel placed -> cancelled
pay cancelled : cannot pay when cancelled
history 1001: placed > paid > shipped > delivered
history 1002: placed > cancelled
Output টা পড়ি:
- Order 1001 যায় placed → paid → shipped। Shipped অবস্থায় cancel ফিরিয়ে দেওয়া হয়, state shipped ই থাকে।
- দ্বিতীয় deliver ফিরিয়ে দেওয়া হয়, কারণ delivered final। Courier এর app callback বারবার পাঠালেও কিছু ভাঙে না।
- Order 1002 সাথে সাথেই cancel হয়; দেরিতে আসা pay ফিরিয়ে দেওয়া হয়।
- History তে থাকে শুধু যে transition গুলো সত্যি ঘটেছে, তাই এটা ভরসা করার মতো একটা audit trail।
- Language ভেদে কী আলাদা: Rust table টা লেখে
(state, event)জোড়ার উপর একটাmatchদিয়ে, আর নতুন state যোগ করলে compiler প্রতিটাmatchদেখিয়ে দেয় যেখানে সেটা handle করতে হবে। TypeScript enum এর বদলে string literal এর union ব্যবহার করে, তাই"shiped"একটা type error; type এর নামOrderEvent, কারণ browser এ আগে থেকেইEventনামে একটা global আছে। Java তেEnumMap, যেটা দ্রুত আর ক্রম ঠিক রাখে। Go তে built-in enum নেই, তাই typed constant আর একটাString()method দিয়ে কাজ চলে; কোনো case বাদ পড়লে compiler ধরে না, table টা এক জায়গায় রাখার এটা আরেকটা কারণ।
Real system এ state machine
- State রাখো enum column হিসেবে (বা
CHECKconstraint সহ string), আর history রাখো row হিসেবে (order_id, from, to, event, at)। History ই তোমার audit log, আবার debug করার tool ও। - Transition আর যে data সে রক্ষা করে, দুটো একসাথে atomic করো। "Paid মার্ক করো আর payment টা লিখে রাখো" একটা database transaction হওয়া উচিত, প্রায়ই
UPDATE ... WHERE state = 'placed'দিয়ে, যাতে দুইটা callback দুজনেই জিতে না যায়। - Side effect হবে transition সফল হওয়ার পরে। "আপনার order shipped" SMS পাঠাও Apply কোনো error না দেওয়ার পরেই, আগে কখনো না।
- বাইরে থেকে অচেনা event আসা স্বাভাবিক। Payment gateway আর courier callback বারবার পাঠায়। যে state machine repeat হওয়া event নিরাপদে ফিরিয়ে দেয়, সেটা তোমার system কে প্রায় বিনা খরচে idempotent বানিয়ে দেয়।
- যখন প্রতিটা state এর নিজের আচরণ লাগে (প্রতিটা state অনেক event কে আলাদা ভাবে handle করে), তখন State pattern প্রতিটা state কে একটা class বানায়। এটা আসছে behavioural pattern এর chapter এ; তার আগ পর্যন্ত একটা table ই যথেষ্ট।
যে design গুলো পরে problem create করে
ভাঙা জীবনচক্রটা বাছো
order.status = "shiped"
একটু থেমে ভাবো
Business চায় customer delivered order ৭ দিনের মধ্যে return করতে পারবে। State machine এ কী বদলাবে?
উত্তর দেখো
পুরনো transition বাঁকিয়ে না, নতুন state আর নতুন transition যোগ করো: delivered --return--> return_requested --refund--> returned। ৭ দিনের rule টা হলো return transition এর উপর একটা guard (input হিসেবে লাগবে delivery র তারিখ আর আজকের তারিখ)। তখন delivered আর final থাকে না, যেটা জীবনচক্রে সত্যিকারের একটা পরিবর্তন, তাই table, diagram আর test একসাথে update করো। এর জন্য cancel আবার ব্যবহার কোরো না: delivery র পরের return আর shipping এর আগের cancel দুইটা আলাদা business event, টাকার হিসাবও আলাদা।
ছোট্ট quiz
১. Status এর জন্য string এর চেয়ে enum ভালো কেন?
উত্তর
Value গুলোর set বন্ধ, আর compiler বা type checker সেটা check করে, তাই typo আর বানানো state ঢুকতে পারে না, আর state handle করা প্রতিটা জায়গা খুঁজে বের করা যায়।
২. Legal transition এর list কোথায় থাকা উচিত?
উত্তর
এক জায়গায়, সাধারণত একটা table বা match হিসেবে, state বদলানোর একমাত্র method টার পাশে। Screen event পাঠায়; rule ঠিক করে না।
৩. Final state কী, আর এটা আলাদা করে বলে দেওয়া কেন দরকার?
উত্তর
যে state থেকে বের হওয়ার কোনো transition নেই, যেমন delivered বা cancelled। এটা বলে দিলে দেরিতে আসা বা বারবার আসা event শেষ হয়ে যাওয়া object কে আর বদলাতে পারে না।
৪. isPaid, isShipped, isCancelled boolean গুলোর বদলে একটা enum কেন?
উত্তর
তিনটা boolean এ আটটা combination হয়, যার বেশিরভাগই অসম্ভব। একটা enum field এ ঠিক ততগুলো state থাকে যতগুলো business এর আছে।
তোমার challenge: return
Return এর flow যোগ করো: delivered order return করা যাবে।
returnevent আরreturnedstate যোগ করো, সাথে একটা নতুন transition: delivered → return → returned।- গল্পের শেষে order 1001 এ
returnপাঠাও, তারপরcancel।
চালানোর আগে আন্দাজ করো নতুন দুইটা লাইন আর নতুন history কী হবে।
হয়েছে বুঝবে যখন নতুন লাইন দুইটা হবে return delivered -> returned আর cancel returned : cannot cancel when returned, history এর লাইন হবে history 1001: placed > paid > shipped > delivered > returned, আর অন্য কোনো লাইন বদলাবে না।
এক কথায়
State গুলো enum হিসেবে লেখো, প্রতিটা legal move একটা table এ রাখো, state private রাখো আর বদলাও শুধু এমন একটা method দিয়ে যেটা table দেখে নেয়। Table এ যা নেই তা ফিরিয়ে দেওয়া হয়, তাই অসম্ভব state আর তৈরিই হয় না, আর table, diagram আর test সবসময় একই কথা বলে।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →