مقدمه

در این درس، با پیاده‌سازی یک الگوی طراحی شیءگرا به نام «الگوی وضعیت» یا State Pattern، مفاهیمی را که در این فصل یاد گرفتیم، به صورت عملی به کار خواهیم بست. الگوی State زمانی مفید است که یک مقدار می‌تواند بسته به وضعیت (state) داخلی خود، رفتارهای متفاوتی داشته باشد. در این الگو، ما مجموعه‌ای از «اشیاء وضعیت» (state objects) را تعریف می‌کنیم که هر کدام رفتار مربوط به یک وضعیت خاص را کپسوله‌سازی می‌کنند. مقدار اصلی، به جای نگهداری مستقیم منطق، ارجاعی به یکی از این اشیاء وضعیت را نگه می‌دارد و کار را به آن محول می‌کند.

ما قصد داریم یک سیستم برای گردش کار یک پست وبلاگ پیاده‌سازی کنیم. یک پست می‌تواند در وضعیت‌های مختلفی مانند Draft (پیش‌نویس)، PendingReview (در انتظار بازبینی) و Published (منتشر شده) باشد و بسته به وضعیت فعلی، رفتارهای متفاوتی (مانند افزودن متن یا تایید کردن) داشته باشد.

تعریف Post و Stateها

ابتدا struct اصلی Post را تعریف می‌کنیم. این struct یک فیلد به نام state دارد که یک trait object از نوع Box<dyn State> را نگه می‌دارد. این به Post اجازه می‌دهد تا هر نوعی را که State را پیاده‌سازی کرده، به عنوان وضعیت فعلی خود بپذیرد.

Copy Icon src/lib.rs
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(),
        }
    }
    // ... other methods ...
}

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 Draft {}
impl State for Draft { /* ... */ }

struct PendingReview {}
impl State for PendingReview { /* ... */ }

struct Published {}
impl State for Published { /* ... */ }

Trait مربوط به State متدهایی را تعریف می‌کند که رفتار هر وضعیت را مشخص می‌کنند. هر متدی که باعث تغییر وضعیت می‌شود (مانند request_review)، مالکیت وضعیت فعلی را گرفته و یک وضعیت جدید را برمی‌گرداند.

پیاده‌سازی رفتارها و انتقال وضعیت

منطق اصلی در پیاده‌سازی State برای هر struct در وضعیت نهفته است. هر وضعیت، تنها متدهایی را پیاده‌سازی می‌کند که در آن وضعیت معتبر هستند.

Copy Icon src/lib.rs
impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {}) // Transition to PendingReview
    }
    // ... other methods return `self` as they are invalid actions in Draft state ...
}

impl State for PendingReview {
    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {}) // Transition to Published
    }
    // ... other methods ...
}

impl State for Published {
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content // Only Published posts show content
    }
    // ... other methods ...
}

حالا متدهای Post به سادگی کار را به شیء وضعیت فعلی خود محول می‌کنند.

Copy Icon src/lib.rs
impl Post {
    // ... new() ...
    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review());
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve());
        }
    }
    
    pub fn content(&self) -> &str {
        // We call the `content` method on the state object
        // and pass the post instance itself.
        self.state.as_ref().unwrap().content(self)
    }
}

الگوی if let Some(s) = self.state.take() به ما اجازه می‌دهد تا به صورت موقت مالکیت وضعیت را از Post خارج کنیم، متد مربوطه را روی آن فراخوانی کنیم تا وضعیت جدید را برگرداند، و سپس وضعیت جدید را دوباره در Post قرار دهیم.

مزایای الگوی State

این پیاده‌سازی از الگوی State به روش Rust، مزایای زیادی دارد:

  • کپسوله‌سازی: تمام رفتارها و قوانین مربوط به هر وضعیت، در struct مربوط به خودش کپسوله شده است. Post اصلی هیچ اطلاعی از این قوانین ندارد.
  • ایمنی نوع: سیستم نوع Rust تضمین می‌کند که شما نمی‌توانید یک عملیات نامعتبر را در یک وضعیت خاص فراخوانی کنید. برای مثال، فراخوانی approve روی یک پست Draft در سطح trait هیچ کاری انجام نمی‌دهد و حالت را تغییر نمی‌دهد.
  • کد خوانا: منطق اصلی Post بسیار ساده و خوانا باقی می‌ماند و تنها به محول کردن کارها به شیء وضعیت فعلی می‌پردازد.

در این درس با پیاده‌سازی یک الگوی طراحی شیءگرا، قدرت trait objectها را برای نوشتن کدهای ماژولار و ایمن در عمل دیدیم. با این درس، فصل «شی‌گرایی در Rust» به پایان می‌رسد. ما دیدیم که چگونه Rust با وجود نداشتن وراثت به سبک کلاسیک، با استفاده از مفاهیمی مانند structها، traitها و trait objectها، به شیوه‌ای قدرتمند و منحصر به فرد، اصول شیءگرایی را پیاده‌سازی می‌کند. در فصل بعدی، به سراغ یکی دیگر از ویژگی‌های قدرتمند Rust، یعنی «بررسی دقیق‌تر Pattern Matching» خواهیم رفت.