من نسخة لصق إلى المكونات: إعادة استخدام الرمز في تطبيقات مختلفة



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

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

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

لكن أولاً ، دعونا نتناول المشاكل ، التي أدى حلها إلى إنشاء مكونات مشتركة. كان هناك العديد منهم:

  • نسخ لصق بين التطبيقات ؛
  • العمليات التي تدخل العصي في العجلات ؛
  • بنية مختلفة من المشاريع.



هذه المقالة هي نسخة نصية من تقريري باستخدام AppsConf 2019 ، والتي يمكن عرضها هنا .

المشكلة: نسخ لصق


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

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

المشكلة هي أن جميع طلباتنا موجودة في مستودعات مختلفة.



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

في هذه الحالة ، سيقوم Andrei إما بكتابة قراره (الذي يحدث في 80٪ من الحالات) أو نسخ حل Lyosha ولصقه وتغيير كل شيء فيه بحيث يناسب تطبيقه أو مهمته أو مزاجه.



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

هذا الموقف يجلب العديد من المشاكل.

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

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

ثالثًا ، تختلف بنية التطبيق تمامًا: من MVP إلى MVI ، من نشاط الله إلى نشاط فردي.

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

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

القرارات: نؤسس العمليات


من المشاكل المذكورة أعلاه ، ترتبط اثنتان بالعمليات:

  1. اثنين من المستودعات التي تشارك المشاريع مع جدار لا يمكن اختراقها.
  2. فرق منفصلة دون الاتصالات المعمول بها ومتطلبات مختلفة من فرق تطبيق المنتج.

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



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

يتمتع زملائي من فريق iOS بتجربة مماثلة ، واتضح أن ذلك لم يكن ناجحًا للغاية ، كما تحدث أنتون شوكان في مؤتمر Mobius العام الماضي.

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

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

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

المشكلة الثانية - التفاعل بين الفرق - تتألف من عدة أجزاء:

  • فرق منفصلة دون اتصال ثابت ؛
  • توزيع غير واضح للمسؤولية عن الوحدات المشتركة ؛
  • متطلبات مختلفة من فرق المنتج.

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

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

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

الحلول: تبسيط الهندسة المعمارية


خطوتنا التالية نحو إعادة الاستخدام كانت لتبسيط الهيكل. لماذا فعلنا هذا؟

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

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

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



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

دعونا نلقي نظرة على مثال لشاشة بسيطة كيف تبدو في الواقع.



نستخدم واجهات RxJava الأساسية للإشارة إلى الأنواع التي يعمل بها العنصر. يتم الإشارة إلى الإدخال بواسطة واجهة المستهلك <T> ، الإخراج - ObservableSource <T>.

// input = Consumer<ViewModel> // output = ObservableSource<Event> class View( val events: PublishRelay<Event> ): ObservableSource<Event> by events, Consumer<ViewModel> { val button: Button val textView: TextView init { button.setOnClickListener { events.accept(Event.ButtonClick) } } override fun accept(model: ViewModel) { textView.text = model.text } } 

باستخدام هذه الواجهات ، يمكننا التعبير عن طريقة العرض كمستهلك <ViewModel> و ObservableSource <Event>. لاحظ أن ViewModel يحتوي فقط على حالة الشاشة وليس له علاقة تذكر بـ MVVM. بعد تلقي النموذج ، يمكننا عرض البيانات منه ، وعندما نضغط على الزر ، نرسل الحدث الذي يتم إرساله إلى الخارج.

 // input = Consumer<Wish> // output = ObservableSource<State> class Feature: ReducerFeature<Wish, State>( initialState = State(counter = 0), reducer = ReducerImpl() ) { class ReducerImpl: Reducer<Wish, State> { override fun invoke(state: State, wish: Wish) = when (wish) { is Increment -> state.copy(counter = state.counter + 1) } } } 

ميزة تنفذ بالفعل ObservableSource والمستهلك بالنسبة لنا. نحن بحاجة إلى نقل الحالة الأولية (العداد يساوي 0) والإشارة إلى كيفية تغيير هذه الحالة.

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

بعد إنشاء العنصرين ، يبقى لنا أن نربطهما.


 val eventToWish: (Event) -> Wish = { when (it) { is ButtonClick -> Increment } } val stateToModel: (State) -> ViewModel = { ViewModel(text = state.counter.toString()) } Binder().apply { bind(view to feature using eventToWish) bind(feature to view using stateToModel) } 

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

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

تعمل عناصر MVICore بشكل جيد مع "حديقة الحيوان" الخاصة بنا من الهياكل بعد كتابة الأغلفة من ObservableSource والمستهلك. على سبيل المثال ، يمكننا التفاف أساليب Use Case من Clean Architecture في Wish / State واستخدامها في السلسلة بدلاً من Feature.



عنصر


أخيرًا ، ننتقل إلى المكونات. ماذا يحلو لهم؟

النظر في الشاشة في التطبيق وتقسيمها إلى أجزاء منطقية.



يمكن تمييزه:

  • شريط الأدوات مع شعار وأزرار في الأعلى ؛
  • بطاقة بها ملف تعريف وشعار ؛
  • قسم انستغرام.

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



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

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

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

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

 object SomeScopedComponent : ScopedComponent<SomeComponent>() { override fun create(): SomeComponent { return DaggerSomeComponent.builder() .build() } override fun SomeComponent.subscribe(): Array<Disposable> = arrayOf( Binder().apply { bind(feature().news to otherFeature()) bind(feature() to view()) } ) } 

بدأت المشاكل في مكانين في وقت واحد:

  1. بدأت شركة DI بالعمل مع المنطق ، مما أدى إلى وصف المكون بأكمله في فئة واحدة.
  2. نظرًا لأن الحاوية متصلة بنشاط أو جزء وتصف الشاشة بأكملها على الأقل ، فهناك الكثير من العناصر في هذه الشاشة / الحاوية ، والتي تترجم إلى كمية كبيرة من التعليمات البرمجية لتوصيل جميع التبعيات من هذه الشاشة.

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



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

 @Scope internal class ComponentImpl @Inject constructor( private val params: ScreenParams, news: NewsRelay, @OnDisposeAction onDisposeAction: () -> Unit, globalFeature: GlobalFeature, conversationControlFeature: ConversationControlFeature, messageSyncFeature: MessageSyncFeature, conversationInfoFeature: ConversationInfoFeature, conversationPromoFeature: ConversationPromoFeature, messagesFeature: MessagesFeature, messageActionFeature: MessageActionFeature, initialScreenFeature: InitialScreenFeature, initialScreenExplanationFeature: InitialScreenExplanationFeature?, errorFeature: ErrorFeature, conversationInputFeature: ConversationInputFeature, sendRegularFeature: SendRegularFeature, sendContactForCreditsFeature: SendContactForCreditsFeature, screenEventTrackingFeature: ScreenEventTrackingFeature, messageReadFeature: MessageReadFeature?, messageTimeFeature: MessageTimeFeature?, photoGalleryFeature: PhotoGalleryFeature?, onlineStatusFeature: OnlineStatusFeature?, favouritesFeature: FavouritesFeature?, isTypingFeature: IsTypingFeature?, giftStoreFeature: GiftStoreFeature?, messageSelectionFeature: MessageSelectionFeature?, reportingFeature: ReportingFeature?, takePhotoFeature: TakePhotoFeature?, giphyFeature: GiphyFeature, goodOpenersFeature: GoodOpenersFeature?, matchExpirationFeature: MatchExpirationFeature, private val pushIntegration: PushIntegration ) : AbstractMviComponent<UiEvent, States>( 

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

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



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



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

وبالتالي ، بالمقارنة مع التكرارات السابقة ، نحصل على:

  • مغلفة المنطق داخل مكون.
  • دعم للتداخل ، مما يجعل من الممكن تقسيم الشاشات إلى أجزاء ؛
  • التفاعل مع المكونات الأخرى من خلال واجهة صارمة من الإدخال / الإخراج مع دعم MVICore ؛
  • اتصال آمن برمجية من التبعيات المكونة (الاعتماد على خنجر باعتباره DI).

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

وهنا لدينا عالم مثالي. لديها مكونات يمكننا من خلالها بناء شجرة قابلة لإعادة الاستخدام بالكامل.

لكننا نعيش في عالم غير كامل.

مرحبا بكم في الواقع!


في عالم غير كامل ، هناك مجموعة من الأشياء التي يجب علينا تحملها. نحن قلقون بشأن ما يلي:

  • وظائف مختلفة: على الرغم من كل التوحيد ، ما زلنا نتعامل مع المنتجات الفردية ذات المتطلبات المختلفة ؛
  • الدعم: كيف بدون وظائف جديدة تحت اختبارات A / B؟
  • إرث (كل ما كتب قبل بنيتنا الجديدة).

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

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



متطلبات متنوعة


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



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

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

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



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

دعم


النظر في اختبار A / B لتباين العناصر البصرية. في هذه الحالة ، لا يتغير منطقنا ، مما يسمح لنا باستبدال تطبيق عرض آخر للواجهة الحالية من ObservableSource والمستهلك.



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

التكامل


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

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

في النهاية


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

تجربتنا تبين أن:

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

زميلي Zsolt Kocsi كتب سابقًا عن MVICore والأفكار التي تقف وراءه. أوصي بشدة بقراءة مقالاته التي ترجمناها على مدونتنا ( 1 ، 2 ، 3 ).

حول RIBs ، يمكنك قراءة المقال الأصلي من Uber . وللمعارف العملية ، أوصي بأخذ بعض الدروس منا (باللغة الإنجليزية).

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


All Articles