
على مدار العامين الماضيين ، قطع مطورو Android في Badoo مسارًا طويلاً وشائكًا من MVP إلى نهج مختلف تمامًا لهندسة التطبيقات. أريد
أنا و
Anublo مشاركة ترجمة لمقال من قبل زميلنا
Zsolt Kocsi ، يصف المشاكل التي واجهناها وحلها.
هذا هو الأول من عدة مقالات مخصصة لتطوير العمارة الحديثة MVI على Kotlin.
لنبدأ من البداية: مشاكل الدولة
في كل لحظة ، يكون للتطبيق حالة معينة تحدد سلوكه وما يراه المستخدم. إذا ركزت فقط على فئتين ، فإن هذه الحالة تتضمن جميع قيم المتغيرات - من الإشارات البسيطة إلى الكائنات الفردية. يعيش كل من هذه المتغيرات حياته الخاصة ويتم التحكم فيها بواسطة أجزاء مختلفة من التعليمات البرمجية. يمكنك تحديد الحالة الحالية للتطبيق فقط عن طريق التحقق من كل واحد تلو الآخر.
من خلال العمل على الشفرة ، نقوم بإنشاء نموذج موجود لعمل النظام في رؤوسنا. نحن ننفذ الحالات المثالية بسهولة عندما يسير كل شيء وفقًا للخطة ، لكننا غير قادرين تمامًا على حساب جميع المشاكل والظروف المحتملة للتطبيق. وعاجلاً أم آجلاً ، فإن أحد الشروط التي لم نتخيلها سيتجاوزنا ، وسنواجه خطأً.
مبدئيًا ، تتم كتابة الشفرة وفقًا لأفكارنا حول كيفية عمل النظام. ولكن في المستقبل ، من خلال
خمس مراحل من تصحيح الأخطاء ، يجب عليك إعادة كل شيء بشق الأنفس ، وتغيير نموذج النظام الذي تم إنشاؤه بالفعل والذي تطور في الرأس في نفس الوقت. يبقى أن نأمل أن نفهم عاجلاً أم آجلاً ما حدث من خطأ وسيتم إصلاح الخلل.
لكن هذا أبعد ما يكون عن المحظوظين دائما. كلما كان النظام أكثر تعقيدًا ، زاد احتمال مواجهة بعض الحالات غير المتوقعة ، والتي سيكون تصحيحها حلماً لفترة طويلة في الكوابيس.
في Badoo ، تكون جميع التطبيقات غير متزامنة إلى حد كبير - ليس فقط بسبب الوظائف الواسعة المتاحة للمستخدم من خلال واجهة المستخدم ، ولكن أيضًا بسبب إمكانية إرسال البيانات في اتجاه واحد من قبل الخادم. تتأثر حالة وسلوك التطبيق كثيرًا - من تغيير حالة الدفع إلى المطابقات الجديدة وطلبات التحقق.
ونتيجة لذلك ، صادفنا في وحدة الدردشة لدينا العديد من الأخطاء الغريبة التي يصعب إعادة إنتاجها والتي أفسدت الكثير من الدم للجميع. في بعض الأحيان تمكن المختبرون من كتابتها ، ولكن لم يتم تكرارها على جهاز المطور. بسبب الكود غير المتزامن ، كان التكرار الكامل لسلسلة من الأحداث غير مرجح للغاية. وبما أن التطبيق لم يتعطل ، لم يكن لدينا حتى أثر المكدس الذي يوضح مكان بدء البحث.
لم تكن الهندسة المعمارية النظيفة قادرة أيضًا على مساعدتنا. حتى بعد إعادة كتابة وحدة الدردشة ، كشفت اختبارات أ / ب عن اختلافات صغيرة ولكنها مهمة في عدد الرسائل من المستخدمين الذين يستخدمون الوحدات الجديدة والقديمة. قررنا أن هذا يرجع إلى صعوبة استنساخ البق وحالة السباق. استمر التناقض بعد التحقق من جميع العوامل الأخرى. عانت مصالح الشركة ، كان من الصعب على المطورين الحفاظ على التعليمات البرمجية.
لا يمكنك إصدار مكوّن جديد إذا كان يعمل بشكل أسوأ من المكون الحالي ، ولكن لا يمكنك إطلاقه أيضًا - نظرًا لأنه استغرق تحديثًا ، كان هناك سبب. لذا ، عليك أن تفهم لماذا ينخفض عدد الرسائل في نظام يبدو طبيعيًا تمامًا ولا يتعطل.
من أين تبدأ البحث؟
المفسد: هذا ليس خطأ العمارة النظيفة - كما هو الحال دائمًا ، فإن العامل البشري هو المسؤول. في النهاية ، بالطبع ، أصلحنا هذه الأخطاء ، لكننا أمضينا الكثير من الوقت والجهد في ذلك. ثم فكرنا: هل هناك طريقة أسهل لتجنب هذه المشاكل؟
الضوء في نهاية النفق ...
إن المصطلحات العصرية مثل Model-View-Intent و "تدفق البيانات أحادي الاتجاه" مألوفة لنا. إذا لم يكن هذا هو الحال في قضيتك ، أنصحك بالبحث عنها - فهناك العديد من المقالات حول هذه الموضوعات على الإنترنت. يوصي مطورو Android بشكل خاص
بمواد Hannes Dorfman المكونة من ثماني قطع .
بدأنا اللعب بهذه الأفكار المأخوذة من تطوير الويب في أوائل عام 2017. اتضح أن النهج مثل Flux و Redux مفيدة جدًا - فقد ساعدتنا في التعامل مع العديد من المشكلات.
بادئ ذي بدء ، من المفيد جدًا احتواء جميع عناصر الحالة (المتغيرات التي تؤثر على واجهة المستخدم وتؤدي إلى إجراءات مختلفة) في كائن واحد -
حالة . عندما يتم تخزين كل شيء في مكان واحد ، تكون الصورة الإجمالية مرئية بشكل أفضل. على سبيل المثال ، إذا كنت تريد أن تتخيل تحميل البيانات باستخدام هذا النهج ، فأنت بحاجة إلى حقول
الحمولة والحمولة . بالنظر إليها ، سترى متى يتم استلام البيانات (
الحمولة ) وما إذا كانت الرسوم المتحركة (
isLoading ) تظهر للمستخدم.
علاوة على ذلك ، إذا ابتعدنا عن تنفيذ التعليمات البرمجية المتوازية مع عمليات الاسترداد والتعبير عن التغييرات في حالة التطبيق كسلسلة من المعاملات ، فسوف نحصل على نقطة إدخال واحدة. نقدم لك
Reducer ، الذي جاء إلينا من البرمجة الوظيفية. يأخذ الحالة الحالية والبيانات المتعلقة بالمزيد من الإجراءات (
النية ) ويخلق حالة جديدة منها:
Reducer = (State, Intent) -> State
استمرارًا للمثال السابق مع تحميل البيانات ، نحصل على الإجراءات التالية:
- بدأ التحميل
- انتهى مع النجاح
ثم يمكنك إنشاء Reducer بالقواعد التالية:
- في حالة StartedLoading ، قم بإنشاء كائن حالة جديد عن طريق نسخ الكائن القديم وتعيين القيمة isLoading على true.
- في حالة FinishedWithSuccess ، قم بإنشاء كائن حالة جديد ، ونسخ الكائن القديم ، حيث سيتم تعيين قيمة isLoading على false وستكون قيمة الحمولة
تم تحميل المباراة.
إذا قمنا بإخراج سلسلة
State الناتجة إلى السجل ، فسوف نرى ما يلي:
- State ( payload = null، isLoading = false) - الحالة الأولية.
- State ( payload = null، isLoading = true) - بعد StartedLoading.
- State ( payload = data، isLoading = false) - بعد FinishedWithSuccess.
من خلال ربط هذه الحالات بواجهة المستخدم ، سترى جميع مراحل العملية: أولاً شاشة فارغة ، ثم شاشة تحميل ، وأخيرًا ، البيانات الضرورية.
هذا النهج له مزايا عديدة.
- أولاً ، من خلال تغيير الحالة مركزيًا باستخدام سلسلة من المعاملات ، لا نسمح بحالة السباق والعديد من الأخطاء المزعجة غير المرئية.
- ثانيًا ، بعد دراسة سلسلة من المعاملات ، يمكننا أن نفهم ما حدث ، ولماذا حدث وكيف أثر ذلك على حالة التطبيق. بالإضافة إلى ذلك ، مع Reducer ، من الأسهل بكثير تخيل جميع تغييرات الحالة قبل الإطلاق الأول للتطبيق على الجهاز.
- أخيرًا ، نحن قادرون على إنشاء واجهة بسيطة. نظرًا لأن جميع الولايات يتم تخزينها في مكان واحد (المتجر) ، والتي تأخذ في الاعتبار النوايا (النوايا) ، وتجري تغييرات باستخدام Reducer وتوضح سلسلة من الحالات ، يمكنك بعد ذلك وضع كل منطق الأعمال في المتجر واستخدام الواجهة لإطلاق النوايا وعرض الحالات.
أم لا؟
... ربما القطار يندفع إليك
من الواضح أن المخفض وحده ليس كافيًا. ماذا عن المهام غير المتزامنة مع نتائج مختلفة؟ كيف تستجيب للدفع من الخادم؟ ماذا عن إطلاق مهام إضافية (على سبيل المثال ، مسح ذاكرة التخزين المؤقت أو تحميل البيانات من قاعدة البيانات المحلية) بعد تغيير الحالة؟ اتضح أننا إما لا ندرج كل هذا المنطق في Reducer (أي ، لن يتم تغطية نصف جيد من منطق الأعمال ، ويجب أن يتم الاهتمام به من قبل أولئك الذين يقررون استخدام المكون الخاص بنا) ، أو أننا نجبر Reducer على القيام بكل شيء في وقت واحد.
متطلبات إطار عمل MVI
بالطبع ، نود أن نرفق منطق الأعمال بالكامل لميزة فردية في مكون مستقل ، حيث يمكن للمطورين من الفرق الأخرى العمل بسهولة من خلال إنشاء مثيل لها والاشتراك في حالتها.
بالإضافة إلى:
- يجب أن يتفاعل بسهولة مع المكونات الأخرى للنظام ؛
- في هيكلها الداخلي يجب أن يكون هناك فصل واضح للواجبات ؛
- يجب أن تكون جميع الأجزاء الداخلية للمكون حتمية تمامًا ؛
- يجب أن يكون التنفيذ الأساسي لمثل هذا المكون بسيطًا ومعقدًا فقط إذا كانت هناك حاجة إلى عناصر إضافية.
لم نتحرك على الفور من Reducer إلى الحل الذي نستخدمه اليوم. واجه كل فريق مشاكل باستخدام مناهج مختلفة ، وبدا من غير المحتمل تطوير حل عالمي يناسب الجميع.
ومع ذلك ، فإن الوضع الحالي للأشياء يناسب الجميع. يسعدنا أن نقدم لك MVICore! كود المصدر للمكتبة مفتوح ومتاح على
جيثب .
ما هو MVICore الجيد
- طريقة سهلة لتنفيذ ميزات أعمال البرمجة التفاعلية مع دفق بيانات أحادي الاتجاه.
- تحجيم: يشمل التنفيذ الأساسي فقط Reducer ، وفي الحالات الأكثر تعقيدًا ، يمكنك استخدام مكونات إضافية.
- حل للعمل مع الأحداث التي لا تريد تضمينها في الحالة ( مشكلة SingleLiveEvent ).
- واجهة برمجة تطبيقات بسيطة لميزات الربط (والمكونات التفاعلية الأخرى لنظامك) إلى واجهة المستخدم وإلى بعضها البعض مع دعم دورة حياة Android (وليس فقط).
- دعم الوسيطة (انظر أدناه) لكل مكون من مكونات النظام.
- مسجل جاهز والقدرة على تصحيح الوقت السفر لكل مكون.
مقدمة موجزة للميزة
نظرًا لنشر الإرشادات خطوة بخطوة بالفعل على GitHub ، فسوف أغفل أمثلة تفصيلية وأركز على المكونات الرئيسية للإطار.
الميزة - العنصر المركزي للإطار الذي يحتوي على كل منطق عمل المكون. يتم تعريف الميزة بثلاث معلمات:
ميزة الواجهة <الرغبات ، الحالة ، الأخبار>يتوافق
Wish مع Intent of Model-View-Intent - هذه هي التغييرات التي نريد رؤيتها في النموذج (نظرًا لأن مصطلح Intent له معنى خاص به في بيئة مطوري Android ، كان علينا العثور على اسم آخر). الرغبات هي نقطة دخول الميزة.
الحالة ، كما فهمت بالفعل ، حالة المكون. الدولة ليست ثابتة: لا يمكننا تغيير قيمها الداخلية ، ولكن يمكننا إنشاء دول جديدة. هذا هو الناتج: في كل مرة ننشئ فيها حالة جديدة ، نمررها إلى تيار Rx.
الأخبار - مكون لمعالجة الإشارات التي لا ينبغي أن تكون في الدولة ؛ يتم استخدام الأخبار مرة واحدة أثناء الإنشاء (
مشكلة SingleLiveEvent ). يعد استخدام الأخبار أمرًا اختياريًا (يمكنك استخدام لا شيء من Kotlin في توقيع الميزة).
أيضا في ميزة يجب أن تكون موجودة
المخفض .
قد تحتوي الميزة على المكونات التالية:
- الفاعل - ينفذ المهام غير المتزامنة و / أو تعديلات الحالة الشرطية بناءً على الحالة الحالية (على سبيل المثال ، التحقق من صحة النموذج). يقوم الممثل بربط Wish برقم تأثير معين ، ثم يقوم بتمريره إلى Reducer (في حالة عدم وجود Actor ، يتلقى Reducer Wish مباشرة).
- NewsPublisher - يتم الاتصال به عندما يصبح Wish أي تأثير ينتج النتيجة كدولة جديدة. بناءً على هذه البيانات ، يقرر إنشاء الأخبار.
- PostProcessor - يسمى أيضًا بعد إنشاء دولة جديدة ويعرف أيضًا التأثير الذي أدى إلى إنشائها. يطلق بعض الإجراءات الإضافية (الإجراءات). الإجراء - هذه "رغبات داخلية" (على سبيل المثال ، مسح ذاكرة التخزين المؤقت) لا يمكن بدؤها من الخارج. يتم تنفيذها في الممثل ، مما يؤدي إلى سلسلة جديدة من الآثار والدول.
- Bootstrapper هو مكون يمكنه تشغيل الإجراءات بمفرده. وتتمثل وظيفتها الرئيسية في تهيئة الميزة و / أو ربط المصادر الخارجية بالإجراء. قد تكون هذه المصادر الخارجية أخبارًا من ميزة أخرى أو بيانات خادم يجب أن تعدل الحالة بدون تدخل المستخدم.
قد يبدو الرسم البياني بسيطًا:

أو تضمين جميع المكونات الإضافية المذكورة أعلاه:

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

ماذا ايضا؟
الميزة ، وهي حجر الزاوية في الإطار ، تعمل على المستوى المفاهيمي. لكن المكتبة لديها الكثير لتقدمه.
- نظرًا لأن جميع مكونات الميزة حتمية (باستثناء الفاعل ، وهو ليس حتمًا تمامًا لأنه يتفاعل مع مصادر البيانات الخارجية ، ولكن حتى مع ذلك ، يتم تحديد الفرع الذي يتم تنفيذه بواسطة بيانات الإدخال ، وليس بالظروف الخارجية) ، يمكن تغليف كل منها في الوسيطة. في الوقت نفسه ، تحتوي المكتبة بالفعل على حلول جاهزة لتسجيل الدخول وتصحيح أخطاء السفر عبر الزمن .
- الوسيطة لا تنطبق فقط على الميزة ، ولكن أيضًا على أي كائنات أخرى تقوم بتطبيق واجهة المستهلك <T> ، مما يجعلها أداة تصحيح لا غنى عنها.
- عند استخدام مصحح أخطاء لتصحيح الأخطاء أثناء التحرك في الاتجاه المعاكس ، يمكنك تنفيذ الوحدة النمطية DebugDrawer .
- تحتوي المكتبة على مكوِّن IDEA الإضافي الذي يمكن استخدامه لإضافة قوالب لعمليات التنفيذ الأكثر شيوعًا للميزة ، والتي توفر الكثير من الوقت.
- هناك فئات مساعدة لدعم Android ، ولكن المكتبة نفسها ليست مرتبطة بـ Android.
- هناك حل جاهز لمكونات الربط لواجهة المستخدم وإلى بعضها البعض من خلال واجهة برمجة التطبيقات الأولية (سيتم مناقشة هذا في المقالة التالية).
نأمل أن تجرّب
مكتبتنا وأن يجلب لك استخدامها الكثير من البهجة مثلنا - إنشائها!
في 24 و 25 نوفمبر ، يمكنك تجربة يدك والانضمام إلينا! سنعقد حدث توظيف متنقل: في يوم واحد سيكون من الممكن المرور بجميع مراحل الاختيار وتلقي عرض. سيأتي زملائي من فرق iOS و Android للتواصل مع المرشحين في موسكو. إذا كنت من مدينة أخرى ، فإن Badoo تتحمل تكاليف السفر. للحصول على دعوة ، انتقل من خلال اختبار الفحص على الرابط . حظ موفق