پیادهسازی یک الگوی طراحی شیگرا
الگوی وضعیت یک الگوی طراحی شیگرا است. هسته این الگو این است که مجموعهای از وضعیتها را که یک مقدار میتواند بهطور داخلی داشته باشد، تعریف کنیم. این وضعیتها با مجموعهای از اشیای وضعیت نمایش داده میشوند و رفتار مقدار بر اساس وضعیت آن تغییر میکند. قصد داریم مثالی از یک ساختار blog post (پست وبلاگ) را بررسی کنیم که یک فیلد برای نگهداشتن وضعیت دارد. این وضعیت یک شیء از مجموعه “پیشنویس” (draft)، “در حال بررسی” (review)، یا “منتشرشده” (published) خواهد بود.
اشیای وضعیت قابلیتهایی را به اشتراک میگذارند: در Rust، البته، ما از ساختارها (structs) و صفتها (traits) به جای اشیا و ارثبری استفاده میکنیم. هر شیء وضعیت مسئول رفتار خود و مدیریت زمانی است که باید به وضعیت دیگری تغییر کند. مقداری که یک شیء وضعیت را نگه میدارد، هیچ اطلاعی از رفتارهای مختلف وضعیتها یا زمان تغییر وضعیت ندارد.
مزیت استفاده از الگوی وضعیت این است که وقتی نیازهای تجاری برنامه تغییر میکنند، نیازی به تغییر کد مقداری که وضعیت را نگه میدارد یا کدی که از آن مقدار استفاده میکند، نداریم. تنها لازم است کد داخل یکی از اشیای وضعیت را برای تغییر قوانین آن یا شاید اضافه کردن اشیای وضعیت جدید بهروزرسانی کنیم.
ابتدا الگوی وضعیت را به روش سنتی شیگرایی پیادهسازی میکنیم، سپس از رویکردی که در Rust طبیعیتر است استفاده خواهیم کرد. بیایید بهصورت مرحلهبهمرحله پیادهسازی یک فرآیند کاری پست وبلاگ با استفاده از الگوی وضعیت را بررسی کنیم.
قابلیت نهایی به این شکل خواهد بود:
- یک پست وبلاگ بهصورت یک پیشنویس خالی شروع میشود.
- وقتی پیشنویس تمام شد، بررسی پست درخواست میشود.
- وقتی پست تأیید شد، منتشر میشود.
- تنها پستهای وبلاگی که منتشر شدهاند متن را برای چاپ بازمیگردانند، بنابراین پستهای تأییدنشده نمیتوانند بهطور تصادفی منتشر شوند.
هر تغییر دیگری که روی یک پست تلاش شود نباید تأثیری داشته باشد. برای مثال، اگر بخواهیم یک پست وبلاگ پیشنویس را قبل از درخواست بررسی تأیید کنیم، پست باید بهعنوان پیشنویس منتشرنشده باقی بماند.
لیستینگ 18-11 این فرآیند کاری را بهصورت کدی نشان میدهد: این یک نمونه از استفاده از API است که قصد داریم در یک
crate کتابخانهای به نام blog
پیادهسازی کنیم. این کد هنوز کامپایل نخواهد شد زیرا هنوز crate blog
را پیادهسازی
نکردهایم.
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());
}
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>
ضروری است.
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 {}
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 اضافه کنیم:
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 {}
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 این پیادهسازی موقت را نشان میدهد:
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 {}
content
در Post
که همیشه یک برش رشته خالی بازمیگرداندبا افزودن این متد content
، تمام موارد تا خط 7 لیستینگ 18-11 به درستی کار میکنند.
درخواست بررسی پست و تغییر وضعیت آن
در مرحله بعد، باید قابلیت درخواست بررسی پست را اضافه کنیم، که باید وضعیت آن را از Draft
به PendingReview
تغییر دهد. لیستینگ 18-15 این کد را نشان میدهد:
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
}
}
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 نشان داده شده است:
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
}
}
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 نشان داده شده است:
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
}
}
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 نشان داده شده است:
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
}
}
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 را در نظر بگیرید:
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
و همچنین متدهایی روی هرکدام را نشان میدهد:
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);
}
}
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 نشان داده شده است، پیادهسازی کنیم:
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,
}
}
}
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 نشان داده شده است:
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());
}
main
برای استفاده از پیادهسازی جدید فرآیند کاری پست وبلاگتغییراتی که باید در main
برای تخصیص مجدد post
انجام میدادیم، به این معناست که این پیادهسازی دیگر کاملاً از
الگوی وضعیت شیگرا پیروی نمیکند: انتقالات بین حالتها دیگر بهطور کامل در پیادهسازی Post
کپسوله نشدهاند.
اما، مزیت ما این است که اکنون وضعیتهای نامعتبر به دلیل سیستم نوع و بررسی نوعی که در زمان کامپایل انجام میشود،
غیرممکن هستند! این تضمین میکند که برخی از باگها، مانند نمایش محتوای یک پست منتشرنشده، قبل از رسیدن به تولید
کشف شوند.
تکالیف پیشنهادی در ابتدای این بخش را روی crate blog
همانطور که پس از لیستینگ 18-21 است امتحان کنید تا ببینید
درباره طراحی این نسخه از کد چه نظری دارید. توجه داشته باشید که برخی از تکالیف ممکن است در این طراحی از پیش
انجام شده باشند.
دیدیم که حتی با وجود اینکه Rust قادر به پیادهسازی الگوهای طراحی شیگرا است، الگوهای دیگر، مانند کدگذاری حالت در سیستم نوع، نیز در Rust در دسترس هستند. این الگوها مزایا و معایب متفاوتی دارند. اگرچه ممکن است با الگوهای شیگرا بسیار آشنا باشید، بازاندیشی مسئله برای بهرهبردن از ویژگیهای Rust میتواند مزایایی مانند جلوگیری از برخی باگها در زمان کامپایل را فراهم کند. الگوهای شیگرا همیشه بهترین راهحل در Rust نخواهند بود، به دلیل ویژگیهایی مانند مالکیت که زبانهای شیگرا ندارند.
خلاصه
فارغ از اینکه پس از خواندن این فصل فکر میکنید Rust یک زبان شیگرا است یا نه، اکنون میدانید که میتوانید از اشیای صفت برای دریافت برخی ویژگیهای شیگرایی در Rust استفاده کنید. تخصیص پویا (Dynamic Dispatch) میتواند انعطافپذیری به کد شما بدهد، اما در ازای آن کمی از عملکرد زمان اجرا را قربانی میکند. میتوانید از این انعطافپذیری برای پیادهسازی الگوهای شیگرا که میتوانند به نگهداری کد شما کمک کنند، استفاده کنید. Rust همچنین دارای ویژگیهای دیگری مانند مالکیت است که زبانهای شیگرا ندارند. یک الگوی شیگرا همیشه بهترین راه برای بهرهبردن از نقاط قوت Rust نخواهد بود، اما بهعنوان یک گزینه در دسترس است.
در ادامه، به بررسی الگوها خواهیم پرداخت که یکی دیگر از ویژگیهای Rust است که انعطافپذیری زیادی را فراهم میکنند. در طول کتاب بهطور مختصر به آنها اشاره کردهایم، اما هنوز بهطور کامل توانایی آنها را ندیدهایم. برویم!