পাঠ ১৮.৩

একটি Object-Oriented design pattern implement করা

Implementing an Object-Oriented Design Pattern

State pattern এক classic OOP design pattern — একটা value-র behaviour তার ভেতরের state অনুসারে পরিবর্তিত হয়। আমরা এখানে এক blog post-এর workflow build করব — draft → pending review → published — দু'ভাবে: trait object দিয়ে classical state pattern, এবং Rust-এর type system দিয়ে state-as-type।

Functional requirement

  1. Blog post শুরু হবে empty draft হিসেবে।
  2. Draft শেষ হলে review request করা যাবে।
  3. Approve হলে publish।
  4. শুধু published post-এর content print হবে।
  5. Invalid transition (যেমন draft সরাসরি approve) effect ফেলবে না।

API আগে থেকে দেখা

src/main.rsrust
use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

User শুধু Post type-এর সাথে interact করছে — state internal।

Post struct আর Draft state

src/lib.rsrust
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

state field Option<Box<dyn State>> — current state-কে boxed trait object হিসেবে রাখে। Option দরকার হবে state move-out করে replace করতে।

Text যোগ করা

impl Post {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

add_text state-এর উপর depend করে না, তাই এটা state pattern-এর অংশ না।

Draft post-এর content empty

impl Post {
    pub fn content(&self) -> &str {
        ""
    }
}

প্রাথমিকভাবে — সবসময় empty। পরে state-এর সাথে wire করব।

Review request — state transition

impl Post {
    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

মূল কয়েকটা ব্যাপার:

  • self: Box<Self> — method শুধু boxed instance-এ call করা যাবে; ownership consume করে।
  • Option::take() — state move করে নেয়, place-এ None বসায়। এতে আগের state drop না হওয়া পর্যন্ত আমরা ownership ধরে রাখতে পারি।
  • Draft::request_review — নতুন PendingReview return।
  • PendingReview::request_reviewself return; idempotent।

approve method

impl Post {
    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}
  • Draft::approve — নিজেকে return; draft সরাসরি approve হয় না।
  • PendingReview::approvePublished
  • Published::* — দুই-ই self return; idempotent।

content method-কে state-aware করা

impl Post {
    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
}
trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

Default content empty string return করে — Draft আর PendingReview এই default-ই use করে। শুধু Published override করে actual content ফেরত দেয়। Lifetime 'a return-করা reference-কে post parameter-এর সাথে tie করে।

এই pattern-এর সুবিধা

  • State-specific behaviour সংশ্লিষ্ট state struct-এ encapsulated।
  • Post জানে না কোন state এখন active — transition state নিজেই handle করে।
  • নতুন state add করতে গেলে match দিয়ে সব জায়গায় code edit-এর দরকার নেই — শুধু নতুন struct + trait impl।

বিকল্প — state-as-type

Rust-এর type system invalid state compile-time-এ অসম্ভব করে দিতে পারে। প্রত্যেক state-এর জন্য আলাদা type:

src/lib.rsrust
pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

লক্ষণীয় — DraftPost-এ content method নেই। তাই draft-এর content পড়ার চেষ্টা compile error দেবে।

Type transformation দিয়ে transition

src/lib.rsrust
impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}
  • Method self consume করে নতুন type return করে।
  • DraftPost::request_reviewPendingReviewPost
  • PendingReviewPost::approvePost
  • Workflow path enforced — Draft → PendingReview → Published; এর বাইরে যাওয়াই অসম্ভব।

নতুন main

src/main.rsrust
use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

Shadowing let post = ... দিয়ে variable নতুন type-এ rebind। Draft বা pending review-তে post.content() call করলে compile error।

দুই approach তুলনা

  • Trait object pattern — state hidden, Post single type, runtime check; नया state যোগ করা সহজ।
  • Type encoding — invalid usage compile-time-এ অসম্ভব, কিন্তু state visible এবং নতুন state add করতে refactor বেশি লাগে।
  • Enum? — সম্ভব, কিন্তু সব জায়গায় match ছড়িয়ে পড়ে; নতুন variant add করলে অনেক জায়গায় edit।

Rust-এ type-encoding বেশি idiomatic — bug compile-time-এ ধরা পড়ে। কিন্তু কোনটা ভালো সেটা নির্ভর করে problem-এর প্রকৃতি ও extensibility requirement-এর উপর।

এই পাঠ থেকে যা শিখলে

  • State pattern — value-র behaviour internal state-এ পরিবর্তিত হয়; Rust-এ trait object দিয়ে সরাসরি implement।
  • self: Box<Self> + Option::take() — state struct ownership move করে replace।
  • Default trait method শুধু কিছু variant-এ override — code repetition কমায়।
  • Rust-এর type system দিয়ে state-as-type — invalid path compile-time-এ block।
  • Two approach-ই trade-off-এ আসে; choose by extensibility vs compile-time guarantee।