در دنیای برنامهنویسی، همه ما با باگهایی دست و پنجه نرم کردهایم که ناشی از «وضعیت اشتباه» (Invalid State) هستند. مثلاً تلاش برای ارسال داده از طریق یک اتصال (Connection) که هنوز باز نشده، یا فراخوانی متد publish() روی مقالهای که هنوز محتوایی ندارد. در اکثر زبانها، ما این مشکل را با استفاده از شرطها (if/else) یا استثناها (Exceptions) در زمان اجرا (Runtime) مدیریت میکنیم.
اما در زبان Rust، ما ابزاری قدرتمندتر داریم: سیستم تایپ. الگوی Typestate به ما اجازه میدهد تا قوانین منطقی برنامهمان را مستقیماً در سیستم تایپ کدگذاری کنیم. به زبان ساده، با این الگو کاری میکنیم که کدهای غیرمنطقی اصلاً کامپایل نشوند.
مشکل: مدیریت وضعیت با Enumها
روش سنتی برای مدیریت وضعیت، استفاده از یک فیلد ساده یا یک Enum است. مثال زیر را در نظر بگیرید:
enum State { Draft, Review, Published }
struct Post {
content: String,
state: State,
}
impl Post {
fn publish(&mut self) {
if let State::Review = self.state {
self.state = State::Published;
println!("Post published!");
} else {
panic!("Cannot publish a post that isn't in review!");
}
}
}در این کد، اگر برنامهنویس دیگری متد publish را روی یک پست در وضعیت Draft صدا بزند، برنامه در زمان اجرا کرش میکند. اینجاست که Typestate وارد عمل میشود تا این خطا را به مرحله کامپایل منتقل کند.
الگوی Typestate چیست؟
الگوی Typestate یعنی استفاده از انواع دادهای (Types) متفاوت برای نمایش وضعیتهای مختلف یک شیء. به جای اینکه یک ساختار ثابت داشته باشیم که یک فیلد state داخلی دارد، ما برای هر وضعیت یک ساختار مجزا تعریف میکنیم.
این الگو در Rust به دلیل دو ویژگی کلیدی بسیار قدرتمند عمل میکند:
Ownership (مالکیت): وقتی یک متد شیء را مصرف میکند (Move)، آن وضعیت دیگر در دسترس نیست.
Generics: اجازه میدهد کد تمیز و بازیافتپذیر بنویسیم.
پیادهسازی گامبهگام
بیایید مثال مدیریت پست وبلاگ را با Typestate بازنویسی کنیم.
گام اول: تعریف وضعیتها
ابتدا برای هر وضعیت یک Struct خالی تعریف میکنیم. اینها فقط نقش «نشانگر» (Marker) را دارند.
struct Draft;
struct Boxed;
struct Published;گام دوم: ساختار Generic
حالا ساختار اصلی را طوری تعریف میکنیم که یک پارامتر نوع (Generic) بپذیرد:
struct Post<S> {
content: String,
_state: std::marker::PhantomData<S>,
}(نکته: PhantomData به کامپایلر میگوید که ما از S استفاده منطقی میکنیم، بدون اینکه فضایی در حافظه اشغال شود).
گام سوم: پیادهسازی متدهای اختصاصی هر وضعیت
حالا جادوی اصلی اتفاق میافتد. ما متدهایی مینویسیم که فقط برای وضعیتهای خاصی در دسترس هستند:
impl Post<Draft> {
pub fn new(content: String) -> Post<Draft> {
Post {
content,
_state: std::marker::PhantomData,
}
}
pub fn request_review(self) -> Post<Boxed> {
Post {
content: self.content,
_state: std::marker::PhantomData,
}
}
}
impl Post<Boxed> {
pub fn publish(self) -> Post<Published> {
println!("Post is now live!");
Post {
content: self.content,
_state: std::marker::PhantomData,
}
}
}چرا این روش انقلابی است؟
۱. حذف خطاهای زمان اجرا:
در کد بالا، متد publish فقط در impl Post<Boxed> تعریف شده است. اگر سعی کنید روی یک Post<Draft> متد publish را صدا بزنید، با این خطای کامپایلر مواجه میشوید:
no method named 'publish' found for struct 'Post<Draft>'
۲. تغییر وضعیت تخریبگر (Destructive State Transitions):
دقت کنید که متدها self را میگیرند (بدون &). این یعنی وقتی request_review صدا زده میشود، شیء قبلی (که در وضعیت Draft بود) Move میشود و از بین میرود. شما دیگر نمیتوانید تصادفاً از شیء قدیمی استفاده کنید.
۳. هزینه صفر (Zero-Cost Abstraction):
تمام این بررسیها در زمان کامپایل انجام میشود. در زمان اجرا، هیچ اثری از این ساختارهای اضافه نیست و برنامه دقیقاً با همان سرعتی اجرا میشود که انگار از هیچ الگویی استفاده نکردهاید.
چه زمانی از Typestate استفاده کنیم؟
این الگو برای موارد زیر فوقالعاده است:
پروتکلهای شبکه: (مثلاً ابتدا Connect سپس Authenticate و بعد SendData).
ساختن اشیاء پیچیده (Builder Pattern): وقتی که باید حتماً فیلدهای X و Y مقداردهی شوند تا بتوان متد build را صدا زد.
درایورهای سختافزار: مثلاً یک پین GPIO که باید ابتدا به عنوان Output تنظیم شود تا بتوان روی آن Write کرد.
چالشها و محدودیتها
با وجود قدرت بالا، Typestate چالشهایی هم دارد:
تکرار کد (Boilerplate): ممکن است مجبور شوید برای هر انتقال وضعیت، یک متد جدید بنویسید و دادهها را از ساختار قدیم به جدید کپی کنید.
پیچیدگی در مجموعه (Collections): ذخیره کردن لیستی از پستها با وضعیتهای مختلف در یک Vec سخت میشود (چون تایپهای متفاوتی دارند)، مگر اینکه از Enum یا Trait Object استفاده کنید که بخشی از امنیت زمان کامپایل را فدا میکند.
نتیجهگیری
الگوی Typestate یکی از زیباترین جلوههای فلسفه "Correctness by Construction" در زبان Rust است. این الگو به ما یاد میدهد که به جای نوشتن تستهای واحد (Unit Tests) بیشمار برای چک کردن وضعیتهای غیرمجاز، میتوانیم معماری برنامه را طوری طراحی کنیم که ایجاد وضعیت غیرمجاز اصلاً غیرممکن باشد.
هیچ امتیازی برای نمایش وجود ندارد.