الحالات المركّبة والتاريخ
الحالات المركّبة والتاريخ
تُنمذج مخططات آلة الحالة التي درستها في الدرس السابق كل سلوك على هيئة مجموعة مسطحة من الحالات المسماة مع انتقالات بينها. هذا يعمل بشكل مثالي للكائنات البسيطة. غير أن الأنظمة الحقيقية كثيرًا ما تحتوي على حالات بالغة التعقيد تستحق هي ذاتها بنيةً داخلية. فموعد العيادة ليس مجرد "نشط" — بل حين يكون نشطًا فهو إما في طور انتظار التأكيد أو مؤكَّد أو قيد التنفيذ. تسطيح هذه الحالات الفرعية في المخطط الخارجي سيرغمك على تكرار كل انتقال يخرج من مجموعة "نشط"، مما يُفضي إلى مخطط متشابك يعسر قراءته.
تحل الحالات المركّبة هذه المشكلة بالسماح لك بتضمين آلة فرعية كاملة داخل مستطيل ذي زوايا مدوّرة واحد. وهي تعادل UML لفكرة "المجلد": تعطي الحالة الخارجية اسمًا للمجموعة، وتلتقط الحالات الداخلية التفاصيل. مقرونةً بـحالات التاريخ الزائفة — التي تتذكر المكان الذي كانت فيه الآلة قبل انقطاع ما — تتيح لك الحالات المركّبة نمذجة سير العمل المعقد في الواقع بصورة دقيقة ومرتبة.
تشريح الحالة المركّبة
تبدو الحالة المركّبة تمامًا كالحالة العادية (مستطيل بزوايا مدوّرة واسمها في الأعلى) لكنها تحتوي على حالات فرعية داخلية وحالتها الزائفة الأولية الخاصة وانتقالاتها الداخلية المرسومة بداخل الحدود الخارجية. القواعد هي:
- لكل حالة مركّبة حالة زائفة أولية واحدة بالضبط داخلها (الدائرة المملوءة)، تُشير إلى الحالة الفرعية التي تُدخَل إليها افتراضيًا.
- الانتقالات القادمة من خارج الحالة المركّبة والمستهدِفة الحالة المركّبة ذاتها تدخل عبر حالتها الفرعية الأولية الافتراضية.
- الانتقالات التي تنشأ داخل حالة فرعية وتخرج عبر الحدود المركّبة تُسمى انتقالات الخروج؛ وهي تخرج بغض النظر عن أي حالة فرعية كانت الآلة فيها.
- قد تمتلك الحالة المركّبة إجراء دخول اختياريًا (يُنفَّذ عند كل دخول للحالة) وإجراء خروج (يُنفَّذ عند كل مغادرة)، ويُكتبان بصيغة
entry / action()وexit / action()داخل صندوق الحالة.
entry / startTimer()، ينفَّذ هذا الإجراء في كل مرة تدخل فيها الآلة عبر أي مسار إلى الحالة المركّبة — سواء عبر الحالة الفرعية الأولية الافتراضية أو عبر انتقال مباشر يستهدف حالة فرعية بعينها.
مثال 1 — دورة حياة طلب المتجر الإلكتروني
تصوّر كائن طلب في متجر إلكتروني. على المستوى الأعلى يمكن أن يكون معلقًا أو قيد المعالجة أو مشحونًا أو ملغيًا. حالة قيد المعالجة مركّبة بذاتها: داخليًا يمر الطلب بـالتحقق من الدفع ثم الانتقاء من المستودع ثم التعبئة قبل الانتقال إلى مشحون. حدث إلغاء طارئ يمكنه الخروج من قيد المعالجة في أي حالة فرعية، محفِّزًا إجراء exit / releaseInventory() واحدًا.
cancel واحد يغادر من أي حالة فرعية؛ exit / releaseInventory() ينفَّذ دائمًا عند مغادرة Processing.إجراءات الدخول والخروج بالتفصيل
قد تُعلن كل حالة — بسيطة كانت أم مركّبة — عن ثلاثة أنواع من السلوك الداخلي:
entry / action()— يُنفَّذ مرة واحدة عند دخول الحالة، بصرف النظر عن الانتقال الذي أحضر الآلة إليها.do / activity()— نشاط مستمر يعمل طوال بقاء الآلة في الحالة ويتوقف تلقائيًا حين تُغادَر.exit / action()— يُنفَّذ مرة واحدة عند مغادرة الحالة، بصرف النظر عن الانتقال المسبِّب للخروج.
بالنسبة للحالة المركّبة، تُطبَّق هذه السلوكيات على عبور الحدود لا على الانتقالات الداخلية. فحين ينتقل الطلب من Payment Verification إلى Warehouse Picking في المثال أعلاه، لا يُنفَّذ إجراء الدخول ولا الخروج للحالة المركّبة — لأن الآلة لا تزال داخل Processing. فقط حين يعبر انتقال ما الحدود الخارجية يعمل releaseInventory().
حالات التاريخ الزائفة
تخيل عضوًا في مكتبة كان في منتصف عملية تجديد كتاب عبر الإنترنت. انتهت مهلة الجلسة وأُعيد توجيهه إلى حالة تسجيل الدخول. حين يسجّل دخوله مجددًا، هل يجب على النظام إعادة تشغيل سير عمل التجديد من البداية، أم استئنافه من حيث توقف العضو تحديدًا؟
يُجيب UML عن هذا بـحالة التاريخ الزائفة. ترسم على هيئة دائرة صغيرة تحتوي الحرف H داخل الحالة المركّبة. حين يستهدف انتقال ما حالة التاريخ الزائفة بدلًا من الحالة المركّبة ذاتها، تُعاد الآلة إلى الحالة الفرعية التي كانت نشطة آخر مرة خرجت فيها من الحالة المركّبة. إذا لم تُدخَل الحالة المركّبة من قبل، تتبع الآلة انتقالًا افتراضيًا محددًا.
ثمة نوعان:
- التاريخ الضحل (H) — يتذكر فقط الحالة الفرعية المباشرة على مستوى الحالة المركّبة. تُدخَل الحالات المركّبة الداخلية ضمن تلك الحالة الفرعية عبر حالتها الزائفة الأولية الخاصة.
- التاريخ العميق (H*) — يتذكر التكوين النشط الدقيق على جميع مستويات التداخل، مستأنفًا الحالة الفرعية الأعمق نشاطًا.
H* فقط حين يستلزم المتطلب التجاري صراحةً الاستئناف على المستوى الأعمق. يغطي التاريخ الضحل غالبية سيناريوهات الاستئناف في الواقع وهو أسهل بكثير على أصحاب المصلحة في الفهم.
مثال 2 — سير عمل تجديد المكتبة مع التاريخ
يُنمذج المخطط أدناه كشك خدمة ذاتية في مكتبة. للآلة ثلاث حالات أعلى مستوى: Idle وSession (مركّبة) وTimeout. داخل Session يتقدم العضو عبر Authenticate وSelect Book وRenew. حدث timeout يخرج من Session ويدفع الآلة إلى Timeout. حين يمرر العضو بطاقته مجددًا (cardSwiped)، تستهدف الآلة حالة التاريخ الضحل (H) داخل Session، مستأنفةً الحالة الفرعية التي كانت نشطة آخر مرة.
قراءة مخططات الحالة المركّبة بعين المحلل
حين تواجه مخطط حالة مركّبة في مواصفة أو مراجعة، اطرح هذه الأسئلة بمنهجية:
- ما المفهوم التجاري الذي تمثله كل حالة مركّبة؟ يجب أن تتوافق مع مرحلة يمكن التعرف عليها — "الطلب قيد التنفيذ"، "الجلسة نشطة"، "المطالبة قيد المراجعة".
- ما الأحداث التي يمكنها الخروج من الحالة المركّبة بغض النظر عن الموضع الداخلي؟ هذه هي مسارات الاستثناء (إلغاء، انتهاء مهلة، تصعيد). تأكد من توثيق كل منها في المتطلبات.
- ما الذي تمثله إجراءات الدخول والخروج بالمصطلحات التجارية؟ كل منها يجب أن يقابل إجراءً نظاميًا ملموسًا — إرسال إشعار أو تسجيل سجل أو تحرير مورد.
- هل ثمة متطلب استئناف؟ إذا قال أصحاب المصلحة "استئناف من حيث توقف المستخدم"، فهذه حالة تاريخ زائفة. وضّح ما إذا كان التاريخ الضحل أم العميق يطابق السلوك المتوقع.
الخلاصة
- تضع الحالة المركّبة آلةً فرعية كاملة (حالة زائفة أولية وحالات فرعية وانتقالات داخلية) داخل مستطيل مدوّر الزوايا، مما يتيح لك تنظيم السلوك المعقد بشكل هرمي.
- تُنفَّذ إجراءات الدخول في كل مرة تُعبَر فيها حدود الحالة المركّبة للداخل؛ وتُنفَّذ إجراءات الخروج في كل مرة تُغادَر — بغض النظر عن المسار الداخلي المتخذ.
- انتقال الخروج على حدود الحالة المركّبة يُطلَق من أي حالة فرعية، ويلتقط مسارات الاستثناء (إلغاء، انتهاء مهلة) دون تكرار الانتقالات.
- تستأنف الحالة الزائفة H (التاريخ الضحل) آخر حالة فرعية مباشرة كانت نشطة عند إعادة الدخول؛ أما H* (التاريخ العميق) فيستأنف أعمق تكوين نشط.
- حالات التاريخ الزائفة تُنمذج متطلبات "الاستئناف من حيث توقفت" — الشائعة في إدارة الجلسات وسير العمل متعدد الخطوات والمهام المنقطعة.