مقدمه
در این درس، با پیادهسازی یک الگوی طراحی شیءگرا به نام «الگوی وضعیت» یا State Pattern،
مفاهیمی را که در این فصل یاد گرفتیم، به صورت عملی به کار خواهیم بست. الگوی State زمانی
مفید است که یک مقدار میتواند بسته به وضعیت (state) داخلی خود، رفتارهای متفاوتی داشته باشد. در
این الگو، ما مجموعهای از «اشیاء وضعیت» (state objects) را تعریف میکنیم که هر کدام رفتار مربوط
به یک وضعیت خاص را کپسولهسازی میکنند. مقدار اصلی، به جای نگهداری مستقیم منطق، ارجاعی به یکی از
این اشیاء وضعیت را نگه میدارد و کار را به آن محول میکند.
ما قصد داریم یک سیستم برای گردش کار یک پست وبلاگ پیادهسازی کنیم. یک پست میتواند در وضعیتهای
مختلفی مانند Draft (پیشنویس)، PendingReview (در انتظار بازبینی) و Published (منتشر شده)
باشد و بسته به وضعیت فعلی، رفتارهای متفاوتی (مانند افزودن متن یا تایید کردن) داشته باشد.
تعریف Post و Stateها
ابتدا struct اصلی Post را تعریف میکنیم. این struct یک فیلد به نام state دارد که یک
trait object از نوع Box<dyn State> را نگه میدارد. این به Post اجازه میدهد تا هر نوعی را
که State را پیادهسازی کرده، به عنوان وضعیت فعلی خود بپذیرد.
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(),
}
}
}
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 در وضعیت نهفته است. هر وضعیت، تنها متدهایی
را پیادهسازی میکند که در آن وضعیت معتبر هستند.
src/lib.rs
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
impl State for PendingReview {
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
impl State for Published {
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
حالا متدهای Post به سادگی کار را به شیء وضعیت فعلی خود محول میکنند.
src/lib.rs
impl Post {
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 {
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»
خواهیم رفت.