نمط الحالة (State)

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

نمط الحالة (State) هو نمط تصميم سلوكي يسمح للكائن بتغيير سلوكه حين تتغير حالته الداخلية، ويبدو حينها كأن الكائن قد غيّر فئته.

المشكلة

يرتبط نمط الحالة بشدة بمبدأ Finite-State Machine (الآلة ذات الحالة المنتهية، أو آلة الحالة اختصارًا)، والفكرة الأساسية لها هي أن هناك عددًا محدودًا من الحالات التي يمكن لبرنامج أن يكون عليها في لحظة ما (انظر ش.1)، ويتصرف البرنامج بشكل مختلف داخل كل حالة فريدة، ويمكن تبديل حالته من حالة إلى أخرى بسرعة وبشكل فوري.

(ش.1) آلة الحالة المنتهية (Finite-State Machine).

لكن وفقًا للحالة الراهنة (current state) التي عليها البرنامج فإنه قد يبدل حالته إلى حالات أخرى بعينها أو لا، وتسمى قوانين التبديل هذه بالانتقالات (transitions)، وهي أيضًا محدودة ومحددة مسبقًا كذلك.

تستطيع تطبيق هذا المنظور على الكائنات بتخيل أن لدينا فئة مستند Document، فهذا المستند قد يكون في حالة من ثلاث، إما مسوَّدة Draft أو تعديل Moderation أو منشور Published. ويعمل أسلوب publish للمستند بشكل مختلف قليلًا في كل حالة له:

  • ففي حالة Draft فإنه ينقل المستند إلى التعديل.
  • وفي التعديل فإنه يجعل المستند عامًا (Public) إن كان المستخدم الحالة يملك صلاحيات الإدارة (Admin).
  • أما في حالة Published فإنه لا يفعل أي شيء. (انظر ش.2)

(ش.2) الحالات الممكنة والانتقالات لكائن مستند.

تُستخدم آلات الحالة عادة مقترنة مع الكثير من المعامِلات الشرطية (conditional operators) مثل -if أو switch- التي تستخدم السلوك المناسب وفقًا للحالة الراهنة للكائن، وعادة ما تكون تلك الحالة مكونة من بعض القيم من حقول الكائن، لكن إن لم تسمع من قبل بآلات الحالة فلعلك قد نفَّذت بنفسك حالة من قبل مرة واحدة على الأقل، فمثلًا، هل ترى الشيفرة التالية مألوفة لك؟

class Document is
    field state: string
    // ...
    method publish() is
        switch (state)
            "draft":
                state = "moderation"
                break
            "moderation":
                if (currentUser.role == 'admin')
                    state = "published"
                break
            "published":
                // لا تفعل شيئًا.
                break
    // ...

ولعل أبرز وجه للقصور في آلة الحالة المبنية على الشرطيات (Conditionals) تظهر بمجرد أن نضيف حالات أكثر إلى فئة Document التي في مثالنا وسلوكيات تعتمد على الحالة كذلك، فأغلب الأساليب ستحتوي على شرطيات مهولة تلتقط السلوك المناسب للأسلوب وفقًا للحالة الراهنة، وستصبح الشيفرة بهذه الصورة صعبة الصيانة والتصحيح بسبب أن أي تعديل على منطق الانتقال (transition logic) قد يتطلب تغيير شرطيات الحالة في كل أسلوب.

وكلما تطور البرنامج زاد تعقيد المشكلة أكثر، ذلك أنه من الصعب توقع كل الحالات المحتملة والانتقالات في مرحلة التصميم، ومن ثم فإن آلة الحالة الرشيقة التي بُنيت بعدد محدود من الشرطيات قد تكبر إلى أن تصير فوضى عارمة مع الوقت.

الحل

يقترح نمط الحالة أن تنشئ فئات جديدة لكل الحالات المحتملة لكائن ما ثم تستخرج كل السلوكيات الخاصة بالحالة (state-specific) إلى تلك الفئات. ويخزن الكائن الأصلي المسمى بالسياق context مرجعًا إلى كائن من كائنات الحالة يمثل الحالة الراهنة، بدلًا من تطبيق جميع السلوكيات بنفسه.، ثم يفوض كل العمل المتعلق بالحالة إلى ذلك الكائن. انظر ش.3

ش.3 يفوض المستند العمل إلى كائن حالة.

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

مثال واقعي

تتصرف الأزرار في هاتفك بشكل مختلف وفقًا لحالة الهاتف الراهنة، وذلك كما يلي:

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

البُنية

ش.4

  1. يخزن السياق Context مرجعًا إلى واحد من كائنات الحالة الحقيقية ويفوض إليه جميع الأعمال المتعلقة بالحالة، ويتواصل السياق مع كائن الحالة من خلال واجهة الحالة، ويكشف محدِّدًا (setter) لتمرير كائن الحالة الجديد إليه.
  2. تصرح واجهة الحالة State عن أساليب خاصة بالحالة (state-specific methods)، ويجب أن تكون هذه الأساليب منطقية لجميع الحالات الحقيقية لأنك لا تريد بعض حالاتك أن تكون فيها أساليب عديمة الفائدة لا تُستدعى.
  3. توفر الحالات الحقيقية استخداماتها الخاصة للأساليب الخاصة بالحالة، ولتجنب تكرار الشيفرات المتشابهة بين الحالات المتعددة، فقد توفر فئات مجردة وسيطة تغلف بعض السلوك المشترك. أيضًا، قد تخزن كائنات الحالة مرجعًا خلفيًا إلى كائن السياق، وتستطيع الحالة من خلال هذا المرجع أن تجلب أي بيانات مطلوبة من كائن السياق إضافة إلى بدء انتقالات الحالة.
  4. تستطيع كل من حالة السياق (context state) والحالة الحقيقية (concrete state) أن تحدد الحالة التالية للسياق وتنفذ انتقال الحالة الفعلية باستبدال كائن الحالة المرتبط بالسياق.

مثال توضيحي

يتيح نمط الحالة في هذا المثال لنفس المتحكمات بمشغل الوسائط أن تتصرف بشكل مختلف وفقًا لحالة التشغيل الراهنة.

ش.5 مثال على تغيير سلوك الكائن بكائنات الحالة.

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

// كسياق، وتحافظ كذلك على مرجع إلى نسخة من إحدى AudioPlayer تتصرف فئة 
// فئات الحالة التي تمثل الحالة الراهنة لمشغل الصوتيات.
class AudioPlayer is
    field state: State
    field UI, volume, playlist, currentSong

    constructor AudioPlayer() is
        this.state = new ReadyState(this)

        // يفوض السياق معالجة مدخلات المستخدم إلى كائن حالة
        // ويعتمد المُخرج افتراضيًا على الحالة النشطة حاليًا
        // بما أن كل حالة تعالج المُدخل بشكل مختلف.
        UI = new UserInterface()
        UI.lockButton.onClick(this.clickLock)
        UI.playButton.onClick(this.clickPlay)
        UI.nextButton.onClick(this.clickNext)
        UI.prevButton.onClick(this.clickPrevious)

    // يجب أن تكون الكائنات الأخرى قادرة على تبديل الحالة النشطة للمشغل
    method changeState(state: State) is
        this.state = state

    // تفوض أساليب الواجهة الرسومية التنفيذَ إلى الحالة النشطة.
    method clickLock() is
        state.clickLock()
    method clickPlay() is
        state.clickPlay()
    method clickNext() is
        state.clickNext()
    method clickPrevious() is
        state.clickPrevious()

    // قد تستدعي حالة ما بعض أساليب الخدمة على السياق.
    method startPlayback() is
        // ...
    method stopPlayback() is
        // ...
    method nextSong() is
        // ...
    method previousSong() is
        // ...
    method fastForward(time) is
        // ...
    method rewind(time) is
        // ...


// تصرح فئة الحالة الأساسية أساليبًا يجب أن تستخدمها كل
// الحالات الحقيقية، وتوفر كذلك مرجعًا خلفيًا إلى كائن السياق
// المرتبط بالحالة، وتستطيع الحالات أن تستخدم مرجعًا خلفيًا لنقل
// السياق إلى حالة أخرى.
abstract class State is
    protected field player: AudioPlayer

    // يمرر السياق نفسه خلال منشئ الحالة، هذا يمكّن الحالة
    // من جلب بعض البيانات السياقية المفيدة عند الحاجة.
    constructor State(player) is
        this.player = player

    abstract method clickLock()
    abstract method clickPlay()
    abstract method clickNext()
    abstract method clickPrevious()


// تستخدم الحالات الحقيقية سلوكيات متعددة مرتبطة بحالة السياق.
class LockedState extends State is

    // حين تلغي قفل مشغِّل مقفل، فإنه يتخذ إحدى حالتين
    method clickLock() is
        if (player.playing)
            player.changeState(new PlayingState(player))
        else
            player.changeState(new ReadyState(player))

    method clickPlay() is
        // مقفل، لذا لا تفعل شيئًا.

    method clickNext() is
        // مقفل، لذا لا تفعل شيئًا.

    method clickPrevious() is
        // مقفل، لذا لا تفعل شيئًا.


// كما تستطيع تلك الحالات أيضًا أن تشغل انتقالات الحالة داخل السياق.
class ReadyState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.startPlayback()
        player.changeState(new PlayingState(player))

    method clickNext() is
        player.nextSong()

    method clickPrevious() is
        player.previousSong()


class PlayingState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.stopPlayback()
        player.changeState(new ReadyState(player))

    method clickNext() is
        if (event.doubleclick)
            player.nextSong()
        else
            player.fastForward(5)

    method clickPrevious() is
        if (event.doubleclick)
            player.previous()
        else
            player.rewind(5)

قابلية التطبيق

  • استخدم نمط الحالة حين يكون لديك كائن يتصرف بطرق مختلفة وفقًا لحالته الراهنة وحين يكون عدد الحالات كبيرًا وكذلك حين تتغير الشيفرة الخاصة بالحالة (state-specific) بشكل متكرر.

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

  • استخدم النمط حين يكون لديك فئة بها شَرطيَّات كبيرة تغير الكيفية التي تتصرف بها الفئة وفقًا للقيم الحالة لحقول الفئة.

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

  • استخدم الحالة حين يكون لديك الكثير من الشيفرات المتكررة في حالات متشابهة وانتقالات لآلة حالة شرطية (condition-based machine).

يسمح لك نمط الحالة بتركيب هرميات من فئات الحالة وتقليل التكرار من خلال استرخاج شيفرة مشتركة إلى فئات مجردة أساسية (abstract base classes).

كيفية الاستخدام

  1. حدد أي فئة ستتصرف كسياق