الفرق بين المراجعتين ل"Design Patterns/state"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
ط
سطر 1: سطر 1:
<noinclude>{{DISPLAYTITLE:نمط الحالة (State)}}</noinclude>
+
<noinclude>{{DISPLAYTITLE:نمط الحالة State}}</noinclude>
 
نمط الحالة (State) هو نمط تصميم سلوكي يسمح للكائن بتغيير سلوكه حين تتغير حالته الداخلية، ويبدو حينها كأن الكائن قد غيّر فئته.
 
نمط الحالة (State) هو نمط تصميم سلوكي يسمح للكائن بتغيير سلوكه حين تتغير حالته الداخلية، ويبدو حينها كأن الكائن قد غيّر فئته.
  

مراجعة 13:49، 4 مارس 2020

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

المشكلة

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

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

لكن وفقًا للحالة الراهنة (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) قد يتطلب تغيير شرطيات الحالة في كل أسلوب.

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

الحل

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

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

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

مثال واقعي

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

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

البُنية

dpst.structure-indexed.png
  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) بشكل متكرر.

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

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

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

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

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

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

  1. حدد أي فئة ستتصرف كسياق، قد تكون فئة موجودة فعلًا لديها شيفرة معتمدة على الحالة، أو فئة جديدة إن كانت الشيفرة الخاصة بالحالة موزعة على فئات متعددة.
  2. صرح عن واجهة الحالة، فرغم أنها قد تعكس جميع الأساليب المصرح عنها في السياق، واستهدف تلك التي قد تحتوي على سلوك خاص بالحالة (state-specific behavior).
  3. أنشئ فئة مشتقة من واجهة الحالة، وذلك لكل حالة فعلية، ثم اذهب إلى أساليب السياق واستخرج كل الشيفرات المتعلفة بتلك الحالة إلى فئتك الجديدة التي أنشأتها. ربما تكتشف أثناء نقل الشيفرة إلى فئة الحالة أنها تعتمد على عناصر خاصة (private members) من السياق، وإليك كيف تجتاز هذا الأمر:
    • إما أن تجعل هذه الأساليب أو الحقول عامة (public).
    • أو تحول السلوك الذي تستخرجه إلى سلوك عام داخل السياق وتستدعيه من فئة الحالة، هذه الطريقة ليست الأمثل لكنها سريعة، ويمكنك إصلاح ذلك لاحقًا.
    • ضع فئات الحالة داخل فئة السياق بشكل متداخل (nesting)، لكن ذلك فقط في حالة إن كانت لغة البرمجة التي تستخدمها تدعم التداخل.
  4. في فئة السياق، أضف حقلًا مرجعيًا لنوع واجهة الحاةل ومحدِّدًا عامًا يسمح بتجاوز قيمة ذلك الحقل.
  5. اذهب إلى أسلوب السياق مرة أخرى واستبدل شرطيات الحالة الفارغة باستدعاءات إلى الأساليب المناسبة لكائن الحالة.
  6. لتبديل حالة السياق، أنشئ نسخة من إحدى فئات الحالة ومررها إلى السياق، تستطيع فعل ذلك داخل السياق نفسه، أو في فئات متعددة، أو داخل العميل. وأينما نفَّذت ذلك فإن الفئة تصبح معتمدة على فئة الحالة الحقيقية التي تبدؤها (instantiates).

المزايا والعيوب

المزايا

  • مبدأ المسؤولية الواحدة. نظِّم الشيفرة المتعلقة بحالات بعينها في فئات منفصلة.
  • مبدأ المفتوح/المغلق. أدخل حالات جديدة دون تغيير فئات الحالة الموجودة فعلًا أو السياق.
  • بسِّط شيفرة السياق من خلال إزالة الشرطيات كبيرة الحجم لآلة الحالة.

العيوب

  • قد يكون من الإسراف استخدام النمط إن كانت آلة الحالة لا تحتوي إلا على حالات قليلة أو حالات قلما تتغير.

العلاقات مع الأنماط الأخرى

  • تتشابه أنماط الجسر والحالة والخطةالمحول إلى حد ما) في بنياتها، فكل هذه الأنماط مبنية على التركيب (composition) الذي يفوض المهام إلى كائنات أخرى، لكنها كذلك تحل مشاكل مختلفة، فالنمط ليس وصفة لهيكلة شيفرتك بشكل معين، فهو كذلك يوصل المشكلة التي يحلها إلى المطورين الآخرين.
  • يمكن النظر إلى نمط الحالة على أنه امتداد لنمط الخطة، فكلا النمطين مبنيان على التركيب، إذ يغيران سلوك السياق من خلال تفويض بعض المهام إلى كائنات مساعدة. والفرق أن نمط الخطة يجعل هذه الكائنات مستقلة تمامًا عن بعضها وغير مدركة لوجود غيرها حولها، بينما لا يقيد نمط الحالةِ الاعتماديات بين الحالات الحقيقية، مما يسمح لها بتغيير حالة السياق متى شاءت.

الاستخدام في لغة جافا

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط الحالة في لغة جافا لتحويل آلات الحالة كبيرة الحجم المبنية على switch إلى كائنات أخرى. إليك بعض الأمثلة على نمط الحالة في مكتبات جافا:

يمكن ملاحظة النمط من خلال الأساليب التي تغير سلوكها وفقًا لحالة الكائنات المتحكَّم بها من الخارج.

واجهة مشغل الوسائط

الحالات States

 states/State.java: واجهة الحالة المشتركة
package refactoring_guru.state.example.states;

import refactoring_guru.state.example.ui.Player;

/**
 * واجهة مشتركة لجميع الحالات.
 */
public abstract class State {
    Player player;

    /**
     * state constructor يمرر السياق نفسه خلال منشئ الحالة
     * هذا قد يساعد الحالة لتجلب بعض البيانات السياقية المفيدة عند الحاجة. 
     */
    State(Player player) {
        this.player = player;
    }

    public abstract String onLock();
    public abstract String onPlay();
    public abstract String onNext();
    public abstract String onPrevious();
}
 states/LockedState.java
package refactoring_guru.state.example.states;

import refactoring_guru.state.example.ui.Player;

/**
 * توفر الحالات الحقيقية الاستخدام الخاص لكل أساليب الواجهة.
 */
public class LockedState extends State {

    LockedState(Player player) {
        super(player);
        player.setPlaying(false);
    }

    @Override
    public String onLock() {
        if (player.isPlaying()) {
            player.changeState(new ReadyState(player));
            return "Stop playing";
        } else {
            return "Locked...";
        }
    }

    @Override
    public String onPlay() {
        player.changeState(new ReadyState(player));
        return "Ready";
    }

    @Override
    public String onNext() {
        return "Locked...";
    }

    @Override
    public String onPrevious() {
        return "Locked...";
    }
}
 states/ReadyState.java
package refactoring_guru.state.example.states;

import refactoring_guru.state.example.ui.Player;

/**
 * تستطيع أيضًا أن تشغِّل انتقالات الحالة داخل السياق.
 */
public class ReadyState extends State {

    public ReadyState(Player player) {
        super(player);
    }

    @Override
    public String onLock() {
        player.changeState(new LockedState(player));
        return "Locked...";
    }

    @Override
    public String onPlay() {
        String action = player.startPlayback();
        player.changeState(new PlayingState(player));
        return action;
    }

    @Override
    public String onNext() {
        return "Locked...";
    }

    @Override
    public String onPrevious() {
        return "Locked...";
    }
}
 states/PlayingState.java
package refactoring_guru.state.example.states;

import refactoring_guru.state.example.ui.Player;

public class PlayingState extends State {

    PlayingState(Player player) {
        super(player);
    }

    @Override
    public String onLock() {
        player.changeState(new LockedState(player));
        player.setCurrentTrackAfterStop();
        return "Stop playing";
    }

    @Override
    public String onPlay() {
        player.changeState(new ReadyState(player));
        return "Paused...";
    }

    @Override
    public String onNext() {
        return player.nextTrack();
    }

    @Override
    public String onPrevious() {
        return player.previousTrack();
    }
}

الواجهة الرسومية UI

 ui/Player.java: شيفرة المشغل الرئيسية
package refactoring_guru.state.example.ui;

import refactoring_guru.state.example.states.ReadyState;
import refactoring_guru.state.example.states.State;

import java.util.ArrayList;
import java.util.List;

public class Player {
    private State state;
    private boolean playing = false;
    private List<String> playlist = new ArrayList<>();
    private int currentTrack = 0;

    public Player() {
        this.state = new ReadyState(this);
        setPlaying(true);
        for (int i = 1; i <= 12; i++) {
            playlist.add("Track " + i);
        }
    }

    public void changeState(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void setPlaying(boolean playing) {
        this.playing = playing;
    }

    public boolean isPlaying() {
        return playing;
    }

    public String startPlayback() {
        return "Playing " + playlist.get(currentTrack);
    }

    public String nextTrack() {
        currentTrack++;
        if (currentTrack > playlist.size() - 1) {
            currentTrack = 0;
        }
        return "Playing " + playlist.get(currentTrack);
    }

    public String previousTrack() {
        currentTrack--;
        if (currentTrack < 0) {
            currentTrack = playlist.size() - 1;
        }
        return "Playing " + playlist.get(currentTrack);
    }

    public void setCurrentTrackAfterStop() {
        this.currentTrack = 0;
    }
}
 ui/UI.java: واجهة المشغل الرسومية
package refactoring_guru.state.example.ui;

import javax.swing.*;
import java.awt.*;

public class UI {
    private Player player;
    private static JTextField textField = new JTextField();

    public UI(Player player) {
        this.player = player;
    }

    public void init() {
        JFrame frame = new JFrame("Test player");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel context = new JPanel();
        context.setLayout(new BoxLayout(context, BoxLayout.Y_AXIS));
        frame.getContentPane().add(context);
        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
        context.add(textField);
        context.add(buttons);

        // يفوض السياق معالجة مدخلات المستخدم إلى كائن حالة، ويعتمد
        // الخرج على الحالة النشطة بما أن كل الحالات يمكنها معالجة
        // المدخلات بشكل مختلف.
        JButton play = new JButton("Play");
        play.addActionListener(e -> textField.setText(player.getState().onPlay()));
        JButton stop = new JButton("Stop");
        stop.addActionListener(e -> textField.setText(player.getState().onLock()));
        JButton next = new JButton("Next");
        next.addActionListener(e -> textField.setText(player.getState().onNext()));
        JButton prev = new JButton("Prev");
        prev.addActionListener(e -> textField.setText(player.getState().onPrevious()));
        frame.setVisible(true);
        frame.setSize(300, 100);
        buttons.add(play);
        buttons.add(stop);
        buttons.add(next);
        buttons.add(prev);
    }
}
 Demo.java: شيفرة البدء
package refactoring_guru.state.example;

import refactoring_guru.state.example.ui.Player;
import refactoring_guru.state.example.ui.UI;

/**
 * فئة العرض، يجتمع كل شيء هنا.
 */
public class Demo {
    public static void main(String[] args) {
        Player player = new Player();
        UI ui = new UI(player);
        ui.init();
    }
}
 OutputDemo.png: لقطة الشاشة

dpst.OutputDemo.png

الاستخدام في لغة #C

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط الحالة في لغة #C لتحويل آلات الحالة كبيرة الحجم المبنية على switch إلى كائنات أخرى.

يمكن ملاحظة النمط من خلال الأساليب التي تغير سلوكها وفقًا لحالة الكائنات المتحكَّم بها من الخارج.

مثال تصوري

يوضح هذا المثال بنية نمط الحالة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 Program.cs: مثال تصوري

using System;

namespace RefactoringGuru.DesignPatterns.State.Conceptual
{
    // يحدد السياق الواجهة التي تهم العملاء، ويحافظ على مرجع
    // إلى نسخة من فئة فرعية لحالة تمثل الحالة الراهنة للسياق.
    class Context
    {
        // مرجع إلى الحالة الراهنة للسياق.
        private State _state = null;

        public Context(State state)
        {
            this.TransitionTo(state);
        }

        // يسمح السياق بتغيير كائن الحالة أثناء وقت التشغيل.
        public void TransitionTo(State state)
        {
            Console.WriteLine($"Context: Transition to {state.GetType().Name}.");
            this._state = state;
            this._state.SetContext(this);
        }

        // يفوض السياق جزءًا من سلوكه إلى كائن الحالة الراهنة.
        public void Request1()
        {
            this._state.Handle1();
        }

        public void Request2()
        {
            this._state.Handle2();
        }
    }
    
    // عن أساليب يجب Base state class تصرح فئة الحالة الأساسية
    // أن تستخدمها جميع الحالات الحقيقية، كما تزود الفئة مرجعًا خلفيًا
    // إلى كائن السياق المرتبط بالحالة، إذ يمكن للحالات أن تستخدم
    // ذلك المرجع لنقل السياق إلى حالة أخرى.
    abstract class State
    {
        protected Context _context;

        public void SetContext(Context context)
        {
            this._context = context;
        }

        public abstract void Handle1();

        public abstract void Handle2();
    }

    // تستخدم الحالات الحقيقية سلوكيات مختلفة مرتبطة 
    // بحالة السياق.
    class ConcreteStateA : State
    {
        public override void Handle1()
        {
            Console.WriteLine("ConcreteStateA handles request1.");
            Console.WriteLine("ConcreteStateA wants to change the state of the context.");
            this._context.TransitionTo(new ConcreteStateB());
        }

        public override void Handle2()
        {
            Console.WriteLine("ConcreteStateA handles request2.");
        }
    }

    class ConcreteStateB : State
    {
        public override void Handle1()
        {
            Console.Write("ConcreteStateB handles request1.");
        }

        public override void Handle2()
        {
            Console.WriteLine("ConcreteStateB handles request2.");
            Console.WriteLine("ConcreteStateB wants to change the state of the context.");
            this._context.TransitionTo(new ConcreteStateA());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // شيفرة العميل.
            var context = new Context(new ConcreteStateA());
            context.Request1();
            context.Request2();
        }
    }
}

 Output.txt: نتائج التنفيذ

Context: Transition to ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA.

الاستخدام في لغة PHP

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط الحالة في لغة PHP لتحويل آلات الحالة كبيرة الحجم المبنية على switch إلى كائنات أخرى.

مثال تصوري

يوضح هذا المثال بنية نمط الحالة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها.\

سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP.

index.php: مثال تصوري

<?php

namespace RefactoringGuru\State\Conceptual;

/**
 * يحدد السياق الواجهة التي تهم العملاء، ويحافظ على مرجع
 * إلى نسخة من فئة فرعية لحالة تمثل الحالة الراهنة للسياق.
 */
class Context
{
    /**
     * @var مرجع إلى الحالة الراهنة للسياق.
     */
    private $state;
    
    public function __construct(State $state)
    {
        $this->transitionTo($state);
    }

    /**
     * يسمح السياق بتغيير كائن الحالة أثناء وقت التشغيل.
     */
    public function transitionTo(State $state): void
    {
        echo "Context: Transition to " . get_class($state) . ".\n";
        $this->state = $state;
        $this->state->setContext($this);
    }

    /**
     * يفوض السياق جزءًا من سلوكه إلى كائن الحالة الراهنة.
     */
    public function request1(): void
    {
        $this->state->handle1();
    }

    public function request2(): void
    {
        $this->state->handle2();
    }
}

/**
 * عن أساليب يجب Base state class تصرح فئة الحالة الأساسية
 * أن تستخدمها جميع الحالات الحقيقية، كما تزود الفئة مرجعًا خلفيًا
 * إلى كائن السياق المرتبط بالحالة، إذ يمكن للحالات أن تستخدم
 * ذلك المرجع لنقل السياق إلى حالة أخرى.
 */
abstract class State
{
    /**
     * @var Context
     */
    protected $context;

    public function setContext(Context $context)
    {
        $this->context = $context;
    }

    abstract public function handle1(): void;

    abstract public function handle2(): void;
}

/**
 * تستخدم الحالات الحقيقية سلوكيات مختلفة مرتبطة
 * بحالة السياق.
 */
class ConcreteStateA extends State
{
    public function handle1(): void
    {
        echo "ConcreteStateA handles request1.\n";
        echo "ConcreteStateA wants to change the state of the context.\n";
        $this->context->transitionTo(new ConcreteStateB);
    }

    public function handle2(): void
    {
        echo "ConcreteStateA handles request2.\n";
    }
}

class ConcreteStateB extends State
{
    public function handle1(): void
    {
        echo "ConcreteStateB handles request1.\n";
    }

    public function handle2(): void
    {
        echo "ConcreteStateB handles request2.\n";
        echo "ConcreteStateB wants to change the state of the context.\n";
        $this->context->transitionTo(new ConcreteStateA);
    }
}

/**
 * شيفرة العميل.
 */
$context = new Context(new ConcreteStateA);
$context->request1();
$context->request2();

الاستخدام في لغة بايثون

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط الحالة في لغة بايثون لتحويل آلات الحالة كبيرة الحجم المبنية على switch إلى كائنات أخرى.

يمكن ملاحظة النمط من خلال الأساليب التي تغير سلوكها وفقًا لحالة الكائنات المتحكَّم بها من الخارج.

مثال تصوري

يوضح هذا المثال بنية نمط الحالة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 main.py: مثال تصوري

from __future__ import annotations
from abc import ABC, abstractmethod


class Context(ABC):
    """
    يحدد السياق الواجهة التي تهم العملاء، ويحافظ على مرجع
    إلى نسخة من فئة فرعية لحالة تمثل الحالة الراهنة للسياق.
    """

    _state = None
    """
    مرجع إلى الحالة الراهنة للسياق.
    """

    def __init__(self, state: State) -> None:
        self.transition_to(state)

    def transition_to(self, state: State):
        """
        يسمح السياق بتغيير كائن الحالة أثناء وقت التشغيل.
        """

        print(f"Context: Transition to {type(state).__name__}")
        self._state = state
        self._state.context = self

    """
    يفوض السياق جزءًا من سلوكه إلى كائن الحالة الراهنة.
    """

    def request1(self):
        self._state.handle1()

    def request2(self):
        self._state.handle2()


class State(ABC):
    """
    عن أساليب يجب Base state class تصرح فئة الحالة الأساسية
    أن تستخدمها جميع الحالات الحقيقية، كما تزود الفئة مرجعًا خلفيًا
    إلى كائن السياق المرتبط بالحالة، إذ يمكن للحالات أن تستخدم
    ذلك المرجع لنقل السياق إلى حالة أخرى.
    """

    @property
    def context(self) -> Context:
        return self._context

    @context.setter
    def context(self, context: Context) -> None:
        self._context = context

    @abstractmethod
    def handle1(self) -> None:
        pass

    @abstractmethod
    def handle2(self) -> None:
        pass


"""
تستخدم الحالات الحقيقية سلوكيات مختلفة مرتبطة بحالة السياق.
"""


class ConcreteStateA(State):
    def handle1(self) -> None:
        print("ConcreteStateA handles request1.")
        print("ConcreteStateA wants to change the state of the context.")
        self.context.transition_to(ConcreteStateB())

    def handle2(self) -> None:
        print("ConcreteStateA handles request2.")


class ConcreteStateB(State):
    def handle1(self) -> None:
        print("ConcreteStateB handles request1.")

    def handle2(self) -> None:
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the context.")
        self.context.transition_to(ConcreteStateA())


if __name__ == "__main__":
    # شيفرة العميل.

    context = Context(ConcreteStateA())
    context.request1()
    context.request2()

 Output.txt: نتائج التنفيذ

Context: Transition to ConcreteStateA
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA

الاستخدام في لغة روبي

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط الحالة في لغة روبي لتحويل آلات الحالة كبيرة الحجم المبنية على switch إلى كائنات أخرى.

يمكن ملاحظة النمط من خلال الأساليب التي تغير سلوكها وفقًا لحالة الكائنات المتحكَّم بها من الخارج.

مثال تصوري

يوضح هذا المثال بنية نمط الحالة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 main.rb: مثال تصوري

# يحدد السياق الواجهة التي تهم العملاء، ويحافظ على مرجع
# إلى نسخة من فئة فرعية لحالة تمثل الحالة الراهنة للسياق.
class Context
  # مرجع إلى الحالة الراهنة للسياق.
  attr_accessor :state
  private :state

  # @param [State] state
  def initialize(state)
    transition_to(state)
  end

  # يسمح السياق بتغيير كائن الحالة أثناء وقت التشغيل.
  def transition_to(state)
    puts "Context: Transition to #{state.class}"
    @state = state
    @state.context = self
  end

  # يفوض السياق جزءًا من سلوكه إلى كائن الحالة الراهنة.

  def request1
    @state.handle1
  end

  def request2
    @state.handle2
  end
end

# عن أساليب يجب Base state class تصرح فئة الحالة الأساسية
# ن تستخدمها جميع الحالات الحقيقية، كما تزود الفئة مرجعًا خلفيًا
# إلى كائن السياق المرتبط بالحالة، إذ يمكن للحالات أن تستخدم
# ذلك المرجع لنقل السياق إلى حالة أخرى.
class State
  attr_accessor :context

  # @abstract
  def handle1
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # @abstract
  def handle2
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# تستخدم الحالات الحقيقية سلوكيات مختلفة مرتبطة بحالة السياق.

class ConcreteStateA < State
  def handle1
    puts 'ConcreteStateA handles request1.'
    puts 'ConcreteStateA wants to change the state of the context.'
    @context.transition_to(ConcreteStateB.new)
  end

  def handle2
    puts 'ConcreteStateA handles request2.'
  end
end

class ConcreteStateB < State
  def handle1
    puts 'ConcreteStateB handles request1.'
  end

  def handle2
    puts 'ConcreteStateB handles request2.'
    puts 'ConcreteStateB wants to change the state of the context.'
    @context.transition_to(ConcreteStateA.new)
  end
end

# شيفرة العميل.

context = Context.new(ConcreteStateA.new)
context.request1
context.request2

 output.txt: نتائج التنفيذ

Context: Transition to ConcreteStateA
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA

الاستخدام في لغة Swift

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط الحالة في لغة Swift لتحويل آلات الحالة كبيرة الحجم المبنية على switch إلى كائنات أخرى.

يمكن ملاحظة النمط من خلال الأساليب التي تغير سلوكها وفقًا لحالة الكائنات المتحكَّم بها من الخارج.

مثال تصوري

يوضح هذا المثال بنية نمط الحالة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

بعد تعلم بنية النمط سيكون من السهل عليك استيعاب المثال التالي المبني على حالة واقعية في لغة Swift.

import XCTest

/// يحدد السياق الواجهة التي تهم العملاء، ويحافظ على مرجع
/// إلى نسخة من فئة فرعية لحالة تمثل الحالة الراهنة للسياق.
class Context {

    /// مرجع إلى الحالة الراهنة للسياق.
    private var state: State

    init(_ state: State) {
        self.state = state
        transitionTo(state: state)
    }

    /// يسمح السياق بتغيير كائن الحالة أثناء وقت التشغيل.
    func transitionTo(state: State) {
        print("Context: Transition to " + String(describing: state))
        self.state = state
        self.state.update(context: self)
    }

    /// يفوض السياق جزءًا من سلوكه إلى كائن الحالة الراهنة.
    func request1() {
        state.handle1()
    }

    func request2() {
        state.handle2()
    }
}

/// عن أساليب يجب Base state class تصرح فئة الحالة الأساسية
/// أن تستخدمها جميع الحالات الحقيقية، كما تزود الفئة مرجعًا خلفيًا
/// إلى كائن السياق المرتبط بالحالة، إذ يمكن للحالات أن تستخدم
/// ذلك المرجع لنقل السياق إلى حالة أخرى.
protocol State: class {

    func update(context: Context)

    func handle1()
    func handle2()
}

class BaseState: State {

    private(set) weak var context: Context?

    func update(context: Context) {
        self.context = context
    }

    func handle1() {}
    func handle2() {}
}

/// تستخدم الحالات الحقيقية سلوكيات مختلفة مرتبطة بحالة السياق.
class ConcreteStateA: BaseState {

    override func handle1() {
        print("ConcreteStateA handles request1.")
        print("ConcreteStateA wants to change the state of the context.\n")
        context?.transitionTo(state: ConcreteStateB())
    }

    override func handle2() {
        print("ConcreteStateA handles request2.\n")
    }
}

class ConcreteStateB: BaseState {

    override func handle1() {
        print("ConcreteStateB handles request1.\n")
    }

    override func handle2() {
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the context.\n")
        context?.transitionTo(state: ConcreteStateA())
    }
}

/// لنرى الآن كيف سيعمل كل ذلك.
class StateConceptual: XCTestCase {

    func test() {
        let context = Context(ConcreteStateA())
        context.request1()
        context.request2()
    }
}

 Output.txt: نتائج التنفيذ

Context: Transition to StateConceptual.ConcreteStateA
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.

Context: Transition to StateConceptual.ConcreteStateB
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.

Context: Transition to StateConceptual.ConcreteStateA

مثال واقعي

 Example.swift: مثال واقعي

import XCTest

class StateRealWorld: XCTestCase {

    func test() {

        print("Client: I'm starting working with a location tracker")
        let tracker = LocationTracker()

        print()
        tracker.startTracking()

        print()
        tracker.pauseTracking(for: 2)

        print()
        tracker.makeCheckIn()

        print()
        tracker.findMyChildren()

        print()
        tracker.stopTracking()
    }
}

class LocationTracker {

    /// تتبع الموقع مفعل افتراضيًا
    private lazy var trackingState: TrackingState = EnabledTrackingState(tracker: self)

    func startTracking() {
        trackingState.startTracking()
    }

    func stopTracking() {
        trackingState.stopTracking()
    }

    func pauseTracking(for time: TimeInterval) {
        trackingState.pauseTracking(for: time)
    }

    func makeCheckIn() {
        trackingState.makeCheckIn()
    }

    func findMyChildren() {
        trackingState.findMyChildren()
    }

    func update(state: TrackingState) {
        trackingState = state
    }
}

protocol TrackingState {

    func startTracking()
    func stopTracking()
    func pauseTracking(for time: TimeInterval)

    func makeCheckIn()
    func findMyChildren()
}

class EnabledTrackingState: TrackingState {

    private weak var tracker: LocationTracker?

    init(tracker: LocationTracker?) {
        self.tracker = tracker
    }

    func startTracking() {
        print("EnabledTrackingState: startTracking is invoked")
        print("EnabledTrackingState: tracking location....1")
        print("EnabledTrackingState: tracking location....2")
        print("EnabledTrackingState: tracking location....3")
    }

    func stopTracking() {
        print("EnabledTrackingState: Received 'stop tracking'")
        print("EnabledTrackingState: Changing state to 'disabled'...")
        tracker?.update(state: DisabledTrackingState(tracker: tracker))
        tracker?.stopTracking()
    }

    func pauseTracking(for time: TimeInterval) {
        print("EnabledTrackingState: Received 'pause tracking' for \(time) seconds")
        print("EnabledTrackingState: Changing state to 'disabled'...")
        tracker?.update(state: DisabledTrackingState(tracker: tracker))
        tracker?.pauseTracking(for: time)
    }

    func makeCheckIn() {
        print("EnabledTrackingState: performing check-in at the current location")
    }

    func findMyChildren() {
        print("EnabledTrackingState: searching for children...")
    }
}

class DisabledTrackingState: TrackingState {

    private weak var tracker: LocationTracker?

    init(tracker: LocationTracker?) {
        self.tracker = tracker
    }

    func startTracking() {
        print("DisabledTrackingState: Received 'start tracking'")
        print("DisabledTrackingState: Changing state to 'enabled'...")
        tracker?.update(state: EnabledTrackingState(tracker: tracker))
    }

    func pauseTracking(for time: TimeInterval) {
        print("DisabledTrackingState: Pause tracking for \(time) seconds")

        for i in 0...Int(time) {
            print("DisabledTrackingState: pause...\(i)")
        }

        print("DisabledTrackingState: Time is over")
        print("DisabledTrackingState: Returing to 'enabled state'...\n")
        self.tracker?.update(state: EnabledTrackingState(tracker: self.tracker))
        self.tracker?.startTracking()
    }

    func stopTracking() {
        print("DisabledTrackingState: Received 'stop tracking'")
        print("DisabledTrackingState: Do nothing...")
    }

    func makeCheckIn() {
        print("DisabledTrackingState: Received 'make check-in'")
        print("DisabledTrackingState: Changing state to 'enabled'...")
        tracker?.update(state: EnabledTrackingState(tracker: tracker))
        tracker?.makeCheckIn()
    }

    func findMyChildren() {
        print("DisabledTrackingState: Received 'find my children'")
        print("DisabledTrackingState: Changing state to 'enabled'...")
        tracker?.update(state: EnabledTrackingState(tracker: tracker))
        tracker?.findMyChildren()
    }
}

 Output.txt: نتائج التنفيذ

Client: I'm starting working with a location tracker

EnabledTrackingState: startTracking is invoked
EnabledTrackingState: tracking location....1
EnabledTrackingState: tracking location....2
EnabledTrackingState: tracking location....3

EnabledTrackingState: Received 'pause tracking' for 2.0 seconds
EnabledTrackingState: Changing state to 'disabled'...
DisabledTrackingState: Pause tracking for 2.0 seconds
DisabledTrackingState: pause...0
DisabledTrackingState: pause...1
DisabledTrackingState: pause...2
DisabledTrackingState: Time is over
DisabledTrackingState: Returing to 'enabled state'...

EnabledTrackingState: startTracking is invoked
EnabledTrackingState: tracking location....1
EnabledTrackingState: tracking location....2
EnabledTrackingState: tracking location....3

EnabledTrackingState: performing check-in at the current location

EnabledTrackingState: searching for children...

EnabledTrackingState: Received 'stop tracking'
EnabledTrackingState: Changing state to 'disabled'...
DisabledTrackingState: Received 'stop tracking'
DisabledTrackingState: Do nothing...

الاستخدام في لغة TypeScript

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط الحالة في لغة TypeScript لتحويل آلات الحالة كبيرة الحجم المبنية على switch إلى كائنات أخرى.

يمكن ملاحظة النمط من خلال الأساليب التي تغير سلوكها وفقًا لحالة الكائنات المتحكَّم بها من الخارج.

مثال تصوري

يوضح هذا المثال بنية نمط الحالة، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 index.ts: مثال تصوري

/**
 * يحدد السياق الواجهة التي تهم العملاء، ويحافظ على مرجع
 * إلى نسخة من فئة فرعية لحالة تمثل الحالة الراهنة للسياق.
 */
class Context {
    /**
     * @type {State} مرجع إلى الحالة الراهنة للسياق.
     */
    private state: State;

    constructor(state: State) {
        this.transitionTo(state);
    }

    /**
     * يسمح السياق بتغيير كائن الحالة أثناء وقت التشغيل.
     */
    public transitionTo(state: State): void {
        console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
        this.state = state;
        this.state.setContext(this);
    }

    /**
     * يفوض السياق جزءًا من سلوكه إلى كائن الحالة الراهنة.
     */
    public request1(): void {
        this.state.handle1();
    }

    public request2(): void {
        this.state.handle2();
    }
}

/**
 * عن أساليب يجب Base state class تصرح فئة الحالة الأساسية
 * أن تستخدمها جميع الحالات الحقيقية، كما تزود الفئة مرجعًا خلفيًا
 * إلى كائن السياق المرتبط بالحالة، إذ يمكن للحالات أن تستخدم
 * ذلك المرجع لنقل السياق إلى حالة أخرى.
 */
abstract class State {
    protected context: Context;

    public setContext(context: Context) {
        this.context = context;
    }

    public abstract handle1(): void;

    public abstract handle2(): void;
}

/**
 * تستخدم الحالات الحقيقية سلوكيات مختلفة مرتبطة بحالة السياق.
 */
class ConcreteStateA extends State {
    public handle1(): void {
        console.log('ConcreteStateA handles request1.');
        console.log('ConcreteStateA wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateB());
    }

    public handle2(): void {
        console.log('ConcreteStateA handles request2.');
    }
}

class ConcreteStateB extends State {
    public handle1(): void {
        console.log('ConcreteStateB handles request1.');
    }

    public handle2(): void {
        console.log('ConcreteStateB handles request2.');
        console.log('ConcreteStateB wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateA());
    }
}

/**
 * شيفرة العميل.
 */
const context = new Context(new ConcreteStateA());
context.request1();
context.request2();

 Output.txt: نتائج التنفيذ

Context: Transition to ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA.

انظر أيضًا

مصادر