اذهب إلى المحتوى

تنفيذ نمط تصميمي Design Pattern كائني التوجه Object-Oriented في لغة رست


Naser Dakhel

نمط الحالة state pattern هو نمط تصميم كائني التوجه Object-Oriented، والمغزى منه هو أننا نعرّف مجموعةً من الحالات التي يمكن للقيمة أن تمتلكها داخليًا، وتُمثَّل الحالات من خلال مجموعة من كائنات الحالة state objects، ويتغير سلوك القيمة بناءً على حالتها. سنعمل من خلال مثال لهيكل منشور مدونة يحتوي على حقل للاحتفاظ بحالته، والتي ستكون كائن حالة من مجموعة القيم "مسودة draft" أو " قيد المراجعة review" أو "منشور published".

تتشارك كائنات الحالة بالوظيفة، ونستخدم الهياكل structs والسمات traits بدلًا من الكائنات objects والوراثة inheritance في لغة رست. كل كائن حالة مسؤول عن سلوكه الخاص وعن تحديد متى يجب عليه أن يتغير من حالة إلى أخرى. لا تعرف القيمة التي تخزّن كائن الحالة شيئًا عن السلوك المختلف للحالات أو متى تنتقل بينها.

ميزة استخدام نمط الحالة هي أنه لن نحتاج إلى تغيير شيفرة القيمة البرمجية التي تحتفظ بالحالة أو الشيفرة البرمجية التي تستخدم القيمة عندما تتغير متطلبات العمل للبرنامج، إذ سنحتاج فقط إلى تعديل الشيفرة البرمجية داخل أحد كائنات الحالة لتغيير قواعدها أو ربما إضافة المزيد من كائنات الحالة.

سننّفذ بدايةً نمط الحالة بطريقة تقليديّة كائنية التوجه، ثم سنستعمل نهجًا أكثر شيوعًا في رست. لننفّذ تدريجيًا طريقةً لتنظيم سير عمل منشور المدونة باستعمال نمط الحالة.

ستكون النتيجة النهائية كما يلي:

  1. يبدأ منشور المدونة مثل مسودة فارغة.
  2. يُطلب مراجعة المنشور عند الانتهاء من المسودة.
  3. يُنشر المنشور عندما يُوافَق عليه.
  4. منشورات المدونة التي هي بحالة "منشور 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...