يتم استخدام نموذج الآلة المحدودة (FSM) في كتابة التعليمات البرمجية لمجموعة متنوعة من المنصات ، بما في ذلك Android. يسمح لك بجعل الرمز أقل إرهاقًا ، ويتناسب تمامًا مع نموذج Model-View-Presenter (MVP) ويخضع نفسه للاختبار البسيط. أخبر المطور فلاديسلاف كوزنتسوف حزب Droid كيف يساعد هذا النموذج في تطوير تطبيق Yandex.Disk.
- أولاً ، لنتحدث عن النظرية. أعتقد أن كل واحد منكم قد سمع عن MVP وجهاز الدولة ، لكننا سنكرر ذلك.

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

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

نطاق آلة الدولة كبير جدًا. لكل عنصر يتم استخدامها وتطبيقها بنجاح.

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

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

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

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

نتحقق من بعض الحالات ، ونتحقق من أننا نحسب ونرسم قابس "انتظر".

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

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


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

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

لا يوجد شيء مخادع هنا ، الشريحة التالية أكثر أهمية.

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

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

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

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

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

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

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


هناك طريقة إصدار ، وعادة ما يتم استدعاؤها في onDestroy ، ويمكنك فصل الموارد وإصدارها بشكل إضافي.

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

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


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

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

أرى العديد من التحسينات المحتملة. لا يعجبني أننا مضطرون إلى إلقاء أيدينا حول هذه الأساليب ، مثل onStart و on Stop و onCreate و onSave والمزيد. يمكنك الارتباط بـ Lifecycle ، ولكن من غير الواضح ما يجب فعله مع saveState. هناك فكرة ، على سبيل المثال ، لجعل جزء مقدم. لما لا؟ جزء بدون واجهة مستخدم يمسك دورة الحياة ، وبشكل عام لن نحتاج إلى أي شيء ، كل شيء سيطير إلينا بمفرده.
نقطة أخرى مثيرة للاهتمام: يتم إعادة إنشاء هذا المقدم في كل مرة ، وإذا كان لديك بيانات كبيرة مخزنة في مقدم العرض ، فانتقلت إلى قاعدة البيانات ، واحتفظت بمؤشر ضخم ، فمن غير المقبول أن تطلب في كل مرة تقوم فيها بتدوير الشاشة. لذلك ، يمكنك تخزين مقدم العرض مؤقتًا ، كما يفعل ، على سبيل المثال ، ViewModule من مكونات الهندسة المعمارية ، وجعل بعض الأجزاء التي ستحتفظ بذاكرة التخزين المؤقت للعارضين وإعادتها لكل طريقة عرض.
يمكنك استخدام الطريقة الجدولية لتحديد آلات الحالة ، لأن نمط الحالة الذي نستخدمه له عيب كبير: بمجرد أن تحتاج إلى إضافة طريقة واحدة إلى حدث جديد ، يجب عليك إضافة التنفيذ إلى جميع الأحفاد. فارغة على الأقل. أو تفعل ذلك في حالة أساسية. هذه ليست مريحة للغاية. لذلك يتم استخدام الطريقة الجدولية لتحديد آلات الحالة في جميع المكتبات - إذا كنت تبحث في GitHub عن كلمة FSM ، فستجد عددًا كبيرًا من المكتبات التي توفر لك نوعًا من الباني حيث يمكنك تعيين الحالة الأولية والحدث والحالة النهائية. توسيع وصيانة آلة الدولة هذه أسهل بكثير.
نقطة أخرى مثيرة للاهتمام: إذا كنت تستخدم نمط الحالة ، إذا بدأت آلة الحالة الخاصة بك في النمو ، فعلى الأرجح سيكون عليك التعامل مع بعض الأحداث بنفس الطريقة حتى لا يتم نسخ الرمز ، فإنك تنشئ حالة أساسية. كلما زادت الأحداث ، كلما بدأت الظروف الأساسية في الظهور ، ازداد التسلسل الهرمي ، وحدث خطأ ما.
كما نعلم ، يجب استبدال الميراث بتفويض ، وتساعد أجهزة الحالة الهرمية في حل هذه المشكلة. لديك حالات لا تعتمد على مستوى الميراث - فقط قم ببناء شجرة من الدول التي تمر بالمعالج أعلاه. يمكنك أيضًا القراءة بشكل منفصل ، وهو أمر مفيد جدًا. في Android ، على سبيل المثال ، يتم استخدام أجهزة الحالة الهرمية في WatchDog Wi-Fi ، التي تراقب حالة الشبكة ، فهي موجودة ، في مصدر Android.

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

يمكنك قفل بعض الطرق باستخدام دالة تُرجع الحجم ، وتستدعي حدثًا آخر بعد onEnter وترى كيف تستجيب حالة معينة لأحداث معينة. في هذه الحالة ، عندما يقع الحدث filesSizeUpdated وعندما يكون AllFilesSize أكبر من الصفر ، يجب أن ننتقل إلى حالة CleanAllFiles الجديدة. بمساعدة التخطيط ، نتحقق من كل هذا.

والأخير - يمكننا اختبار النظام بأكمله. نقوم ببناء الدولة ، وإرسال حدث إليها والتحقق من سلوك النظام. لدينا ثلاث مراحل من الاختبار. , UI, , , , .
, 70%. 80% . , .

, ? — . - .
. . - , , - , — , .
- , , , . , , . , , . - , , . , , . lock . - , .
— . , , , , . , - , , -, , . , . , .