أجهزة الكمبيوتر شبابيك إنترنت

ولاية. فئات الدولة لكل ولاية

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

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

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

مزيد من التفاصيل حول مثال موضوع F# جعل الدول غير الشرعية غير قابلة للتمثيلتم الكشف عنه على موقع سكوت فلاشين.

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

بنية تحتية

فئة عامة StateAttribute: سمة (نوع عام StateType ( get; ) public StateAttribute(TypestateType) ( StateType =stateType ?? throw new ArgumentNullException(nameof(stateType)); ) ) حالة فئة مجردة عامة حيث T: فئة (حالة محمية (كيان T) ( كيان = كيان ؟؟ رمي ArgumentNullException(nameof(entity)); ) محمية T Entity ( get; ) ) فئة ثابتة عامة StateCodeExtensions ( حالة ثابتة عامة إلى الدولة (رمز حالة التعداد هذا، كيان الكائن) حيث T: الفئة // نعم، نعم الانعكاس بطيء. استبدله بشجرة التعبير المترجمة // أو IL Emit وسيكون سريعًا => (State ) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute) ().نوع الحالة، الكيان)؛ )

موضوع النقاش

لنعلن عن كيان "عربة التسوق":

الواجهة العامة IHasState حيث Tentity: class ( TStateCode StateCode ( get; ) State الحالة ( get; ) ) فئة عامة جزئية سلة التسوق: IHasState (مستخدم مستخدم عام (احصل على؛ مجموعة محمية؛) رمز حالة CartStateCode العام (احصل على؛ مجموعة محمية؛) حالة عامة الحالة => StateCode.ToState (هذا)؛ المجموع العشري العام ( get; protected set; ) ICollection الظاهري المحمي المنتجات (احصل على؛ مجموعة؛) = قائمة جديدة ()؛ // ORM فقط المحمي Cart() ( ) public Cart(User user) ( User = user ?? throw new ArgumentNullException(nameof(user)); StateCode = StateCode = CartStateCode.Empty; ) public Cart(User user, IEnumerable المنتجات): this(user) (StateCode = StateCode = CartStateCode.Empty; foreach (var المنتج في المنتجات) ( Products.Add(product); ) ) public Cart(User user, IEnumerable المنتجات، الإجمالي العشري): هذا (المستخدم، المنتجات) (إذا (الإجمالي<= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
سننفذ فئة واحدة لكل حالة سلة التسوق: فارغة ونشطة ومدفوعة، لكننا لن نعلن عن واجهة مشتركة. دع كل دولة تنفذ السلوك المناسب فقط. هذا لا يعني أن فئات EmptyCartState وActiveCartState وPaidCartState لا يمكنها جميعًا تنفيذ نفس الواجهة. يمكنهم ذلك، ولكن يجب أن تحتوي هذه الواجهة فقط على الأساليب المتوفرة في كل ولاية. في حالتنا، تتوفر طريقة Add في EmptyCartState وActiveCartState، حتى نتمكن من وراثتها من AddableCartStateBase الملخص. ومع ذلك، يمكنك فقط إضافة العناصر إلى سلة التسوق غير المدفوعة، لذلك لن تكون هناك واجهة مشتركة لجميع الولايات. بهذه الطريقة نضمن عدم وجود InvalidOperationException في الكود الخاص بنا في وقت الترجمة.

سلة التسوق العامة الجزئية (التعداد العام CartStateCode: بايت (فارغ، نشط، مدفوع) الواجهة العامة IAddableCartState ( ActiveCartState Add(منتج المنتج); IEnumerable المنتجات (احصل على؛)) الواجهة العامة INotEmptyCartState (IEnumerable المنتجات ( get; ) العدد العشري الإجمالي ( get; ) ) فئة مجردة عامة AddableCartState: State ، IAddableCartState ( AddableCartState المحمي (كيان سلة التسوق): قاعدة (الكيان) ( ) ActiveCartState العام Add(منتج المنتج) ( Entity.Products.Add(product); Entity.StateCode = CartStateCode.Active; return (ActiveCartState)Entity.State;) IEnumerable العامة المنتجات => الكيان. المنتجات؛ ) فئة عامة EmptyCartState: AddableCartState ( عامة EmptyCartState(كيان سلة التسوق): قاعدة (الكيان) ( ) ) فئة عامة ActiveCartState: AddableCartState، INotEmptyCartState ( ActiveCartState عامة (كيان عربة التسوق): قاعدة (كيان) ( ) دفع PaidCartState العام (الإجمالي العشري) ( Entity.Total = الإجمالي؛ Entity.StateCode = CartStateCode.Paid؛ إرجاع (PaidCartState)Entity.State؛) الحالة العامة Remove(منتج المنتج) ( Entity.Products.Remove(product); if(!Entity.Products.Any()) ( Entity.StateCode = CartStateCode.Empty; ) إرجاع Entity.State; ) عام EmptyCartState Clear() ( Entity. Products.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State;) العدد العشري العام الإجمالي => Products.Sum(x => x.Price); ) الطبقة العامة PaidCartState: الحالة ، INotEmptyCartState ( IEnumerable العام المنتجات => الكيان. المنتجات؛ العدد العشري العام Total => Entity.Total; PaidCartState العامة (كيان سلة التسوق): القاعدة (الكيان) ( ) ) )
يتم الإعلان عن الدول المتداخلة ( متداخلة) الطبقات ليست عرضية. تتمتع الفئات المتداخلة بإمكانية الوصول إلى الأعضاء المحميين في فئة Cart، مما يعني أنه لا يتعين علينا التضحية بتغليف الكيان لتنفيذ السلوك. لتجنب الفوضى في ملف فئة الكيان، قمت بتقسيم الإعلان إلى قسمين: Cart.cs وCartStates.cs باستخدام الكلمة الأساسية الجزئية.

الإجراء العام GetViewResult(State cartState) (التبديل (cartState) (حالة Cart.ActiveCartState activeState: return View("Active"، activeState)؛ case Cart.EmptyCartStateemptyState: return View("Empty"،emptState);case Cart.PaidCartStatePaidCartState: return View(" Paid"،PaidCartState)؛ الافتراضي: رمي InvalidOperationException(); ) )
اعتمادا على حالة العربة، سوف نستخدم طرق عرض مختلفة. بالنسبة لعربة التسوق الفارغة، سنعرض الرسالة "عربة التسوق فارغة". ستحتوي عربة التسوق النشطة على قائمة المنتجات، وإمكانية تغيير عدد المنتجات وإزالة بعضها، وزر "تقديم طلب" وإجمالي مبلغ الشراء.

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

خاتمة

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

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

هذا موضوع واسع جدًا، لذا لإبقاء هذا الفصل قصيرًا قدر الإمكان، سأترك بعض الأمثلة الواضحة على التعليمات البرمجية وسيتعين عليك ملء بعض الثغرات بنفسك. آمل أن هذا لا يجعلهم أقل قابلية للفهم.

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

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

كلنا كنا هناك

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

بطلة باطلة::handleInput(إدخال الإدخال) ( if (input == PRESS_B) ( yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) )

لاحظت وجود خطأ؟

لا يوجد كود هنا لمنع "القفز في الهواء"؛ استمر في الضغط على B أثناء وجودها في الهواء وسوف تطير للأعلى مرارًا وتكرارًا. أسهل طريقة لحل هذه المشكلة هي إضافة علامة منطقية هيJumping_ إلى Heroine، والتي ستتتبع متى قفزت البطلة:

void Heroine::handleInput(Input input) ( if (input == PRESS_B) ( if (!isJumping_) ( isJumping_ = true ; // Jump... ) ) )

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

بطلة باطلة::handleInput(إدخال الإدخال) (إذا (الإدخال == PRESS_B) ( // دعنا نقفز إذا لم نكن قد فعلنا ذلك بالفعل...) else if (input == PRESS_DOWN) ( if (!isJumping_) ( setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( setGraphics(IMAGE_STAND); ) )

هل لاحظت خطأ هنا؟

باستخدام هذا الرمز يمكن للاعب:

  1. اضغط لأسفل إلى القرفصاء.
  2. اضغط على B للقفز من وضعية الجلوس.
  3. الافراج عن أسفل أثناء وجوده في الهواء.

في الوقت نفسه، ستتحول البطلة إلى رسومات الوقوف مباشرة في الهواء. سيتعين علينا إضافة علم آخر ...

باطلة Heroine::handleInput(Input input) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) ) وإلا إذا (input == RELEASE_DOWN) ( if (isDucking_) ( isDucking_ = false ; setGraphics(IMAGE_STAND); ) ) )

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

باطلة Heroine::handleInput(Input input) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) else ( isJumping_ = false ; setGraphics(IMAGE_DIVE); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( // Standing... ) ) )

أبحث عن الأخطاء مرة أخرى. وجدت ذلك؟

لقد قمنا بفحص للتأكد من أنه من المستحيل القفز في الهواء، ولكن ليس أثناء التدخل. إضافة علم آخر...

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

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

إن الحالات المتفرعة والمتغيرة المعقدة هي بالضبط أنواع التعليمات البرمجية التي يجب عليك تجنبها.

آلات الحالة المحدودة هي خلاصنا

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

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

خلاصة القول هي:

    لدينا مجموعة ثابتة تنص علىوالتي قد تحتوي على مدفع رشاش.في مثالنا، هذه هي الوقوف، والقفز، والانحناء، والتدحرج.

    يمكن أن يكون الجهاز فقط في واحدالدولة في أي وقت من الأوقات.بطلتنا لا تستطيع القفز والوقوف في نفس الوقت. في الواقع، يتم استخدام ولايات ميكرونيزيا الموحدة في المقام الأول لمنع ذلك.

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

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

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

في أنقى صورها، هذه هي الموزة بأكملها: الحالات والمدخلات والانتقالات. يمكنك تصويرهم في شكل مخطط كتلة. ولسوء الحظ، فإن المترجم لن يفهم مثل هذه الشخبطة. فكيف إذن ينفذآلة الدولة المحدودة؟ تقدم The Gang of Four نسختها الخاصة، لكننا سنبدأ بنسخة أبسط.

تشبيه FSM المفضل لدي هو البحث النصي القديم Zork. لديك عالم يتكون من غرف متصلة بواسطة الممرات. ويمكنك استكشافها عن طريق إدخال أوامر مثل "الذهاب شمالًا".

تتوافق هذه الخريطة تمامًا مع تعريف آلة الحالة المحدودة. الغرفة التي أنت فيها هي الحالة الحالية. كل خروج من الغرفة هو مرحلة انتقالية. أوامر التنقل - الإدخال.

التعدادات والمفاتيح

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

في حالتنا، باستخدام التعداد يمكننا وصف جميع حالات ولايات ميكرونيزيا الموحدة الخاصة بنا بشكل كامل بهذه الطريقة:

حالة التعداد ( STATE_STANDING، STATE_JUMPING، STATE_DUCKING، STATE_DIVING)؛

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

باطلة Heroine::handleInput(Input input) (التبديل (state_) (الحالة STATE_STANDING: إذا (input == PRESS_B) (state_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP);) وإلا إذا (input == PRESS_DOWN) (state_ = STATE_DUCKING; setGraphics(IMAGE_DUCK); ) فاصل; الحالة STATE_JUMPING: إذا (الإدخال == PRESS_DOWN) (state_ = STATE_DIVING; setGraphics(IMAGE_DIVE);) فاصل; الحالة STATE_DUCKING: إذا (الإدخال == RELEASE_DOWN) (state_ = STATE_STANDING; setGraphics (IMAGE_STAND)؛ ) استراحة ; ) )

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

الآن لن تتمكن البطلة من الدخول غير مؤكدحالة. عند استخدام الأعلام المنطقية، كانت بعض المجموعات ممكنة، لكنها لم تكن منطقية. عند استخدام التعداد جميع القيم صحيحة.

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

أضف حقل ChargeTime_ إلى Heroine لتخزين وقت الشحن. لنفترض أن لدينا بالفعل طريقة تحديث () يتم استدعاؤها في كل إطار. ولنضيف إليها الكود التالي:

بطلة باطلة::update() ( if (state_ == STATE_DUCKING) ( ChargeTime_++; if (chargeTime_ > MAX_CHARGE) ( superBomb(); ) ) )

إذا خمنت نمط طريقة التحديث، فقد فزت بالجائزة!

في كل مرة نجلس فيها مرة أخرى، نحتاج إلى إعادة ضبط هذا المؤقت. للقيام بذلك نحن بحاجة إلى تغيير HandleInput() :

باطلة Heroine::handleInput(Input input) (التبديل (state_) (الحالة STATE_STANDING: إذا (input == PRESS_DOWN) (state_ = STATE_DUCKING; ChargeTime_ = 0; setGraphics(IMAGE_DUCK);) // معالجة المدخلات المتبقية...استراحة ؛ // دول أخرى... } }

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

حالة القالب

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

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

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

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

الأمر ليس واضحًا جدًا. في النهاية، يتأقلم Switch مع هذا أيضًا. بالنسبة لمثالنا مع البطلة، سيبدو القالب كما يلي:

واجهة الحالة

أولاً، دعونا نحدد واجهة للحالة. كل جزء من السلوك المعتمد على الدولة - أي. كل ما قمنا بتنفيذه مسبقًا باستخدام المحول يتحول إلى طريقة افتراضية لهذه الواجهة. في حالتنا، هما HandleInput() وupdate() .

فئة HeroineState (عامة: افتراضية ~HeroineState() () مقبض الفراغ الظاهريInput{} {} };

دروس لكل ولاية

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

فئة DuckingState: HeroineState العامة (عامة: DuckingState(): ChargeTime_(0) () مقبض الفراغ الظاهريInput (البطلة والبطلة، إدخال الإدخال)(إذا (الإدخال == RELEASE_DOWN) ( // الانتقال إلى الحالة الدائمة... Heroine.setGraphics(IMAGE_STAND); )) تحديث الفراغ الظاهري (البطلة والبطلة)( ChargeTime_++; إذا (chargeTime_ > MAX_CHARGE) (herine.superBomb(); ) ) خاص : int ChargeTime_; );

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

الوفد إلى الدولة

بطلة الطبقة (عام : مقبض الفراغ الظاهري (إدخال الإدخال)(state_->handleInput(*this, input);) تحديث الفراغ الظاهري ()(state_->تحديث(*هذا );) // أساليب أخرى...خاص: HeroineState*state_; );

"لتغيير الحالة" نحتاج فقط إلى جعل الحالة_ تشير إلى كائن HeroineState مختلف. وهذا ما يتكون منه نمط الدولة بالفعل.

يبدو مشابهًا تمامًا لقوالب إستراتيجية GoF ونوع الكائنات. في كل ثلاثة لدينا كائن رئيسي يفوض إلى العبد. والفرق هو غاية.

  • والغرض من الاستراتيجية هو انخفاض الاتصال(الفصل) بين الطبقة الرئيسية وسلوكها.
  • الغرض من كائن الكتابة هو إنشاء عدد من الكائنات التي تتصرف بنفس الطريقة من خلال مشاركة كائن نوع مشترك فيما بينها.
  • الغرض من الدولة هو تغيير سلوك الكائن الرئيسي عن طريق تغيير الكائن الذي تفوضه.

أين هي كائنات الدولة هذه؟

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

الدول الساكنة

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

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

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

إن المكان الذي تضع فيه المثيل الثابت متروك لك. العثور على المكان الذي سيكون مناسبا. دعونا نضع مثالنا في فئة أساسية. بلا سبب.

فئة HeroineState (عامة: حالة الوقوف الثابتة؛ حالة البطة الثابتة؛ حالة القفز الثابتة؛ حالة الغوص الثابتة؛ حالة الغوص الثابتة؛ حالة البطة الثابتة؛ حالة القفز الثابتة؛ حالة الغوص الثابتة؛ حالة القفز الثابتة؛ حالة الغوص الثابتة // بقية الكود... };

كل حقل من هذه الحقول الثابتة هو مثال لحالة تستخدمها اللعبة. لجعل البطلة تقفز، ستفعل الحالة الدائمة شيئًا مثل:

إذا (الإدخال == PRESS_B) (heroine.state_ = &HeroineState::jumping; Heroine.setGraphics(IMAGE_JUMP); )

حالات الدولة

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

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

بدلاً من ذلك، سنسمح لـ HandleInput() على HeroineState بإرجاع حالة جديدة اختياريًا. عندما يحدث هذا، ستقوم Heroine بإزالة الحالة القديمة واستبدالها بالحالة الجديدة، مثل هذا:

باطلة Heroine::handleInput(Input input) ( HeroineState*state =state_->handleInput(*this , input); if (state != NULL ) (حذفstate_;state_ =state;))

بهذه الطريقة لا نقوم بإزالة الحالة السابقة حتى نعود من طريقتنا. الآن، يمكن أن تنتقل الحالة الدائمة إلى حالة الغوص عن طريق إنشاء مثيل جديد:

HeroineState* StandingState::handleInput(Heroine&herine, Input input) ( if (input == PRESS_DOWN) ( // كود آخر... return new DuckingState(); ) // ابق في هذه الحالة. return NULL ; )

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

بالطبع، عند تخصيص الذاكرة للحالة ديناميكيًا، يجب أن تفكر في تجزئة الذاكرة المحتملة. يمكن أن يساعدك قالب تجمع الكائنات.

خطوات تسجيل الدخول والخروج

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

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

HeroineState* DuckingState::handleInput(Heroine&herine, Input input) ( if (input == RELEASE_DOWN) (herine.setGraphics(IMAGE_STAND); return new StandingState(); ) // كود آخر... )

ما نريده حقًا هو أن تتحكم كل دولة في رسوماتها الخاصة. يمكننا تحقيق ذلك عن طريق الإضافة إلى الدولة إجراء الإدخال (إجراء الدخول):

الحالة الدائمة للفئة: HeroineState العامة ( public : أدخل الفراغ الافتراضي (البطلة والبطلة)(herine.setGraphics(IMAGE_STAND);) // كود آخر... );

بالعودة إلى Heroine، نقوم بتعديل الكود للتأكد من أن تغيير الحالة يكون مصحوبًا باستدعاء وظيفة إجراء الإدخال للحالة الجديدة:

void Heroine::handleInput(Input input) ( HeroineState*state =state_->handleInput(*this , input); if (state != NULL ) (حذفstate_;state_ =state; // استدعاء إجراء الإدخال للحالة الجديدة. State_->أدخل(*هذا); ))

سيؤدي هذا إلى تبسيط كود DuckingState:

HeroineState* DuckingState::handleInput(Heroine&herine, Input input) ( if (input == RELEASE_DOWN) ( return new StandingState(); ) // رمز آخر... )

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

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

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

وماذا حققنا؟

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

تساعدك آلة الحالة على فك تشابك التعليمات البرمجية الخاصة بك بشكل جدي من خلال تنظيمها في بنية صارمة للغاية. كل ما لدينا هو مجموعة ثابتة من الحالات، وحالة حالية واحدة، وانتقالات مضمنة.

الإنسان الآلي المحدود ليس تورينج كاملاً. تصف نظرية الأتمتة الاكتمال من خلال سلسلة من النماذج المجردة، كل منها أكثر تعقيدًا من سابقتها. تعد آلة تورينج واحدة من أكثر الآلات تعبيراً.

"اكتمل تورينج" يعني نظامًا (عادةً ما يكون لغة برمجة) معبرًا بدرجة كافية لتنفيذ آلة تورينج. وهذا بدوره يعني أن جميع لغات تورينج الكاملة معبرة بشكل متساوٍ تقريبًا. ولايات ميكرونيزيا الموحدة ليست معبرة بما يكفي لدخول هذا النادي.

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

آلة الدولة التنافسية

قررنا إضافة قدرة بطلتنا على حمل السلاح. على الرغم من أنها مسلحة الآن، إلا أنها لا تزال قادرة على القيام بكل ما كان يمكنها القيام به من قبل: الركض، والقفز، والانحناء، وما إلى ذلك. ولكن الآن، أثناء قيامها بكل هذا، يمكنها أيضًا إطلاق النار من سلاح.

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

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

المشكلة هنا هي أننا نخلط بين جزأين من الدولة - ذلك يفعلوماذا في ذلك يحمل في يديه- في جهاز واحد. لنمذجة جميع المجموعات الممكنة، نحتاج إلى إنشاء حالة لكل منها الأزواج. الحل واضح: تحتاج إلى إنشاء جهازين منفصلين للحالة.

إذا أردنا أن نتوحد نحالات العمل و محالات ما نحتفظ به في أيدينا في آلة واحدة ذات حالة محدودة - نحتاجها ن × متنص على. إذا كان لدينا رشاشين، فسنحتاج ن + متنص على.

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

بطلة الطبقة ( // بقية الكود...خاص: HeroineState*state_; HeroineState* المعدات_; );

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

عندما تقوم البطلة بتفويض المدخلات إلى الحالات، فإنها تمرر الترجمة إلى كلا جهازي الحالة:

بطلة باطلة::handleInput(إدخال الإدخال) (state_->handleInput(*this , input); Equipment_->handleInput(*this , input);)

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

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

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

آلة الدولة الهرمية

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

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

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

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

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

في الواقع، إذا استخدمنا نمط الحالة الأصلي لتنفيذ ولايات ميكرونيزيا الموحدة، فيمكننا بالفعل استخدام وراثة الفئة لتنفيذ التسلسل الهرمي. دعونا نحدد فئة أساسية للفئة الفائقة:

فئة OnGroundState: HeroineState العامة ( public : مقبض الفراغ الظاهريInput (البطلة والبطلة، إدخال الإدخال)( if (input == PRESS_B) ( // Jump... ) else if (input == PRESS_DOWN) ( // Squat... ) ));

والآن سوف ترثها كل فئة فرعية:

فئة DuckingState: OnGroundState العامة (عامة: مقبض الفراغ الظاهريInput (البطلة والبطلة، إدخال الإدخال)(إذا (الإدخال == RELEASE_DOWN) (/انهض...) آخر ( // لم تتم معالجة الإدخال. ولذلك، فإننا نمررها إلى أعلى في التسلسل الهرمي. OnGroundState::handleInput(heroine, input); )) ));

وبطبيعة الحال، ليست هذه هي الطريقة الوحيدة لتنفيذ التسلسل الهرمي. ولكن، إذا لم تستخدم قالب Gang of Four State، فلن يعمل. بدلاً من ذلك، يمكنك وضع نموذج لتسلسل هرمي واضح للحالات الحالية والدول العظمى كومةالدول بدلا من دولة واحدة في الطبقة الرئيسية.

ستكون الحالة الحالية في الجزء العلوي من المكدس، وتحتها تكون حالتها العليا، ثم الحالة العليا لـ هذاالدول العظمى ، إلخ. وعندما تحتاج إلى تنفيذ سلوك خاص بالحالة، فإنك تبدأ من أعلى المكدس ثم تتجه نحو الأسفل حتى تتعامل الحالة معه. (وإذا لم تتم معالجتها، فأنت ببساطة تتجاهلها).

آلة أوتوماتيكية مع ذاكرة المجلة

هناك امتداد شائع آخر لأجهزة الحالة يستخدم أيضًا مكدس الحالة. هنا فقط يمثل المكدس مفهومًا مختلفًا تمامًا ويستخدم لحل مشكلات مختلفة.

المشكلة هي أن آلة الدولة ليس لديها مفهوم قصص. هل تعرف في أي ولاية أنت؟ أنت، لكن ليس لديك معلومات حول الحالة التي أنت فيها كان. وعليه، لا توجد طريقة سهلة للعودة إلى الحالة السابقة.

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

وبما أن هذا السلوك يتكرر بين حالات متعددة، فهذا هو المكان الذي يمكن فيه استخدام آلة الحالة الهرمية لإعادة استخدام التعليمات البرمجية.

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

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

ما نحتاجه حقًا هو القدرة على تخزين الحالة التي كنا عليها قبل إطلاق النار وتذكرها مرة أخرى بعد إطلاق النار. هنا مرة أخرى يمكن لنظرية الأوتوماتا أن تساعدنا. تسمى بنية البيانات المقابلة Pushdown Automaton.

حيث يكون لدينا في جهاز آلي محدود مؤشر واحد للحالة، في جهاز آلي مزود بذاكرة مخزنة، يوجد مؤشر واحد للحالة كومة. في ولايات ميكرونيزيا الموحدة، يحل الانتقال إلى حالة جديدة محل الحالة السابقة. يسمح لك الجهاز المزود بذاكرة للمجلات أيضًا بالقيام بذلك، ولكنه يضيف عمليتين إضافيتين:

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

    أنت تستطيع يستخرج (البوب) الحالة العليا من المكدس. تختفي الدولة ويصبح ما تحتها حالياً.

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

ما مدى فائدة هذه الأشياء حقًا؟

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

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

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

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

"نمطولاية" source.ru

الحالة هي نمط من سلوك الكائن الذي يحدد وظائف مختلفة اعتمادًا على الحالة الداخلية للكائن. موقع الويب مصدر الموقع الأصلي

الشروط، المهمة، الغرض

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

تحفيز

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

الفكرة الرئيسية لهذا النمط هي تقديم فئة مجردة TCPStateلتمثيل حالات الاتصال المختلفة. تعلن هذه الفئة عن واجهة مشتركة بين جميع الفئات التي تصف العاملين المختلفين. المصدر الأصلي.ru

حالة. في هذه الفئات الفرعية TCPStateيتم تنفيذ السلوك الخاص بالدولة. على سبيل المثال، في الفصول الدراسية تم إنشاء TCPEو TPCمغلقتم تنفيذ السلوك الخاص بالدولة مقررو مغلقعلى التوالى. موقع الويب المصدر الأصلي

original.ru

فصل اتصال TCPيخزن كائن حالة (مثيل لفئة فرعية TCPState) يمثل الحالة الحالية للاتصال، ويقوم بتفويض جميع الطلبات المعتمدة على الحالة إلى هذا الكائن. اتصال TCPيستخدم مثيله الخاص للفئة الفرعية TCPStateبسيط للغاية: طرق الاتصال لواجهة واحدة TCPState، اعتمادًا فقط على الفئة الفرعية المحددة المخزنة حاليًا TCPState-أ - النتيجة مختلفة، أي. في الواقع، يتم تنفيذ العمليات الخاصة بحالة الاتصال هذه فقط. المصدر original.ru

وفي كل مرة تتغير حالة الاتصالاتصال TCPيغير كائن حالته. على سبيل المثال، عند إغلاق الاتصال القائم، اتصال TCPيستبدل مثيل فئة تم إنشاء TCPEينسخ TPCمغلق. موقع الموقع المصدر الأصلي

علامات التطبيق واستخدام نمط الدولة

استخدم نمط الحالة في الحالات التالية: source.ru
  1. عندما يعتمد سلوك الكائن على حالته ويجب أن يتغير في وقت التشغيل. المصدر original.ru
  2. عندما يحتوي رمز العملية على عبارات شرطية تتكون من العديد من الفروع، حيث يعتمد اختيار الفرع على الحالة. عادة في هذه الحالة يتم تمثيل الحالة بالثوابت المذكورة. غالبًا ما يتم تكرار نفس بنية العبارة الشرطية في العديد من العمليات، ويقترح نمط الحالة وضع كل فرع في فئة منفصلة. يتيح لك هذا التعامل مع حالة الكائن ككائن مستقل يمكن أن يتغير بشكل مستقل عن الآخرين. موقع مصدر الموقع الأصلي

حل

موقع الويب المصدر الأصلي

source.ru

المشاركون في نمط الدولة

source.ru
  1. سياق(TCPConnection) - السياق.
    يحدد واجهة واحدة للعملاء.
    يخزن مثيل فئة فرعية حالة الخرسانةالذي يحدد الوضع الحالي. com.codelab.
  2. ولاية(TCPState) - الحالة.
    يحدد واجهة لتغليف السلوك المرتبط بحالة سياق معينة. موقع مصدر الموقع الأصلي
  3. الفئات الفرعية حالة الخرسانة(TCPEstablished، TCPListen، TCPClosed) - حالة محددة.
    تقوم كل فئة فرعية بتنفيذ السلوك المرتبط ببعض حالات السياق سياق. موقع الموقع المصدر الأصلي

مخطط لاستخدام نمط الدولة

فصل سياقتفويض الطلبات إلى الكائن الحالي حالة الخرسانة. موقع الموقع المصدر الأصلي

يمكن للسياق تمرير نفسه كوسيطة إلى كائن ولاية، والتي ستقوم بمعالجة الطلب. هذا يسمح لكائن الحالة ( حالة الخرسانة) الوصول إلى السياق إذا لزم الأمر. com.codelab.

سياق- هذه هي الواجهة الرئيسية للعملاء. يمكن للعملاء تكوين السياق باستخدام كائنات الحالة ولاية(أكثر دقة حالة الخرسانة). بمجرد تكوين السياق، لم يعد العملاء بحاجة إلى التواصل مباشرة مع كائنات الحالة (فقط من خلال الواجهة المشتركة ولاية). source.ru

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

أسئلة بخصوص تنفيذ نمط الدولة

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

    المصدر original.ru
  2. بديل جدولي.
    هناك طريقة أخرى لتنظيم التعليمات البرمجية المستندة إلى الحالة. هذا هو مبدأ آلة الحالة المحدودة. يستخدم جدولًا لتعيين المدخلات لانتقالات الحالة. وبمساعدتها، يمكنك تحديد الحالة التي تحتاج إلى الانتقال إليها عند وصول بيانات إدخال معينة. في الأساس، نقوم باستبدال الكود الشرطي ببحث في الجدول.
    الميزة الرئيسية للجهاز هي انتظامه: لتغيير معايير الانتقال، يكفي تعديل البيانات فقط، وليس التعليمات البرمجية. ولكن هناك أيضًا عيوب:
    - غالبًا ما يكون البحث في الجدول أقل كفاءة من استدعاء دالة،
    - تقديم منطق الانتقال في شكل جدولي موحد يجعل المعايير أقل وضوحا، وبالتالي أكثر صعوبة في الفهم،
    - عادة ما يكون من الصعب إضافة الإجراءات المصاحبة للتحولات بين الدول. تأخذ الطريقة الجدولية في الاعتبار الحالات والانتقالات بينها، ولكنها تحتاج إلى استكمالها حتى يمكن إجراء حسابات عشوائية مع كل تغيير في الحالة.
    يمكن صياغة الفرق الرئيسي بين أجهزة الحالة المستندة إلى الجدول وحالة النمط على النحو التالي: نموذج حالة النمط للسلوك المعتمد على الحالة، وتركز طريقة الجدول على تحديد التحولات بين الحالات. original.ru

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

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

    source.ru الأصلي
  4. باستخدام التغيير الديناميكي.
    من الممكن تغيير السلوك حسب الطلب عن طريق تغيير فئة الكائن في وقت التشغيل، ولكن معظم اللغات الموجهة للكائنات لا تدعم ذلك. الاستثناء هو Perl وJavaScript واللغات الأخرى المستندة إلى محرك البرمجة النصية والتي توفر مثل هذه الآلية وبالتالي تدعم حالة النمط مباشرة. يسمح هذا للكائنات بتغيير سلوكها عن طريق تغيير رمز الفصل الخاص بها. source.ru

    .ru المصدر الأصلي

نتائج

نتائج الاستخدام حالة النمط: المصدر original.ru
  1. توطين السلوك المعتمد على الدولة.
    ويقسمها إلى أجزاء تقابل الولايات. يضع نمط الحالة كل السلوكيات المرتبطة بحالة معينة في كائن منفصل. لأن الكود المعتمد على الحالة موجود بالكامل في إحدى الفئات الفرعية للفئة ولاية، ثم يمكنك إضافة حالات وانتقالات جديدة ببساطة عن طريق إنتاج فئات فرعية جديدة.
    بدلا من ذلك، يمكن للمرء استخدام أعضاء البيانات لتحديد الحالات الداخلية، ثم عمليات الكائن سياقسوف تحقق من هذه البيانات. ولكن في هذه الحالة، ستكون العبارات الشرطية أو العبارات الفرعية المشابهة منتشرة في جميع أنحاء رمز الفئة سياق. ومع ذلك، فإن إضافة حالة جديدة يتطلب تغيير العديد من العمليات، مما يجعل الصيانة صعبة. يحل نمط الحالة هذه المشكلة، ولكنه يخلق أيضًا مشكلة أخرى، حيث ينتهي الأمر بتوزيع سلوك الحالات المختلفة بين عدة فئات فرعية ولاية. وهذا يزيد من عدد الفصول. بالطبع، فئة واحدة أكثر إحكاما، ولكن إذا كان هناك العديد من الحالات، فإن هذا التوزيع يكون أكثر كفاءة، وإلا فسيتعين على المرء أن يتعامل مع البيانات الشرطية المرهقة.
    إن وجود عبارات شرطية مرهقة أمر غير مرغوب فيه، كما هو الحال مع الإجراءات الطويلة. إنها متجانسة جدًا، ولهذا السبب يصبح تعديل الكود وتوسيعه مشكلة. يوفر نمط الحالة طريقة أفضل لتنظيم التعليمات البرمجية المعتمدة على الحالة. لم يعد المنطق الذي يصف انتقالات الحالة مغلفًا بعبارات متجانسة لوأو يُحوّلولكن موزعة بين الفئات الفرعية ولاية. من خلال تغليف كل انتقال وإجراء في فئة، تصبح الحالة كائنًا كاملاً. يؤدي ذلك إلى تحسين بنية الكود وجعل الغرض منه أكثر وضوحًا. original.ru
  2. يجعل التحولات بين الدول واضحة.
    إذا كان الكائن يحدد حالته الحالية فقط من حيث البيانات الداخلية، فإن التحولات بين الحالات ليس لها تمثيل صريح؛ تظهر فقط كتخصيصات لمتغيرات معينة. إن تقديم كائنات منفصلة لحالات مختلفة يجعل التحولات أكثر وضوحًا. وبالإضافة إلى ذلك، الكائنات ولايةيمكن أن تحمي السياق سياقمن عدم تطابق المتغيرات الداخلية، لأن التحولات من وجهة نظر السياق هي أفعال ذرية. لإجراء النقل، تحتاج إلى تغيير قيمة متغير واحد فقط (متغير الكائن ولايةفي الفصل سياق)، بدلاً من عدة. source.ru
  3. يمكن مشاركة كائنات الحالة.
    إذا كان في كائن الدولة ولايةلا توجد متغيرات مثيل، مما يعني أن الحالة التي تمثلها مشفرة فقط بواسطة النوع نفسه، ومن ثم يمكن لسياقات مختلفة مشاركة نفس الكائن ولاية. عندما يتم فصل الدول بهذه الطريقة، فهي في الأساس انتهازية (انظر النمط الانتهازي) وليس لديها دولة داخلية، بل سلوك فقط. موقع مصدر الموقع الأصلي

مثال

دعونا نلقي نظرة على تنفيذ المثال من القسم ""، أي. بناء بعض بنية اتصال TCP البسيطة. هذه نسخة مبسطة من بروتوكول TCP، وهي بالطبع لا تمثل البروتوكول بأكمله أو حتى جميع حالات اتصالات TCP. source.ru الأصلي

أولا وقبل كل شيء، دعونا نحدد الفئة اتصال TCP، والذي يوفر واجهة لنقل البيانات ويتعامل مع طلبات تغيير الحالة: TCPConnection. source.ru الأصلي

في متغير الأعضاء ولايةفصل اتصال TCPيتم تخزين مثيل للفئة TCPState. تقوم هذه الفئة بتكرار واجهة تغيير الحالة المحددة في الفئة اتصال TCP. المصدر original.ru

موقع الموقع المصدر الأصلي

اتصال TCPيفوض جميع الطلبات المعتمدة على الحالة إلى المثيل المخزن في الحالة TCPState. أيضا في الصف اتصال TCPهناك عملية دولة التغيير، والذي يمكنك من خلاله كتابة مؤشر إلى كائن آخر في هذا المتغير TCPState. منشئ الطبقة اتصال TCPتهيئة ولايةمؤشر إلى حالة مغلقة TPCمغلق(سنقوم بتعريفه أدناه). المصدر original.ru

source.ru الأصلي

كل عملية TCPStateيقبل المثال اتصال TCPكمعلمة، وبالتالي السماح للكائن TCPStateالوصول إلى بيانات الكائن اتصال TCPوتغيير حالة الاتصال. .ru

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

original.ru

في الفئات الفرعية TCPStateيتم تنفيذ السلوك المعتمد على الدولة. يمكن أن يكون اتصال TCP في العديد من الحالات: مقرر(المثبتة)، الاستماع(الاستماع)، مغلق(مغلق)، وما إلى ذلك، ولكل منهم فئة فرعية خاصة به TCPState. للتبسيط، سننظر بالتفصيل في 3 فئات فرعية فقط - تم إنشاء TCPE, TCPListenو TPCمغلق. موقع الويب المصدر الأصلي

مصدر رو

في الفئات الفرعية TCPStateينفذ السلوك المعتمد على الحالة لتلك الطلبات الصالحة في تلك الحالة. .ru

موقع الموقع المصدر الأصلي

بعد تنفيذ الإجراءات الخاصة بالدولة، يتم تنفيذ هذه العمليات المصدر original.ru

سبب دولة التغييرلتغيير حالة الكائن اتصال TCP. هو نفسه ليس لديه معلومات حول بروتوكول TCP. إنها فئات فرعية TCPStateتحديد التحولات بين الدول والإجراءات التي يمليها البروتوكول. original.ru

المصدر original.ru

التطبيقات المعروفة لنمط الدولة

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

لتعكس اعتماد سلوك المحرر على الأداة الحالية، يمكنك استخدام نمط الحالة. المصدر الأصلي.ru

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

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

original.ru

الغرض من نمط الدولة

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

المشكلة التي يتعين حلها

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

مناقشة نمط الدولة

يحل نمط الحالة هذه المشكلة على النحو التالي:

  • يقدم فئة السياق التي تحدد واجهة للعالم الخارجي.
  • يقدم فئة الدولة المجردة.
  • يمثل "الحالات" المختلفة لجهاز الحالة كفئات فرعية للحالة.
  • تحتوي فئة السياق على مؤشر للحالة الحالية التي تتغير عندما تتغير حالة آلة الحالة.

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

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

هيكل نمط الدولة

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

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

  • تسليم البضاعة للمشتري، لا يشترط التغيير.
  • إعطاء المشتري البضاعة والتغيير.
  • لن يحصل المشتري على البضائع بسبب عدم وجود المال الكافي.
  • لن يحصل المشتري على البضائع بسبب غيابها.

باستخدام نمط الدولة

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

ملامح نمط الدولة

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

تنفيذ نمط الدولة

دعونا نفكر في مثال لآلة الحالة المحدودة مع حالتين محتملتين وحدثين.

#يشمل استخدام اسم للمحطة؛ آلة الفئة (حالة الفئة * الحالية؛ العامة: آلة ()؛ باطلة setCurrent(State *s) (current = s;) void on(); void off(); ); حالة الفصل (عام: الفراغ الظاهري on(Machine *m) (cout<< " already ON\n"; } virtual void off(Machine *m) { cout << " already OFF\n"; } }; void Machine::on() { current->على هذا)؛ ) آلة باطلة::off() (current->off(this);) فئة ON: الحالة العامة ( public: ON() ( cout<< " ON-ctor "; }; ~ON() { cout << " dtor-ON\n"; }; void off(Machine *m); }; class OFF: public State { public: OFF() { cout << " OFF-ctor "; }; ~OFF() { cout << " dtor-OFF\n"; }; void on(Machine *m) { cout << " going from OFF to ON"; m->setCurrent(new ON()); امسح هذه؛ ))); باطلة ON::off(Machine *m) (cout<< " going from ON to OFF"; m->setCurrent(new OFF()); امسح هذه؛ ) Machine::Machine() (current = new OFF(); cout<< "\n"; } int main() { void(Machine:: *ptrs)() = { Machine::off, Machine::on }; Machine fsm; int num; while (1) { cout << "Enter 0/1: "; cin >> الأعداد؛ (fsm.*ptrs)(); ))

15.02.2016
21:30

نمط الحالة مخصص لتصميم الفئات التي لها حالات منطقية مستقلة متعددة. دعنا ننتقل مباشرة إلى المثال.

لنفترض أننا نقوم بتطوير فئة التحكم في كاميرا الويب. يمكن أن تكون الكاميرا في ثلاث حالات:

  1. لم يتم تهيئة. دعنا نسميها NotConnectedState؛
  2. تمت التهيئة وجاهزة للانطلاق، ولكن لم يتم التقاط أي إطارات حتى الآن. فليكن ReadyState؛
  3. وضع التقاط الإطار النشط. دعونا نشير إلى ActiveState .

وبما أننا نعمل مع نمط الحالة، فمن الأفضل أن نبدأ بصورة مخطط الحالة:

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

سأقدم على الفور القائمة الكاملة مع الحد الأدنى من التعليقات. سنناقش التفاصيل الأساسية لهذا التنفيذ بمزيد من التفاصيل أدناه.

#يشمل #define DECLARE_GET_INSTANCE(ClassName) \ static ClassName* getInstance() (\ static ClassName example;\ return \ ) class WebCamera ( public: typedef std::string Frame; public: // *********** ************************************** // الاستثناءات // ****** ******************************************** الطبقة غير مدعومة: الأمراض المنقولة جنسيا العامة: :استثناء ( )؛ عام: // ***************************************** ******* ********* // تنص على // ***************************** ******* ************** فئة NotConnectedState؛ فئة ReadyState؛ فئة ActiveState؛ فئة الحالة (عام: Virtual ~State() () اتصال باطل افتراضي (WebCamera*) ( رمي NotSupported ()؛) قطع الاتصال الافتراضي (WebCamera * cam) (std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->ChangeState(NotConnectedState::getInstance()); ) بداية باطلة افتراضية (WebCamera*) (رمي NotSupported();) توقف باطلة افتراضية(WebCamera*) (رمي NotSupported();) إطار افتراضي getFrame(WebCamera*) (رمي NotSupported();) محمي: State() ( ) ); //*************************************************** ** فئة NotConnectedState: الحالة العامة (عام: DECLARE_GET_INSTANCE(NotConnectedState) اتصال باطل (WebCamera* cam) (std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->ChangeState(ReadyState::getInstance()); ) قطع الاتصال باطل (WebCamera *) ( throw NotSupported(); ) خاص: NotConnectedState() ( )); //*************************************************** ** فئة ReadyState: الحالة العامة ( public: DECLARE_GET_INSTANCE(ReadyState) بداية باطلة (WebCamera* cam) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->ChangeState(ActiveState::getInstance()); ) خاص: ReadyState() ( )); //*************************************************** ** فئة ActiveState: الحالة العامة ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam-> << "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } }; public: explicit WebCamera(int camID) : m_camID(camID), m_state(NotConnectedState::getInstance()) { } ~WebCamera() { try { disconnect(); } catch(const NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } } void connect() { m_state->ربط (هذا)؛ ) باطل قطع الاتصال () ( m_state-> قطع الاتصال (هذا)؛ ) بداية باطلة () ( m_state-> بدء (هذا)؛ ) توقف باطل () ( m_state-> توقف (هذا)؛ ) إطار getFrame () (إرجاع m_state ->getFrame(this); ) خاص: void ChangeState(State* newState) ( m_state = newState; ) خاص: int m_camID; الدولة* m_state; );

ألفت انتباهك إلى الماكرو DECLARE_GET_INSTANCE. بالطبع، لا يُنصح باستخدام وحدات الماكرو في لغة C++. ومع ذلك، ينطبق هذا على الحالات التي يعمل فيها الماكرو كنظير لوظيفة القالب. في هذه الحالة، قم دائما بإعطاء الأفضلية للأخير.

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

نعلن عن فئات الحالة في الفئة الرئيسية - WebCamera. للإيجاز، استخدمت التعريفات المضمنة لوظائف الأعضاء من جميع الفئات. ومع ذلك، في التطبيقات الحقيقية، من الأفضل اتباع التوصيات المتعلقة بفصل الإعلان والتنفيذ إلى ملفات h وcpp.

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

أساس التسلسل الهرمي لفئات الحالة هو الفئة المجردة WebCamera::State:

حالة الفئة (عامة: Virtual ~State() () اتصال باطل افتراضي (WebCamera*) (رمي NotSupported();) قطع اتصال باطل افتراضي (WebCamera* cam) (std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->ChangeState(NotConnectedState::getInstance()); ) بداية باطلة افتراضية (WebCamera*) (رمي NotSupported();) توقف باطلة افتراضية(WebCamera*) (رمي NotSupported();) إطار افتراضي getFrame(WebCamera*) (رمي NotSupported();) محمي: State() ( ) );

تتوافق جميع وظائف أعضائها مع وظائف فئة WebCamera نفسها. يحدث التفويض المباشر:

كاميرا ويب فئة ( // ... اتصال باطل () ( m_state->connect(this); ) قطع اتصال باطل () ( m_state->disconnect(this); ) بداية باطلة () ( m_state->start(this); ) توقف باطلة () ( m_state->stop(this); ) إطار getFrame() ( إرجاع m_state->getFrame(this); ) // ... State* m_state; )

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

لا تفعل فئة WebCamera في حد ذاتها أي شيء تقريبًا. إنه يعتمد بشكل كامل على دوله. وهذه الدول بدورها تحدد شروط تنفيذ العمليات وتوفر السياق اللازم.

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

وفي الوقت نفسه، نقدم تطبيقًا افتراضيًا لـ WebCamera::State::disconnect(). وهذا السلوك مناسب لاثنتين من الحالات الثلاث. ونتيجة لذلك، فإننا نمنع تكرار التعليمات البرمجية.

لتغيير الحالة، استخدم وظيفة العضو الخاص WebCamera::changeState() :

تغيير حالة الفراغ (الدولة * newState) ( m_state = newState؛ )

الآن إلى تنفيذ دول محددة. بالنسبة إلى WebCamera::NotConnectedState، يكفي تجاوز عمليات الاتصال () وقطع الاتصال ():

فئة NotConnectedState: الحالة العامة (عام: DECLARE_GET_INSTANCE(NotConnectedState) اتصال باطل (WebCamera* cam) (std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->ChangeState(ReadyState::getInstance()); ) قطع الاتصال باطل (WebCamera *) ( throw NotSupported(); ) خاص: NotConnectedState() ( ));

لكل ولاية، يمكنك إنشاء مثيل واحد. وهذا مضمون لنا من خلال الإعلان عن منشئ خاص.

عنصر آخر مهم في التنفيذ المقدم هو أننا لا ننتقل إلى دولة جديدة إلا إذا نجحنا. على سبيل المثال، إذا حدث فشل أثناء تهيئة الكاميرا، فمن السابق لأوانه الدخول إلى حالة الاستعداد. الفكرة الرئيسية هي المراسلات الكاملة بين الحالة الفعلية للكاميرا (في حالتنا) وكائن الحالة.

إذن، الكاميرا جاهزة للانطلاق. لنقم بإنشاء فئة WebCamera::ReadyState State المقابلة:

حالة جاهزية الفئة: الحالة العامة (عام: DECLARE_GET_INSTANCE(ReadyState) بداية باطلة (WebCamera* cam) (std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->ChangeState(ActiveState::getInstance()); ) خاص: ReadyState() ( ));

من حالة الاستعداد يمكننا الدخول إلى حالة التقاط الإطار النشطة. لهذا الغرض، يتم توفير عملية start()، والتي قمنا بتنفيذها.

أخيرًا وصلنا إلى الحالة المنطقية الأخيرة للكاميرا، WebCamera::ActiveState:

Class ActiveState: الحالة العامة ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam->ChangeState(ReadyState::getInstance()); ) إطار getFrame(WebCamera*) ( std::cout<< "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } };

في هذه الحالة، يمكنك التوقف عن التقاط الإطارات باستخدام stop() . ونتيجة لذلك، سيتم إعادتنا إلى WebCamera::ReadyState. بالإضافة إلى ذلك، يمكننا استقبال الإطارات التي تتراكم في المخزن المؤقت للكاميرا. للتبسيط، نعني بكلمة "إطار" سلسلة عادية. في الواقع، سيكون هذا نوعًا من مصفوفة البايت.

يمكننا الآن كتابة مثال نموذجي للعمل مع فئة WebCamera الخاصة بنا:

Int main() ( WebCamera cam(0); حاول ( // cam في NotConnectedState cam.connect(); // cam في ReadyState cam.start(); // cam في ActiveState std::cout<< cam.getFrame() << std::endl; cam.stop(); // Можно было сразу вызвать disconnect() // cam в Состоянии ReadyState cam.disconnect(); // cam в Состоянии NotConnectedState } catch(const WebCamera::NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } return 0; }

هذا ما سيتم إخراجه إلى وحدة التحكم نتيجة لذلك:

تهيئة الكاميرا... ابدأ دفق الفيديو... احصل على الإطار الحالي... الإطار الحالي أوقف دفق الفيديو... قم بإلغاء تهيئة الكاميرا...

الآن دعونا نحاول إثارة الخطأ. لنستدعي Connect() مرتين على التوالي:

Int main() ( WebCamera cam(0); حاول ( // cam في NotConnectedState cam.connect(); // cam في ReadyState // لكن في هذه الحالة لم يتم توفير عملية الاتصال ()! cam.connect( ); // يرمي استثناء NotSupported ) Catch(const WebCamera::NotSupported& e) ( std::cout<< "Произошло исключение!!!" << std::endl; // ... } catch(...) { // Обрабатываем исключение } return 0; }

وهنا ما يخرج منه:

جارٍ تهيئة الكاميرا... حدث استثناء!!! فلنقم بإلغاء تعريف الكاميرا..

يرجى ملاحظة أن الكاميرا لا تزال غير مهيأة. حدث استدعاء قطع الاتصال () في أداة تدمير WebCamera. أولئك. تظل الحالة الداخلية للكائن صحيحة تمامًا.

الاستنتاجات

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