نمط الحالة state pattern هو نمط تصميم كائني التوجه Object-Oriented، والمغزى منه هو أننا نعرّف مجموعةً من الحالات التي يمكن للقيمة أن تمتلكها داخليًا، وتُمثَّل الحالات من خلال مجموعة من كائنات الحالة state objects، ويتغير سلوك القيمة بناءً على حالتها. سنعمل من خلال مثال لهيكل منشور مدونة يحتوي على حقل للاحتفاظ بحالته، والتي ستكون كائن حالة من مجموعة القيم "مسودة draft" أو " قيد المراجعة review" أو "منشور published".
تتشارك كائنات الحالة بالوظيفة، ونستخدم الهياكل structs والسمات traits بدلًا من الكائنات objects والوراثة inheritance في لغة رست. كل كائن حالة مسؤول عن سلوكه الخاص وعن تحديد متى يجب عليه أن يتغير من حالة إلى أخرى. لا تعرف القيمة التي تخزّن كائن الحالة شيئًا عن السلوك المختلف للحالات أو متى تنتقل بينها.
ميزة استخدام نمط الحالة هي أنه لن نحتاج إلى تغيير شيفرة القيمة البرمجية التي تحتفظ بالحالة أو الشيفرة البرمجية التي تستخدم القيمة عندما تتغير متطلبات العمل للبرنامج، إذ سنحتاج فقط إلى تعديل الشيفرة البرمجية داخل أحد كائنات الحالة لتغيير قواعدها أو ربما إضافة المزيد من كائنات الحالة.
سننّفذ بدايةً نمط الحالة بطريقة تقليديّة كائنية التوجه، ثم سنستعمل نهجًا أكثر شيوعًا في رست. لننفّذ تدريجيًا طريقةً لتنظيم سير عمل منشور المدونة باستعمال نمط الحالة.
ستكون النتيجة النهائية كما يلي:
- يبدأ منشور المدونة مثل مسودة فارغة.
- يُطلب مراجعة المنشور عند الانتهاء من المسودة.
- يُنشر المنشور عندما يُوافَق عليه.
- منشورات المدونة التي هي بحالة "منشور published" هي المنشورات الوحيدة التي تعيد محتوًى ليُطبع، بحيث لا يمكن نشر المنشورات غير الموافق عليها عن طريق الخطأ.
يجب ألا يكون لأي تعديلات أخرى أُجريت على إحدى المنشورات أي تأثير، فعلى سبيل المثال إذا حاولنا الموافقة على مسودة منشور مدونة قبل أن نطلب المراجعة، فيجب أن يبقى المنشور مسودةً غير منشورة.
تظهر الشيفرة 11 سير العمل هذا على هيئة شيفرة برمجية، وهذا مثال عن استعمال الواجهة البرمجية API التي سننفّذها في وحدة مكتبة مصرّفة library crate تسمى blog
. لن تُصرَّف الشيفرة البرمجية التالية بنجاح، لأننا لم ننفّذ الوحدة المصرفة blog
بعد.
اسم الملف: 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()); }
[الشيفرة 11: الشيفرة التي تُظهر السلوك المرغوب الذي نريد لوحدتنا المصرفة blog
أن تحتويه]
نريد السماح للمستخدم بإنشاء مسودة منشور مدونة جديد باستعمال Post::new
، كما نريد السماح بإضافة نص إلى منشور المدونة، إذ يجب ألّا نحصل على أي نص إذا حاولنا الحصول على محتوى المنشور فورًا قبل الموافقة وذلك لأن المنشور لا يزال في وضع المسودة. أضفنا assert_eq!
في الشيفرة البرمجية لأغراض توضيحية. قد يكون اختبار الوحدة unit test المناسب لهذا هو التأكيد على أن مسودة منشور مدونة تعرض سلسلةً نصيةً string فارغة من تابع content
لكننا لن نكتب أي اختبارات لهذا المثال حاليًا.
نريد بعد ذلك تمكين طلب مراجعة المنشور ونريد أن يُعيد content
سلسلةً نصيةً فارغة أثناء انتظار المراجعة، ويُنشر المنشور عندما يتلقى الموافقة، مما يعني أنه سيُعاد نص المنشور عندما نستدعي content
.
لاحظ أن النوع الوحيد الذي نتفاعل معه من الوحدة المصرّفة هو النوع Post
، إذ سيستعمل هذا النوع نمط الحالة وسيحتوي على قيمة واحدة من ثلاث قيم هي كائنات حالة تمثل الحالات المختلفة التي يمكن أن يكون المنشور فيها، ألا وهي مسودة، أو انتظار المراجعة، أو النشر. يجري التحكم بالتغيير من حالة إلى أخرى داخليًا ضمن نوع Post
، إذ تتغير الحالات استجابةً للتوابع التي يستدعيها مستخدمو مكتبتنا في نسخة Post
، لكن لا يتعين عليهم إدارة تغييرات الحالة مباشرةً، كما لا يمكن للمستخدمين ارتكاب خطأ في الحالات مثل نشر منشور قبل مراجعته.
تعريف المنشور وإنشاء نسخة جديدة في حالة المسودة
لنبدأ بتنفيذ المكتبة؛ نعلم أننا بحاجة إلى هيكل عام public struct يدعى Post
ليخزّن محتوى المنشور، لذا سنبدأ بتعريف الهيكل ودالة عامة مرتبطة associated تدعى new
لإنشاء نسخة من Post
كما هو موضح في الشيفرة 12. سننشئ أيضًا سمةً خاصة تدعى State
تعرّف السلوك الذي يجب أن تتمتع به جميع كائنات حالة Post
.
سيخزّن هيكل Post
بعد ذلك كائن السمة <Box<dyn State
داخل <Option<T
في حقل خاص يسمى state
ليخزّن كائن الحالة، وسترى سبب ضرورة <Option<T
بعد قليل.
اسم الملف: 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 {}
[الشيفرة 12: تعريف هيكل Post
ودالة new
التي تنشئ نسخة من Post
وسمة State
وهيكل Draft
جدد]
تعرّف السمة State
السلوك المشترك بين حالات النشر المختلفة. كائنات الحالة هي Draft
و PendingReview
و Published
وسوف تنفِّذ جميعها سمة State
. لا تحتوي السمة في الوقت الحالي على أي توابع وسنبدأ بتعريف حالة Draft
فقط لأن هذه هي الحالة التي نريد أن يبدأ المنشور فيها.
عندما ننشئ Post
جديد نعيّن حقل state
الخاص به بقيمة Some
التي تحتوي على Box
، وتشير Box
هنا إلى نسخة جديدة للهيكل Draft
، أي أنه عندما ننشئ نسخةً جديدة من Post
فإنها ستبدأ مثل مسودة. نظرًا لأن حقل state
للهيكل Post
هو خاص ولا توجد طريقةٌ لإنشاء Post
في أي حالة أخرى. عيّننا سلسلة نصية String
فارغة جديدة للحقل content
في الدالة Post::new
.
تخزين نص محتوى المنشور
كنا في الشيفرة 11 قادرين على استدعاء تابع يسمى add_text
وتمريرstr&
إليه ليُضاف لاحقًا على أنه محتوى نص منشور المدونة. ننفّذ هذا مثل تابع بدلًا من عرض حقل content
على أنه pub
حتى نتمكن لاحقًا من تنفيذ تابع يتحكم بكيفية قراءة بيانات حقل content
. تابع add_text
واضح جدًا، لذا سنضيف التنفيذ في الشيفرة 13 إلى كتلة impl Post
.
اسم الملف: src/lib.rs
impl Post { // --snip-- pub fn add_text(&mut self, text: &str) { self.content.push_str(text); } }
[الشيفرة 13: تنفيذ تابع add_text
لإضافة نص لمنشور content
]
يأخذ تابع add_text
مرجعًا متغيّرًا mutable إلى self
لأننا نعدّل نسخة Post
التي نستدعي add_text
عليها، ثم نستدعي push_str
على String
في content
ونمرّر الوسيط text
لإضافتها إلى content
المحفوظ. لا يعتمد هذا السلوك على حالة المنشور لذا فهو ليس جزءًا من نمط الحالة. لا يتفاعل تابع add_text
مع حقل state
إطلاقًا، لكنه جزءٌ من السلوك الذي نريد دعمه.
ضمان أن محتوى مسودة المنشور فارغ
ما زلنا بحاجة تابع content
حتى بعد استدعائنا add_text
وإضافة بعض المحتوى إلى منشورنا وذلك لإعادة شريحة سلسلة نصية string slice فارغة لأن المنشور لا يزال في حالة المسودة كما هو موضح في السطر 7 من الشيفرة 11. لننفّذ حاليًا تابع content
بأبسط شيء يستوفي هذا المتطلب؛ وهو إعادة شريحة سلسلة نصية فارغة دائمًا، إلا أننا سنغير هذا لاحقًا بمجرد تقديم القدرة على تغيير حالة المنشور حتى يمكن نشره. حتى الآن يمكن أن تكون المنشورات في حالة المسودة فقط لذلك يجب أن يكون محتوى المنشور فارغًا دائمًا. تُظهر الشيفرة 14 تنفيذ الموضع المؤقت هذا.
اسم الملف: src/lib.rs
impl Post { // --snip-- pub fn content(&self) -> &str { "" } }
[الشيفرة 14: إضافة تنفيذ موضع مؤقت للتابع content
على Post
يُعيد دائمًا شريحة سلسلة فارغة]
يعمل الآن كل شيء كما خُطّط له مع إضافة تابع content
في الشيفرة 11 حتى السطر 7.
طلب مراجعة للمنشور يغير حالته
نحتاج بعد ذلك إلى إضافة إمكانية طلب مراجعة منشور وتغيير حالته من Draft
إلى PendingReview
، وتوضّح الشيفرة 15 هذا الأمر.
اسم الملف: src/lib.rs
impl Post { // --snip-- 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 } }
[الشيفرة 15: تنفيذ توابع request_review
على Post
وسمة State
]
نعطي Post
تابعًا عام يدعى request_review
، والذي سيأخذ مرجعًا متغيّرًا يشير إلى self
، نستدعي بعد ذلك التابع request_review
الداخلي على الحالة Post
الحالية، إذ يستخدم request_review
الثاني الحالة الحالية ويعيد حالةً جديدة.
نضيف التابع request_review
إلى السمة State
؛ إذ ستحتاج جميع الأنواع التي تطبق السمة الآن إلى تنفيذ تابع request_review
. لاحظ أن لدينا <self: Box<Self
بدلًا من self
، أو self&
، أو mut self&
بمثابة معامل أول للتابع. تعني هذه الصيغة أن التابع صالحٌ فقط عندما يُستدعى على Box
يحتوي النوع. تأخذ هذه الطريقة بالصياغة ملكية <Box<Self
مما يبطل الحالة القديمة بحيث يمكن أن تتحول قيمة حالة Post
إلى حالة جديدة.
يجب أن يأخذ تابع request_review
ملكية قيمة الحالة القديمة لاستخدامها، وهذه هي فائدة استخدام Option
في حقل state
الخاص بالمنشور Post
؛ إذ نستدعي تابع take
لأخذ قيمة Some
من حقل state
وترك None
في مكانها لأن رست لا تسمح لنا بامتلاك حقول غير مأهولة في الهياكل. يتيح لنا ذلك نقل قيمة 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
كما هو ونعيد شريحة سلسلة نصية string slice فارغة. يمكننا الآن الحصول على Post
في حالة PendingReview
وكذلك في حالة Draft
إلا أننا نريد السلوك ذاته في حالة PendingReview
. تعمل الشيفرة 11 الآن بنجاح وصولًا للسطر 10.
إضافة approve لتغيير سلوك التابع content
سيكون تابع approve
مشابهًا لتابع request_review
، إذ سيضبط قيمة state
على القيمة التي تقول الحالة الحالية أنها يجب أن تكون عليها عند الموافقة على تلك الحالة كما هو موضح في الشيفرة 16.
اسم الملف: src/lib.rs
impl Post { // --snip-- 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 approve(self: Box<Self>) -> Box<dyn State> { self } } struct PendingReview {} impl State for PendingReview { // --snip-- 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 } }
[الشيفرة 16: تنفيذ تابع approve
على Post
وسمة State
]
نضيف تابع approve
إلى سمة State
ونضيف هيكلًا جديدًا ينفّذ السمة State
، والحالة Published
.
لن يكون للتابع approve
على Draft
عند استدعائه أي تأثير على غرار الطريقة التي يعمل بها request_review
في PendingReview
، وذلك لأن التابع approve
سيعيد self
، بينما يعيد التابع approve
عندما نستدعيه على PendingReview
نسخةً جديدةً ضمن صندوق boxed لهيكل Published
. ينفّذ هيكل Published
السمة State
وتعيد نفسها من أجل كل من تابع request_review
وتابع approve
لأن المنشور يجب أن يبقى في حالة Published
في تلك الحالات.
نحتاج الآن إلى تحديث تابع content
على Post
، إذ نريد أن تعتمد القيمة المُعادة من content
على حالة Post
الحالية، لذلك نعرّف Post
مفوض للتابع content
المعرّف على قيمة الحقل state
الخاص به كما هو موضح في الشيفرة 17.
اسم الملف: src/lib.rs
impl Post { // --snip-- pub fn content(&self) -> &str { self.state.as_ref().unwrap().content(self) } // --snip-- }
[الشيفرة 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
الذي نعلم أنه لن يهلع أبدًا لأننا نعلم أن التوابع الموجودة على Post
تضمن أن state
سيحتوي دائمًا على القيمة Some
عند الانتهاء من هذه التوابع، وهذه إحدى الحالات التي تحدثنا عنها سابقًا في قسم "الحالات التي تعرف فيها معلومات أكثر من المصرف" من الفصل الاختيار ما بين الماكرو panic! والنوع Result للتعامل مع الأخطاء في لغة Rust، وهي عندما نعلم أن قيمة None
غير ممكنة أبدًا على الرغم من أن المصرف غير قادر على فهم ذلك.
سيكون تأثير التحصيل القسري deref coercion في هذه المرحلة ساريًا على كل من &
و Box
عندما نستدعي content
على <Box<dyn State&
، لذلك يُستدعى تابع content
في النهاية على النوع الذي ينفذ سمة State
. هذا يعني أننا بحاجة إلى إضافة content
إلى تعريف سمة State
وهنا سنضع منطق المحتوى الذي سيُعاد اعتمادًا على الحالة التي لدينا كما هو موضح في الشيفرة 18.
اسم الملف: src/lib.rs
trait State { // --snip-- fn content<'a>(&self, post: &'a Post) -> &'a str { "" } } // --snip-- struct Published {} impl State for Published { // --snip-- fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content } }
[الشيفرة 18: إضافة تابع content
إلى سمة State
]
نضيف تنفيذًا مبدئيًا للتابع content
الذي يُعيد شريحة سلسلة نصية فارغة، ويعني هذا أننا لسنا بحاجة إلى تنفيذ content
في هيكلي Draft
و PendingReview
، إذ سيُعيد هيكل Published
تعريف التابع content
ويعيد القيمة في post.content
.
لاحظ أننا نحتاج إلى توصيف لدورة الحياة lifetime على هذا التابع كما ناقشنا سابقًا في الفصل مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة Rust، إذ نأخذ هنا مرجعًا إلى post
مثل وسيط ونعيد مرجعًا إلى جزء من post
وبالتالي ترتبط دورة حياة المرجع المُعاد بدورة حياة وسيط post
.
تعمل الشيفرة 11 الآن كاملةً بعد أن طبّقنا نمط الحالة state pattern مع قواعد سير عمل منشور المدونة. المنطق المتعلق بالقواعد موجود في كائنات الحالة بدلًا من بعثرته في جميع أنحاء Post
.
لماذا لم نستخدم تعدادًا؟
لربما كنت تتساءل عن سبب عدم استخدامنا enum
مع حالات المنشورات المختلفة الممكنة مثل متغايرات variants؛ هذا بالتأكيد حل ممكن، جرّبه وقارن النتائج النهائية لترى أيهما تفضل. أحد عيوب استعمال التعداد هو أن كل مكان يتحقق من قيمة التعداد سيحتاج إلى تعبير match
أو ما شابه للتعامل مع كل متغاير ممكن، ويمكن أن يتطلب هذا الحل شيفرةً برمجيةً مكررة أكثر مقارنةً مع حل كائن السمة هذا.
سلبيات استخدام نمط الحالة
وضّحنا أن رست قادرة على تنفيذ نمط الحالة كائنية التوجه لتغليف أنواع مختلفة من السلوك التي يجب أن يتمتع بها المنشور في كل حالة. لا تعرف التوابع في 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
، ومع ذلك فإن هذا من شأنه أن ينتهك سلامة الكائن لأن السمة لا تعرف فعلًا ما ستكون عليه self
الحقيقية. نريد أن نكون قادرين على استعمال State
مثل كائن سمة لذلك نحتاج إلى أن تكون توابعها آمنة من الكائنات.
تتضمن التكرارات الأخرى عمليات تنفيذ مماثلة لتوابع request_review
و approve
على Post
، يفوّض كلا التابعين تنفيذ التابع ذاته على القيمة في حقل state
للقيمة Option
وتعيين القيمة الجديدة لحقل state
إلى النتيجة. إذا كان لدينا الكثير من التوابع في Post
التي اتبعت هذا النمط، فقد نفكر في تعريف ماكرو لإزالة التكرار.
لا نستفيد استفادة كاملة من نقاط قوة رست بقدر الإمكان عبر تنفيذ نمط الحالة تمامًا كما هو معرّف في اللغات البرمجية كائنية التوجه الأخرى. دعنا نلقي نظرةً على بعض التغييرات الممكن إجراؤها على الوحدة المصرفة blog
، والتي من شأنها أن تجعل الحالات غير الصالحة والانتقالات transitions أخطاءً تظهر وقت التصريف.
ترميز الحالات والسلوك مثل أنواع
سنوضّح كيفية إعادة التفكير بنمط الحالة للحصول على مجموعة مختلفة من المقايضات، وذلك بدلًا من تغليف الحالات والانتقالات بحيث لا يكون لدى الشيفرة البرمجية الخارجية أي معرفة بها. نرمّز الحالات إلى أنواع مختلفة، وبالتالي سيمنع نظام فحص النوع في رست محاولات استخدام مسودات المنشورات، بحيث لا يُسمح إلا بالمنشورات المنشورة وذلك عن طريق إصدار خطأ في المصرّف.
لننظر إلى الجزء الأول من دالة main
في الشيفرة 11.
اسم الملف: src/main.rs
fn main() { let mut post = Post::new(); post.add_text("I ate a salad for lunch today"); assert_eq!("", post.content()); }
ما زلنا نسمح بإنشاء منشورات جديدة في حالة المسودة باستخدام Post::new
والقدرة على إضافة نص إلى محتوى المنشور، ولكن بدلًا من وجود تابع content
في مسودة المنشور التي تعيد سلسلةً نصيةً فارغة، سنعمل على تعديلها بحيث لا تحتوي مسودة المنشورات على تابع content
إطلاقًا؛ وستحصل بهذه الطريقة على خطأ في المصرف إذا حاولنا الحصول على محتوى مسودة منشور يخبرنا أن التابع غير موجود، ونتيجةً لذلك سيكون من المستحيل بالنسبة لنا عرض محتوى مسودة المنشور عن طريق الخطأ في مرحلة الإنتاج production لأن هذه الشيفرة البرمجية لن تصرف. تُظهر الشيفرة 19 تعريف هيكلي Post
و DraftPost
إضافةً إلى التوابع الخاصة بكل منهما.
اسم الملف: 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); } }
[الشيفرة 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
كما هو موضح في الشيفرة 20.
اسم الملف: src/lib.rs
impl DraftPost { // --snip-- 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, } } }
[الشيفرة 20: هيكل PendingReviewPost
مُنشأ عن طريق استدعاء request_review
على DraftPost
وتابع approve
الذي يرجع PendingReviewPost
إلى Post
منشور]
يأخذ التابعان request_review
و approve
ملكية self
وبالتالي تستخدم نُسَخ DraftPost
و PendingReviewPost
وتحوّلهما إلى PendingReviewPost
و Post
منشور published على التوالي، وبهذه الطريقة لن يكون لدينا أي نسخ متبقية من DraftPost
بعد أن استدعينا request_review
عليها وما إلى ذلك.
لا يحتوي هيكل PendingReviewPost
على تابع content
معرف عليه لذلك تؤدي محاولة قراءة محتواها إلى حدوث خطأ في المصرّف كما هو الحال مع DraftPost
، لأن الطريقة الوحيدة للحصول على نسخة Post
قد جرى نشره وله تابع content
معرّف هي استدعاء تابع approve
على PendingReviewPost
والطريقة الوحيدة للحصول على PendingReviewPost
هي استدعاء تابع request_review
على DraftPost
، إذ رمّزنا الآن سير عمل منشور المدونة إلى نظام النوع.
يتعين علينا أيضًا إجراء بعض التغييرات الصغيرة على main
، إذ يُعيد التابعان request_review
و approve
حاليًا نسخًا جديدة بدلًا من تعديل الهيكل الذي استدعيت عليهما، لذلك نحتاج إلى إضافة المزيد من الإسنادات الخفية let post =
لحفظ الأمثلة المُعادة. لا يمكن أيضًا أن تكون لدينا تأكيدات بسلاسل نصية فارغة حول محتويات المسودة ومنشورات بانتظار المراجعة، فنحن لسنا بحاجتها. لا يمكننا تصريف الشيفرة البرمجية التي تحاول استعمال محتوى المنشورات في تلك الحالات بعد الآن. تظهر الشيفرة البرمجية الجديدة ضمن الدالة main
في الشيفرة 21.
اسم الملف: 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()); }
[الشيفرة 21: تعديل main
لاستعمال التنفيذ الجديد لسير عمل منشور مدونة]
تعني التغييرات التي احتجنا لإجرائها على main
من أجل إعادة تعيين post
أن هذا التنفيذ لم يعد يتبع نمط الحالة كائنية التوجه بعد الآن، إذ لم تعد كامل التحوّلات بين الحالات مغلّفة في تنفيذ Post
، ومع ذلك فإن النقطة التي بصالحنا هنا هي أن الحالات غير الصالحة أصبحت الآن مستحيلة بسبب نظام النوع والتحقق من النوع الذي يحدث في وقت التصريف، إذ يضمن ذلك اكتشاف أخطاء معينة مثل عرض محتوى منشور لم يُنشر قبل الوصول لمرحلة الإنتاج.
جرّب المهام المقترحة في بداية المقالة على وحدة blog
المصرفة كما هي بعد الشيفرة 21 لمعرفة ما هو رأيك في تصميم هذا الإصدار من الشيفرة البرمجية. لاحظ أن بعض المهام قد تكون مكتملة فعلًا في هذا التصميم.
رأينا أنه على الرغم من أن رست قادرة على تنفيذ أنماط تصميم كائنية التوجه، إلا أن أنماطًا أخرى مثل ترميز الحالة في نظام النوع متاحة أيضًا في رست. هذه الأنماط لها مقايضات مختلفة. يمكن أن تكون على دراية كبيرة بالأنماط كائنية التوجه لكن يمكن أن توفّر إعادة التفكير في المشكلة للاستفادة من ميزات رست عدّة فوائد مثل منع بعض الأخطاء في وقت التصريف. لن تكون الأنماط كائنية التوجه هي الحل الأفضل دائمًا في رست نظرًا لوجود ميزات معينة مثل الملكية التي لا تمتلكها اللغات كائنية التوجه.
ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming Features of Rust من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.