پیاده‌سازی یک الگوی طراحی شی‌گرا

الگوی وضعیت یک الگوی طراحی شی‌گرا است. هسته این الگو این است که مجموعه‌ای از وضعیت‌ها را که یک مقدار می‌تواند به‌طور داخلی داشته باشد، تعریف کنیم. این وضعیت‌ها با مجموعه‌ای از اشیای وضعیت نمایش داده می‌شوند و رفتار مقدار بر اساس وضعیت آن تغییر می‌کند. قصد داریم مثالی از یک ساختار blog post (پست وبلاگ) را بررسی کنیم که یک فیلد برای نگه‌داشتن وضعیت دارد. این وضعیت یک شیء از مجموعه “پیش‌نویس” (draft)، “در حال بررسی” (review)، یا “منتشرشده” (published) خواهد بود.

اشیای وضعیت قابلیت‌هایی را به اشتراک می‌گذارند: در Rust، البته، ما از ساختارها (structs) و صفت‌ها (traits) به جای اشیا و ارث‌بری استفاده می‌کنیم. هر شیء وضعیت مسئول رفتار خود و مدیریت زمانی است که باید به وضعیت دیگری تغییر کند. مقداری که یک شیء وضعیت را نگه می‌دارد، هیچ اطلاعی از رفتارهای مختلف وضعیت‌ها یا زمان تغییر وضعیت ندارد.

مزیت استفاده از الگوی وضعیت این است که وقتی نیازهای تجاری برنامه تغییر می‌کنند، نیازی به تغییر کد مقداری که وضعیت را نگه می‌دارد یا کدی که از آن مقدار استفاده می‌کند، نداریم. تنها لازم است کد داخل یکی از اشیای وضعیت را برای تغییر قوانین آن یا شاید اضافه کردن اشیای وضعیت جدید به‌روزرسانی کنیم.

ابتدا الگوی وضعیت را به روش سنتی شی‌گرایی پیاده‌سازی می‌کنیم، سپس از رویکردی که در Rust طبیعی‌تر است استفاده خواهیم کرد. بیایید به‌صورت مرحله‌به‌مرحله پیاده‌سازی یک فرآیند کاری پست وبلاگ با استفاده از الگوی وضعیت را بررسی کنیم.

قابلیت نهایی به این شکل خواهد بود:

  1. یک پست وبلاگ به‌صورت یک پیش‌نویس خالی شروع می‌شود.
  2. وقتی پیش‌نویس تمام شد، بررسی پست درخواست می‌شود.
  3. وقتی پست تأیید شد، منتشر می‌شود.
  4. تنها پست‌های وبلاگی که منتشر شده‌اند متن را برای چاپ بازمی‌گردانند، بنابراین پست‌های تأییدنشده نمی‌توانند به‌طور تصادفی منتشر شوند.

هر تغییر دیگری که روی یک پست تلاش شود نباید تأثیری داشته باشد. برای مثال، اگر بخواهیم یک پست وبلاگ پیش‌نویس را قبل از درخواست بررسی تأیید کنیم، پست باید به‌عنوان پیش‌نویس منتشرنشده باقی بماند.

لیستینگ 18-11 این فرآیند کاری را به‌صورت کدی نشان می‌دهد: این یک نمونه از استفاده از API است که قصد داریم در یک crate کتابخانه‌ای به نام blog پیاده‌سازی کنیم. این کد هنوز کامپایل نخواهد شد زیرا هنوز crate blog را پیاده‌سازی نکرده‌ایم.

Filename: src/main.rs
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());
}
Listing 18-11: کدی که رفتار مورد نظر ما برای crate blog را نشان می‌دهد

ما می‌خواهیم به کاربر اجازه دهیم یک پست وبلاگ پیش‌نویس جدید با Post::new ایجاد کند. می‌خواهیم امکان اضافه کردن متن به پست وبلاگ را فراهم کنیم. اگر فوراً بخواهیم محتوای پست را دریافت کنیم، قبل از تأیید، نباید هیچ متنی دریافت کنیم، زیرا پست هنوز یک پیش‌نویس است. ما از assert_eq! در کد برای اهداف نمایشی استفاده کرده‌ایم. یک تست واحد عالی برای این مورد این است که تأیید کنیم یک پست وبلاگ پیش‌نویس یک رشته خالی از متد content بازمی‌گرداند، اما قصد نداریم برای این مثال تست بنویسیم.

سپس می‌خواهیم امکان درخواست بررسی برای پست فراهم شود و می‌خواهیم content در حین انتظار برای بررسی یک رشته خالی بازگرداند. وقتی پست تأیید شود، باید منتشر شود، به این معنی که متن پست هنگام فراخوانی content بازگردانده خواهد شد.

توجه داشته باشید که تنها نوعی که از crate تعامل داریم، نوع Post است. این نوع از الگوی وضعیت استفاده خواهد کرد و مقداری نگه می‌دارد که یکی از سه شیء وضعیت نمایش‌دهنده وضعیت‌های مختلف یک پست باشد—پیش‌نویس، در انتظار بررسی، یا منتشرشده. تغییر از یک وضعیت به وضعیت دیگر به‌صورت داخلی در نوع Post مدیریت می‌شود. تغییر وضعیت‌ها در پاسخ به متدهایی که کاربران کتابخانه ما روی نمونه Post فراخوانی می‌کنند اتفاق می‌افتد، اما کاربران مجبور نیستند تغییر وضعیت‌ها را مستقیماً مدیریت کنند. همچنین، کاربران نمی‌توانند در مورد وضعیت‌ها اشتباه کنند، مانند انتشار یک پست قبل از بررسی آن.

تعریف Post و ایجاد یک نمونه جدید در وضعیت پیش‌نویس

بیایید پیاده‌سازی کتابخانه را شروع کنیم! می‌دانیم که به یک ساختار Post عمومی نیاز داریم که مقداری محتوا را نگه می‌دارد، بنابراین با تعریف این ساختار و یک تابع مرتبط new عمومی برای ایجاد یک نمونه از Post شروع می‌کنیم. این تعاریف در لیستینگ 18-12 آمده‌اند. همچنین، یک صفت خصوصی State ایجاد خواهیم کرد که رفتاری را که تمام اشیای وضعیت برای Post باید داشته باشند تعریف می‌کند.

سپس، Post یک شیء صفت Box<dyn State> را درون یک Option<T> در یک فیلد خصوصی به نام state نگه خواهد داشت تا شیء وضعیت را مدیریت کند. در ادامه خواهید دید که چرا Option<T> ضروری است.

Filename: 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 {}

struct Draft {}

impl State for Draft {}
Listing 18-12: تعریف یک ساختار Post و یک تابع new که یک نمونه جدید از Post ایجاد می‌کند، یک صفت State، و یک ساختار Draft

صفت State رفتاری را که وضعیت‌های مختلف پست‌ها به اشتراک می‌گذارند تعریف می‌کند. اشیای وضعیت شامل Draft, PendingReview و Published هستند و همه آن‌ها صفت State را پیاده‌سازی خواهند کرد. فعلاً صفت هیچ متدی ندارد و ما با تعریف تنها وضعیت Draft شروع می‌کنیم، زیرا این وضعیت است که می‌خواهیم پست در آن شروع شود.

وقتی یک Post جدید ایجاد می‌کنیم، فیلد state آن را به یک مقدار Some تنظیم می‌کنیم که یک Box را نگه می‌دارد. این Box به یک نمونه جدید از ساختار Draft اشاره می‌کند. این کار تضمین می‌کند که هرگاه یک نمونه جدید از Post ایجاد شود، به‌عنوان یک پیش‌نویس شروع شود. از آنجا که فیلد state در Post خصوصی است، هیچ راهی برای ایجاد یک Post در وضعیت دیگری وجود ندارد! در تابع Post::new، فیلد content را به یک String جدید و خالی تنظیم می‌کنیم.

ذخیره متن محتوای پست

در لیستینگ 18-11 دیدیم که می‌خواهیم بتوانیم یک متد به نام add_text فراخوانی کنیم و یک &str به آن بدهیم که به عنوان محتوای متنی پست وبلاگ اضافه شود. این کار را به‌صورت یک متد پیاده‌سازی می‌کنیم تا فیلد content را به‌جای تعریف آن به‌صورت pub کنترل کنیم و بتوانیم در آینده متدی برای کنترل چگونگی خواندن داده فیلد content پیاده‌سازی کنیم. متد add_text نسبتاً ساده است، بنابراین بیایید پیاده‌سازی آن را به بلوک impl Post در لیستینگ 18-13 اضافه کنیم:

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

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

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

trait State {}

struct Draft {}

impl State for Draft {}
Listing 18-13: پیاده‌سازی متد add_text برای افزودن متن به content یک پست

متد add_text یک ارجاع متغیر به self می‌گیرد، زیرا در حال تغییر نمونه Post هستیم که add_text روی آن فراخوانی شده است. سپس، متد push_str را روی String موجود در content فراخوانی می‌کنیم و آرگومان text را برای افزودن به محتوای ذخیره‌شده به آن می‌دهیم. این رفتار به وضعیتی که پست در آن قرار دارد وابسته نیست، بنابراین بخشی از الگوی وضعیت نیست. متد add_text هیچ تعاملی با فیلد state ندارد، اما بخشی از رفتاری است که می‌خواهیم پشتیبانی کنیم.

اطمینان از خالی بودن محتوای یک پست پیش‌نویس

حتی پس از فراخوانی add_text و افزودن محتوایی به پست، همچنان می‌خواهیم متد content یک برش رشته خالی بازگرداند، زیرا پست هنوز در وضعیت پیش‌نویس است، همان‌طور که در خط 7 لیستینگ 18-11 نشان داده شده است. فعلاً متد content را با ساده‌ترین چیزی که این نیاز را برآورده می‌کند پیاده‌سازی می‌کنیم: همیشه بازگرداندن یک برش رشته خالی. بعداً این را تغییر خواهیم داد تا قابلیت تغییر وضعیت پست به حالت منتشرشده را اضافه کنیم. تاکنون، پست‌ها فقط می‌توانند در وضعیت پیش‌نویس باشند، بنابراین محتوای پست باید همیشه خالی باشد. لیستینگ 18-14 این پیاده‌سازی موقت را نشان می‌دهد:

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

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

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

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

trait State {}

struct Draft {}

impl State for Draft {}
Listing 18-14: افزودن یک پیاده‌سازی موقت برای متد content در Post که همیشه یک برش رشته خالی بازمی‌گرداند

با افزودن این متد content، تمام موارد تا خط 7 لیستینگ 18-11 به درستی کار می‌کنند.

درخواست بررسی پست و تغییر وضعیت آن

در مرحله بعد، باید قابلیت درخواست بررسی پست را اضافه کنیم، که باید وضعیت آن را از Draft به PendingReview تغییر دهد. لیستینگ 18-15 این کد را نشان می‌دهد:

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

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

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

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

    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
    }
}
Listing 18-15: پیاده‌سازی متدهای request_review برای Post و صفت State

ما یک متد عمومی به نام request_review به Post اضافه می‌کنیم که یک ارجاع متغیر به self می‌گیرد. سپس یک متد داخلی request_review را روی وضعیت فعلی Post فراخوانی می‌کنیم، و این متد دوم وضعیت فعلی را مصرف کرده و یک وضعیت جدید بازمی‌گرداند.

ما متد request_review را به صفت State اضافه می‌کنیم؛ تمام انواعی که این صفت را پیاده‌سازی می‌کنند اکنون باید متد request_review را پیاده‌سازی کنند. توجه داشته باشید که به جای self، &self یا &mut self به‌عنوان اولین پارامتر متد، از self: Box<Self> استفاده کرده‌ایم. این نحو به این معنی است که متد فقط زمانی معتبر است که روی یک Box نگه‌دارنده نوع فراخوانی شود. این نحو مالکیت Box<Self> را می‌گیرد و وضعیت قدیمی را باطل می‌کند تا مقدار وضعیت Post بتواند به یک وضعیت جدید تبدیل شود.

برای مصرف وضعیت قدیمی، متد request_review نیاز به گرفتن مالکیت مقدار وضعیت دارد. اینجاست که Option در فیلد state از Post وارد عمل می‌شود: ما متد take را فراخوانی می‌کنیم تا مقدار Some را از فیلد state خارج کرده و یک مقدار None به جای آن قرار دهیم، زیرا Rust به ما اجازه نمی‌دهد فیلدهای ساختار را بدون مقدار رها کنیم. این کار به ما امکان می‌دهد مقدار state را از Post منتقل کنیم، نه اینکه آن را قرض بگیریم. سپس مقدار state پست را به نتیجه این عملیات تنظیم خواهیم کرد.

باید به‌طور موقت state را به None تنظیم کنیم، نه اینکه مستقیماً آن را با کدی مانند self.state = self.state.request_review(); تنظیم کنیم، تا مالکیت مقدار state را بدست آوریم. این کار اطمینان می‌دهد که Post نمی‌تواند از مقدار قدیمی state پس از تبدیل آن به یک وضعیت جدید استفاده کند.

متد request_review در Draft یک نمونه جدید از ساختار PendingReview را که نشان‌دهنده وضعیت زمانی است که یک پست منتظر بررسی است بازمی‌گرداند. ساختار PendingReview نیز متد request_review را پیاده‌سازی می‌کند، اما هیچ تبدیلی انجام نمی‌دهد. بلکه خودش را بازمی‌گرداند، زیرا وقتی برای یک پست در وضعیت PendingReview درخواست بررسی می‌کنیم، باید در همان وضعیت باقی بماند.

اکنون می‌توانیم مزایای الگوی وضعیت را مشاهده کنیم: متد request_review در Post بدون توجه به مقدار state آن یکسان است. هر وضعیت مسئول قوانین خاص خود است.

ما متد content در Post را به همان صورت باقی می‌گذاریم که یک برش رشته خالی بازمی‌گرداند. اکنون می‌توانیم یک Post در وضعیت PendingReview و همچنین در وضعیت Draft داشته باشیم، اما می‌خواهیم همان رفتار در وضعیت PendingReview نیز باشد. لیستینگ 18-11 اکنون تا خط 10 کار می‌کند!

افزودن approve برای تغییر رفتار content

متد approve شبیه متد request_review خواهد بود: این متد مقدار state را به مقداری تنظیم می‌کند که وضعیت فعلی هنگام تأیید باید داشته باشد، همان‌طور که در لیستینگ 18-16 نشان داده شده است:

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

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

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

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

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

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 {
    // --snip--
    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 {
    // --snip--
    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
    }
}
Listing 18-16: پیاده‌سازی متد approve در Post و صفت State

ما متد approve را به صفت State اضافه می‌کنیم و یک ساختار جدید که صفت State را پیاده‌سازی می‌کند، یعنی وضعیت Published، اضافه می‌کنیم.

مشابه کاری که request_review در PendingReview انجام می‌دهد، اگر متد approve را روی یک Draft فراخوانی کنیم، هیچ تأثیری نخواهد داشت زیرا approve مقدار self را بازمی‌گرداند. وقتی approve را روی PendingReview فراخوانی می‌کنیم، یک نمونه جدید از ساختار Published که در یک Box قرار دارد، بازمی‌گرداند. ساختار Published صفت State را پیاده‌سازی می‌کند، و برای متدهای request_review و approve خودش را بازمی‌گرداند، زیرا در این موارد پست باید در وضعیت Published باقی بماند.

اکنون باید متد content در Post را به‌روزرسانی کنیم. می‌خواهیم مقدار بازگشتی از content به وضعیت فعلی Post بستگی داشته باشد، بنابراین می‌خواهیم Post این وظیفه را به متد content تعریف‌شده در وضعیت خود واگذار کند، همان‌طور که در لیستینگ 18-17 نشان داده شده است:

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

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

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

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--

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

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
    }
}
Listing 18-17: به‌روزرسانی متد content در Post برای ارجاع به متد content در State

چون هدف این است که تمام این قوانین در داخل ساختارهایی که صفت State را پیاده‌سازی می‌کنند باقی بماند، ما یک متد content را روی مقدار state فراخوانی می‌کنیم و نمونه پست (یعنی self) را به‌عنوان آرگومان به آن می‌دهیم. سپس مقداری که از متد content روی مقدار state بازمی‌گردد را بازمی‌گردانیم.

ما متد as_ref را روی Option فراخوانی می‌کنیم زیرا می‌خواهیم یک ارجاع به مقدار داخل Option داشته باشیم، نه مالکیت مقدار. چون state یک Option<Box<dyn State>> است، وقتی as_ref را فراخوانی می‌کنیم، یک Option<&Box<dyn State>> بازمی‌گردد. اگر as_ref را فراخوانی نکنیم، با یک خطا مواجه می‌شویم زیرا نمی‌توانیم state را از &self که به‌عنوان پارامتر به تابع داده شده است خارج کنیم.

سپس متد unwrap را فراخوانی می‌کنیم که می‌دانیم هرگز وحشت (panic) نخواهد کرد، زیرا می‌دانیم متدهای Post تضمین می‌کنند که state همیشه یک مقدار Some دارد وقتی این متدها کارشان را تمام می‌کنند. این یکی از مواردی است که در بخش “مواردی که شما اطلاعات بیشتری نسبت به کامپایلر دارید” در فصل 9 در مورد آن صحبت کردیم، زمانی که می‌دانیم یک مقدار None هرگز ممکن نیست، حتی اگر کامپایلر نتواند این موضوع را درک کند.

در این مرحله، وقتی content را روی &Box<dyn State> فراخوانی می‌کنیم، تبدیل خودکار به نوع ارجاع (deref coercion) روی & و Box اعمال می‌شود تا متد content در نهایت روی نوعی که صفت State را پیاده‌سازی می‌کند، فراخوانی شود. این بدان معناست که باید content را به تعریف صفت State اضافه کنیم، و اینجا جایی است که منطق مربوط به بازگرداندن محتوا بر اساس وضعیت فعلی قرار خواهد گرفت، همان‌طور که در لیستینگ 18-18 نشان داده شده است:

Filename: 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(),
        }
    }

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

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

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

trait State {
    // --snip--
    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 {
        ""
    }
}

// --snip--

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 {
    // --snip--
    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
    }
}
Listing 18-18: Adding the content method to the State trait

ما برای متد content یک پیاده‌سازی پیش‌فرض اضافه می‌کنیم که یک برش رشته خالی بازمی‌گرداند. این کار باعث می‌شود نیازی به پیاده‌سازی content روی ساختارهای Draft و PendingReview نداشته باشیم. ساختار Published متد content را بازنویسی کرده و مقدار موجود در post.content را بازمی‌گرداند.

توجه داشته باشید که برای این متد نیاز به حاشیه‌نویسی طول عمر داریم، همان‌طور که در فصل 10 توضیح داده شد. چون یک ارجاع به یک post را به‌عنوان آرگومان می‌گیریم و یک ارجاع به بخشی از آن post را بازمی‌گردانیم، طول عمر ارجاع بازگشتی به طول عمر آرگومان post مرتبط است.

و تمام—اکنون تمام لیستینگ 18-11 کار می‌کند! ما الگوی وضعیت را با قوانین مربوط به فرآیند کاری پست وبلاگ پیاده‌سازی کرده‌ایم. منطق مربوط به قوانین در اشیای وضعیت قرار دارد، نه اینکه در سراسر Post پراکنده باشد.

چرا از Enum استفاده نکردیم؟

ممکن است این سؤال برای شما پیش آمده باشد که چرا از یک enum با حالت‌های مختلف پست به‌عنوان متغیرها استفاده نکردیم. این قطعاً یک راه‌حل ممکن است؛ آن را امتحان کنید و نتایج نهایی را مقایسه کنید تا ببینید کدام را ترجیح می‌دهید! یکی از معایب استفاده از enum این است که هر جا مقدار enum بررسی می‌شود نیاز به یک عبارت match یا چیزی مشابه برای مدیریت تمام متغیرهای ممکن داریم. این می‌تواند نسبت به راه‌حل اشیای صفتی که استفاده کردیم تکراری‌تر باشد.

مزایا و معایب الگوی وضعیت

ما نشان داده‌ایم که Rust قادر است الگوی وضعیت شی‌گرا را برای کپسوله کردن رفتارهای مختلف یک پست در هر حالت پیاده‌سازی کند. متدهای Post هیچ اطلاعی از رفتارهای مختلف ندارند. با روشی که کد را سازمان‌دهی کرده‌ایم، تنها باید در یک مکان به‌دنبال راه‌های مختلف رفتار یک پست منتشرشده بگردیم: پیاده‌سازی صفت State روی ساختار Published.

اگر بخواهیم یک پیاده‌سازی جایگزین ایجاد کنیم که از الگوی وضعیت استفاده نمی‌کند، ممکن است به‌جای آن از عبارات match در متدهای Post یا حتی در کد main استفاده کنیم که وضعیت پست را بررسی کرده و رفتار را در همان مکان‌ها تغییر می‌دهد. این به این معناست که باید در مکان‌های مختلفی جست‌وجو کنیم تا تمام پیامدهای یک پست در حالت منتشرشده را بفهمیم! و با اضافه شدن حالت‌های بیشتر، این موضوع فقط بدتر خواهد شد: هر یک از آن عبارات match نیاز به یک شاخه دیگر خواهند داشت.

با الگوی وضعیت، متدهای Post و مکان‌هایی که از Post استفاده می‌کنیم نیازی به عبارات match ندارند، و برای اضافه کردن یک حالت جدید، فقط کافی است یک ساختار جدید اضافه کرده و متدهای صفت را روی همان ساختار پیاده‌سازی کنیم.

پیاده‌سازی با استفاده از الگوی وضعیت به‌راحتی قابلیت گسترش برای اضافه کردن عملکردهای بیشتر را دارد. برای دیدن سادگی نگهداری کدی که از الگوی وضعیت استفاده می‌کند، چند پیشنهاد زیر را امتحان کنید:

  • یک متد reject اضافه کنید که وضعیت پست را از PendingReview به Draft تغییر دهد.
  • دو فراخوانی به approve نیاز داشته باشید تا وضعیت به Published تغییر کند.
  • اجازه دهید کاربران فقط زمانی که یک پست در حالت Draft است متن محتوا اضافه کنند. نکته: بگذارید شیء وضعیت مسئول تغییراتی باشد که ممکن است در محتوا ایجاد شود، اما مسئول اصلاح مستقیم Post نباشد.

یکی از معایب الگوی وضعیت این است که به دلیل اینکه وضعیت‌ها انتقال بین حالت‌ها را پیاده‌سازی می‌کنند، برخی از وضعیت‌ها به یکدیگر وابسته هستند. اگر یک حالت دیگر بین PendingReview و Published اضافه کنیم، مانند Scheduled، باید کد در PendingReview را تغییر دهیم تا به Scheduled منتقل شود. اگر نیازی نبود که PendingReview با اضافه شدن یک حالت جدید تغییر کند، کار کمتری می‌داشتیم، اما این به معنای تغییر به یک الگوی طراحی دیگر خواهد بود.

یکی دیگر از معایب این است که ما برخی از منطق‌ها را تکرار کرده‌ایم. برای حذف برخی از این تکرارها، ممکن است سعی کنیم برای متدهای request_review و approve در صفت State پیاده‌سازی پیش‌فرضی ایجاد کنیم که self را بازمی‌گرداند؛ با این حال، این با dyn سازگار نخواهد بود، زیرا صفت دقیقاً نمی‌داند self چه خواهد بود. ما می‌خواهیم بتوانیم از State به‌عنوان یک شیء صفت استفاده کنیم، بنابراین متدهای آن باید با dyn سازگار باشند.

پیاده‌سازی مشابه متدهای request_review و approve روی Post نیز نوعی تکرار است. هر دو متد اجرای متد مشابه روی مقدار موجود در فیلد state از Option را به آن واگذار کرده و مقدار جدید فیلد state را به نتیجه تنظیم می‌کنند. اگر متدهای زیادی روی Post داشته باشیم که این الگو را دنبال می‌کنند، ممکن است تعریف یک ماکرو را برای حذف این تکرار در نظر بگیریم (بخش “ماکروها” در فصل 20 را ببینید).

با پیاده‌سازی الگوی وضعیت دقیقاً همان‌طور که برای زبان‌های شی‌گرا تعریف شده است، به‌طور کامل از نقاط قوت Rust استفاده نمی‌کنیم. بیایید نگاهی به تغییراتی بیندازیم که می‌توانیم در crate blog ایجاد کنیم تا وضعیت‌ها و انتقالات نامعتبر به خطاهای زمان کامپایل تبدیل شوند.

کدگذاری وضعیت‌ها و رفتار به‌عنوان انواع

به شما نشان خواهیم داد که چگونه الگوی وضعیت را دوباره طراحی کنید تا مجموعه‌ای متفاوت از مزایا و معایب به دست آورید. به‌جای اینکه وضعیت‌ها و انتقالات را کاملاً کپسوله کنیم تا کد خارجی از آن‌ها اطلاعی نداشته باشد، وضعیت‌ها را به انواع مختلف کدگذاری می‌کنیم. در نتیجه، سیستم بررسی نوع Rust تلاش برای استفاده از پست‌های پیش‌نویس در جاهایی که فقط پست‌های منتشرشده مجاز هستند را با صدور یک خطای کامپایلر متوقف می‌کند.

ابتدا قسمت اول main در لیستینگ 18-11 را در نظر بگیرید:

Filename: src/main.rs
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());
}

ما همچنان امکان ایجاد پست‌های جدید در وضعیت پیش‌نویس با استفاده از Post::new و افزودن متن به محتوای پست را فراهم می‌کنیم. اما به‌جای داشتن متد content روی یک پست پیش‌نویس که یک رشته خالی بازمی‌گرداند، آن را به گونه‌ای طراحی می‌کنیم که پست‌های پیش‌نویس اصلاً متد content نداشته باشند. به این ترتیب، اگر بخواهیم محتوای یک پست پیش‌نویس را دریافت کنیم، خطای کامپایلر دریافت خواهیم کرد که به ما می‌گوید این متد وجود ندارد. در نتیجه، نمایش محتوای پست‌های پیش‌نویس در محیط تولید به‌طور تصادفی غیرممکن می‌شود، زیرا آن کد حتی کامپایل نخواهد شد. لیستینگ 18-19 تعریف یک ساختار Post و یک ساختار DraftPost و همچنین متدهایی روی هرکدام را نشان می‌دهد:

Filename: src/lib.rs
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);
    }
}
Listing 18-19: یک Post با یک متد content و یک DraftPost بدون متد content

هر دو ساختار Post و DraftPost دارای یک فیلد خصوصی به نام content هستند که متن پست وبلاگ را ذخیره می‌کند. این ساختارها دیگر فیلد state ندارند زیرا کدگذاری وضعیت را به انواع ساختارها منتقل کرده‌ایم. ساختار Post نماینده یک پست منتشرشده است و دارای متد content است که مقدار content را بازمی‌گرداند.

ما همچنان یک تابع Post::new داریم، اما به‌جای بازگرداندن نمونه‌ای از Post، یک نمونه از DraftPost بازمی‌گرداند. از آنجا که content خصوصی است و هیچ تابعی وجود ندارد که Post را بازگرداند، در حال حاضر امکان ایجاد نمونه‌ای از Post وجود ندارد.

ساختار DraftPost یک متد add_text دارد، بنابراین می‌توانیم همانند قبل متن را به content اضافه کنیم، اما توجه کنید که DraftPost متد content تعریف‌شده ندارد! بنابراین اکنون برنامه تضمین می‌کند که تمام پست‌ها به‌صورت پست‌های پیش‌نویس شروع می‌شوند و پست‌های پیش‌نویس محتوای خود را برای نمایش در دسترس ندارند. هر تلاشی برای دور زدن این محدودیت‌ها منجر به خطای کامپایلر خواهد شد.

پیاده‌سازی انتقال‌ها به‌عنوان تبدیل به انواع مختلف

چگونه می‌توانیم یک پست منتشرشده داشته باشیم؟ ما می‌خواهیم قانون را اجرا کنیم که یک پست پیش‌نویس باید بررسی و تأیید شود قبل از اینکه بتواند منتشر شود. یک پست در حالت “در انتظار بررسی” همچنان نباید هیچ محتوایی نمایش دهد. بیایید این محدودیت‌ها را با اضافه کردن یک ساختار دیگر به نام PendingReviewPost، تعریف متد request_review روی DraftPost برای بازگرداندن یک PendingReviewPost و تعریف یک متد approve روی PendingReviewPost برای بازگرداندن یک Post، همان‌طور که در لیستینگ 18-20 نشان داده شده است، پیاده‌سازی کنیم:

Filename: src/lib.rs
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 {
    // --snip--
    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,
        }
    }
}
Listing 18-20: یک PendingReviewPost که با فراخوانی request_review روی DraftPost ایجاد می‌شود و یک متد approve که یک PendingReviewPost را به یک Post منتشرشده تبدیل می‌کند

متدهای request_review و approve مالکیت self را می‌گیرند، بنابراین نمونه‌های DraftPost و PendingReviewPost را مصرف کرده و آن‌ها را به‌ترتیب به یک PendingReviewPost و یک Post منتشرشده تبدیل می‌کنند. به این ترتیب، پس از فراخوانی request_review روی یک DraftPost و به همین ترتیب، هیچ نمونه‌ای از DraftPost باقی نمی‌ماند. ساختار PendingReviewPost متد content تعریف‌شده‌ای ندارد، بنابراین تلاش برای خواندن محتوای آن منجر به خطای کامپایلر می‌شود، همان‌طور که در مورد DraftPost اتفاق می‌افتد. چون تنها راه برای گرفتن یک نمونه از Post منتشرشده که متد content تعریف‌شده‌ای دارد، فراخوانی متد approve روی یک PendingReviewPost است، و تنها راه برای گرفتن یک PendingReviewPost فراخوانی متد request_review روی یک DraftPost است، ما اکنون فرآیند کاری پست وبلاگ را به سیستم نوع کدگذاری کرده‌ایم.

اما همچنین باید تغییرات کوچکی در main ایجاد کنیم. متدهای request_review و approve نمونه‌های جدیدی بازمی‌گردانند به‌جای اینکه ساختاری که روی آن فراخوانی شده‌اند را تغییر دهند، بنابراین باید تخصیص‌های مجدد با let post = اضافه کنیم تا نمونه‌های بازگشتی را ذخیره کنیم. همچنین نمی‌توانیم تأییدیه‌های مربوط به خالی بودن محتوای پست‌های پیش‌نویس و در انتظار بررسی را داشته باشیم، و نیازی به آن‌ها نیست: دیگر نمی‌توانیم کدی که سعی می‌کند محتوای پست‌های در این حالت‌ها را استفاده کند، کامپایل کنیم. کد به‌روزشده در main در لیستینگ 18-21 نشان داده شده است:

Filename: src/main.rs
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());
}
Listing 18-21: تغییرات در main برای استفاده از پیاده‌سازی جدید فرآیند کاری پست وبلاگ

تغییراتی که باید در main برای تخصیص مجدد post انجام می‌دادیم، به این معناست که این پیاده‌سازی دیگر کاملاً از الگوی وضعیت شی‌گرا پیروی نمی‌کند: انتقالات بین حالت‌ها دیگر به‌طور کامل در پیاده‌سازی Post کپسوله نشده‌اند. اما، مزیت ما این است که اکنون وضعیت‌های نامعتبر به دلیل سیستم نوع و بررسی نوعی که در زمان کامپایل انجام می‌شود، غیرممکن هستند! این تضمین می‌کند که برخی از باگ‌ها، مانند نمایش محتوای یک پست منتشرنشده، قبل از رسیدن به تولید کشف شوند.

تکالیف پیشنهادی در ابتدای این بخش را روی crate blog همان‌طور که پس از لیستینگ 18-21 است امتحان کنید تا ببینید درباره طراحی این نسخه از کد چه نظری دارید. توجه داشته باشید که برخی از تکالیف ممکن است در این طراحی از پیش انجام شده باشند.

دیدیم که حتی با وجود اینکه Rust قادر به پیاده‌سازی الگوهای طراحی شی‌گرا است، الگوهای دیگر، مانند کدگذاری حالت در سیستم نوع، نیز در Rust در دسترس هستند. این الگوها مزایا و معایب متفاوتی دارند. اگرچه ممکن است با الگوهای شی‌گرا بسیار آشنا باشید، بازاندیشی مسئله برای بهره‌بردن از ویژگی‌های Rust می‌تواند مزایایی مانند جلوگیری از برخی باگ‌ها در زمان کامپایل را فراهم کند. الگوهای شی‌گرا همیشه بهترین راه‌حل در Rust نخواهند بود، به دلیل ویژگی‌هایی مانند مالکیت که زبان‌های شی‌گرا ندارند.

خلاصه

فارغ از اینکه پس از خواندن این فصل فکر می‌کنید Rust یک زبان شی‌گرا است یا نه، اکنون می‌دانید که می‌توانید از اشیای صفت برای دریافت برخی ویژگی‌های شی‌گرایی در Rust استفاده کنید. تخصیص پویا (Dynamic Dispatch) می‌تواند انعطاف‌پذیری به کد شما بدهد، اما در ازای آن کمی از عملکرد زمان اجرا را قربانی می‌کند. می‌توانید از این انعطاف‌پذیری برای پیاده‌سازی الگوهای شی‌گرا که می‌توانند به نگه‌داری کد شما کمک کنند، استفاده کنید. Rust همچنین دارای ویژگی‌های دیگری مانند مالکیت است که زبان‌های شی‌گرا ندارند. یک الگوی شی‌گرا همیشه بهترین راه برای بهره‌بردن از نقاط قوت Rust نخواهد بود، اما به‌عنوان یک گزینه در دسترس است.

در ادامه، به بررسی الگوها خواهیم پرداخت که یکی دیگر از ویژگی‌های Rust است که انعطاف‌پذیری زیادی را فراهم می‌کنند. در طول کتاب به‌طور مختصر به آن‌ها اشاره کرده‌ایم، اما هنوز به‌طور کامل توانایی آن‌ها را ندیده‌ایم. برویم!