بنية طبقة تنفيذ المهام غير المتزامنة

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



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


نبذة عن المتحدث: ستيبان غونشاروف ( ستيبانغو ) يعمل لصالح غراب - إنه يشبه أوبر ، ولكن في جنوب شرق آسيا. شارك في تطوير أندرويد لأكثر من 9 سنوات. مهتم في Kotlin منذ عام 2014 ، ومنذ عام 2016 - يستخدمه في المنتج. تنظمه مجموعة مستخدمي Kotlin في سنغافورة. هذا أحد الأسباب وراء ظهور جميع أمثلة الأكواد البرمجية في Kotlin ، وليس لأنها عصرية.

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

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

قضايا RX


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

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

سنحاول حل بعض هذه المشاكل. سنحل كل مشكلة مرة واحدة ، ثم نعيد استخدام النتيجة.

المشكلة رقم 1: أداء مهمة واحدة أكثر من مرة


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

كيفية حل المشكلة؟ كل مطور لديه أسلوبه المفضل للحل: سوف يلتزم أحدهم debounce ، والآخر debounce الزر فقط في حالة clickable = false . لا يوجد نهج عام ، لذلك ستظهر هذه الأخطاء أو تختفي من تطبيقنا. نحن نحل المشكلة فقط عندما يخبرنا ضمان الجودة: "أوه ، لقد نقرت هنا ، وكسرت"!

حل قابل للتطوير؟


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

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

 typealias Id = String interface Act { val id: Id } 

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

واجهات قد تحتوي على الممتلكات.

للمبرمجين الذين يأتون من Java ، هذا غير متوقع. عادة ما يضيفون أساليب getId() داخل الواجهة ، لكن هذا هو الحل الخاطئ ، من وجهة نظر Kotlin.

كيف سنصمم؟


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

لماذا لا يكفي معرف؟


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

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

تقديم التجريدات الجديدة: MapDisposable


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

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

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

تقديم التجريدات الجديدة: ActExecutor


المكون التجريدي التالي هو ActExecutor. يبدأ أو لا يبدأ مهام جديدة ، ويعتمد على MapDisposable ويفوض معالجة الأخطاء. كيفية اختيار اسم - انظر الوثائق .

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

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

 interface ActExecutor { fun execute( act: Act, e: (Throwable) -> Unit = ::logError) } interface MapDisposable { fun contains(id: Id): Boolean fun add(id: Id, disposable: () -> T) fun remove(id: Id) } 

MapDisposable محدود أيضًا: خذ واجهة الخريطة وانسخ الأساليب ، add remove . تختلف طريقة add عن الخريطة: الحجة الثانية هي لامدا للتجميل والراحة. الراحة هي أنه يمكننا مزامنة lambda لمنع ظروف السباق غير المتوقعة. لكننا لن نتحدث عن هذا ، سنواصل العمارة.

تنفيذ واجهة


لقد أعلنا جميع الواجهات وسوف نحاول تنفيذ شيء بسيط. خذ CompleteableAct و SingleAct .

 class CompletableAct ( override val id: Id, override val completable: Completable ) : Act class SingleAct<T : Any>( override val id: Id, override val single: Single<T> ) : Act 

CompleteableAct هو التفاف على اكتمال. في حالتنا ، يحتوي ببساطة على معرف - وهو ما نحتاجه. SingleAct هو نفسه تقريبا. يمكننا تنفيذ ربما و Flowable كذلك ، ولكن بالتفصيل على أول تطبيقين.

بالنسبة لـ Single ، حددنا النوع العام <T : Any> . كمطور Kotlin ، أفضل استخدام مثل هذا النهج.

حاول استخدام الأدوية غير الخالية.

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

 class ActExecutorImpl ( val map: MapDisposable ): ActExecutor { fun execute( act: Act, e: (Throwable) -> Unit ) = when { map.contains(act.id) -> { log("${act.id} - in progress") } else startExecution(act, e) log("${act.id} - Started") } } 

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

لليقظة للغاية - لا يوجد التزامن ، ولكن التزامن هو في التعليمات البرمجية المصدر على جيثب.

 fun startExecution(act: Act, e: (Throwable) -> Unit) { val removeFromMap = { mapDisposable.remove(act.id) } mapDisposable.add(act.id) { when (act) { is CompletableAct -> act.completable .doFinally(removeFromMap) .subscribe({}, e) is SingleAct<*> -> act.single .doFinally(removeFromMap) .subscribe({}, e) else -> throw IllegalArgumentException() } } 

استخدم lambdas كوسيطة أخيرة لتحسين إمكانية قراءة التعليمات البرمجية. إنه جميل وسيشكر زملائك.

سنستخدم بعض شرائح Kotlin ونضيف وظائف التمديد لـ Completeable و Single. معهم ، لا يتعين علينا البحث عن طريقة المصنع لإنشاء أكمل قابل للتنفيذ و SingleAct - سننشئها من خلال وظائف الإضافة.

 fun Completable.toAct(id: Id): Act = CompletableAct(id, this) fun <T: Any> Single<T>.toAct(id: Id): Act = SingleAct(id, this) 

يمكن إضافة وظائف التمديد إلى أي فئة.

النتيجة


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

 fun act() = Completable.timer(2, SECONDS).toAct("Hello") executor.apply { execute(act()) execute(act()) execute(act()) } Hello - Act Started Hello - Act Duplicate Hello - Act Duplicate Hello - Act Finished 

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

تم حل المشكلة الأولى. الآن دعونا نوسع الحل لمنحه المرونة.

المشكلة رقم 2: ما مهمة الإلغاء؟


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

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

إضافة مكونات


نسمي استراتيجيات سلوك الاستعلام وننشئ واجهات اثنين لهم: StrategyHolder و Strategy . نحن أيضا إنشاء 2 الكائنات التي هي المسؤولة عن أي استراتيجية لتطبيق.

 interface StrategyHolder { val strategy: Strategy } sealed class Strategy object KillMe : Strategy() object SaveMe : Strategy() 

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

الطبقة المختومة هي أسهل لتمديد والكتابة أقصر.

تحديث المكونات الحالية


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

 interface Act : StrategyHolder { val id: String } class CompletableAct( override val id: String, override val completable: Completable, override val strategy: Strategy = SaveMe ) : Act 

الاستراتيجيات


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

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

  • إذا كانت استراتيجية SaveMe هي نفسها التي فعلناها من قبل ، فلن يتغير شيء.
  • إذا كانت الاستراتيجية هي KillMe ، فقتل الطلب السابق واطلق طلبًا جديدًا.

 override fun execute(act: Act, e: (Throwable) -> Unit) = when { map.contains(act.id) -> when (act.strategy) { KillMe -> { map.remove(act.id) startExecution(act, e) } SaveMe -> log("${act.id} - Act duplicate") } else -> startExecution(act, e) } 

النتيجة


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

 executor.apply { execute(Completable.timer(2, SECONDS) .toAct("Hello", KillMe)) execute(Completable.timer(2, SECONDS) .toAct("Hello", KillMe)) execute(Completable.timer(2, SECONDS) .toAct("Hello«, KillMe)) } Hello - Act Started Hello - Act Canceled Hello - Act Started Hello - Act Canceled Hello - Act Started Hello - Act Finished 

نقوم بإنشاء مهمة غير متزامنة ، وننقل الاستراتيجية ، وفي كل مرة نبدأ مهمة جديدة ، سيتم إلغاء جميع المهام السابقة ، وليس المهام التالية.

المشكلة 3: لا توجد استراتيجيات كافية


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

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

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

مجموعات واستراتيجيات للمجموعات


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

 interface GroupStrategyHolder { val groupStrategy: GroupStrategy val groupKey: String } sealed class GroupStrategy object Default : GroupStrategy() object KillGroup : GroupStrategy() 

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

 interface Act : StrategyHolder, GroupStrategyHolder { val id: String } class CompletableAct( override val id: String, override val completable: Completable, override val strategy: Strategy = SaveMe, override val groupStrategy: GroupStrategy = Default override val groupKey: String = "" ) : Act 

نكرر الخطوات التي تحدثت عنها سابقًا: نأخذ الواجهة ونوسع ونضيف حقلين إضافيين إلى CompletableAct و SingleAct.

تحديث التنفيذ


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

 MapDisposable -> GroupDisposable ... override fun execute(act: Act, e: (Throwable) -> Unit) { if (act.groupStrategy == KillGroup) groupDisposable.removeGroup(act.groupKey) return when { groupDisposable.contains(act.groupKey, act.id) -> when (act.strategy) { KillMe -> { stop(act.groupKey, act.id) startExecution(act, e) } SaveMe -> log("${act.id} - Act duplicate") } else -> startExecution(act, e) } } 

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

النتيجة


 fun act(id: String)= Completable.timer(2, SECONDS).toAct( id = id, groupStrategy = KillGroup, groupKey = "Like-Dislike-PostId-1234" ) executor.apply { execute(act(“Like”)) execute(act(“Dislike”)) execute(act(“Like”)) } Like - Act Started Like - Act Canceled Dislike - Act Started Dislike - Act Canceled Like - Act Started Like - Act Finished 

إذا كنا بحاجة إلى مثل هذه الاستعلامات المعقدة ، فسنضيف حقلين: groupStrategy و معرف المجموعة. معرف المجموعة هو معلمة محددة ، لأنه لدعم العديد من طلبات متشابهة / لا تعجبك ، تحتاج إلى إنشاء مجموعة لكل زوج من الطلبات التي تنتمي إلى نفس الكائن. في هذه الحالة ، يمكنك تسمية المجموعة Like-Dislike-PostId وإضافة معرف المنشور هناك. في كل مرة نحب فيها المنشورات المجاورة ، سنكون متأكدين من أن كل شيء يعمل بشكل صحيح للنشر السابق ، وللنشرة التالية.

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

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

لتسهيل القراءة ، استخدم المعلمات المسماة.

العمارة


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



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

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



ماذا لإضافة؟


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

الاحتمالات


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

سلاسل المكالمات. بسبب التفاف سلاسل RX ، يصبح من الممكن إجراء تسلسل لها ، لأن RX لا يقوم بالتسلسل افتراضيًا.

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

دعونا النظر في الاحتمالات بمزيد من التفصيل.

معالجة دورة الحياة


 class ActExecutorImpl( lifecycle: Lifecycle ) : ActExecutor { inir { lifecycle.doOnDestroy { cancelAll() } } ... 

هذا مثال على تنفيذ دورة الحياة. في أبسط الحالات - مع شظايا Destroy أو إلغاؤها باستخدام Activity ، نمرر دورة حياة دورة التنفيذ إلى Executor لدينا ، وعندما يحدث حدث onDestroy ، نحذف جميع الطلبات . هذا حل بسيط يلغي الحاجة إلى نسخ تعليمات برمجية مماثلة في طريقة عرض النماذج. LifeData يفعل الشيء نفسه تقريبا.

إنقاذ / استعادة


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

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

سلاسل الاتصال


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

المقاييس


من المثير للاهتمام معرفة عدد الاستعلامات الموازية التي يتم تنفيذها في المتوسط ​​في الخلفية . بعد المقاييس ، يمكنك فهم سبب شكاوى المستخدمين حول الخمول. , , .

, , , , - - 10% . , .

الخاتمة


— , . «» . , , , , .

, , . — - , , — . — . , . . .

. Kotlin, . , .

AppsConf 2018, AppsConf 2019 . 38 : , Android, UX, , - , , Kotlin.

, youtube- 22–23 .

Source: https://habr.com/ru/post/ar437592/


All Articles