بنية متعددة الوحدات الروبوت. من الألف إلى الياء

مرحبا بالجميع!

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

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

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

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

بالإجابة على السؤال الأول ، ما هو حجم AppComponent ، يمكنني أن أعترف - كبير ، كبير حقًا. وعذبتني باستمرار. كيف حدث ذلك؟ بادئ ذي بدء ، هذا بسبب منظمة DI. مع DI سنبدأ.

مثلما فعلت DI من قبل


أعتقد أن العديد من الأشخاص قد شكلوا في رؤوسهم شيئًا مثل هذا الرسم التخطيطي لتبعيات المكونات والمجالات المقابلة:


ماذا لدينا هنا


AppComponent ، الذي استوعب جميع التبعيات على الإطلاق مع نطاقات Singleton . أعتقد أن الجميع تقريبًا لديهم هذا المكون.

الميزةالمكونات . كانت كل ميزة في نطاقها الخاص وكانت مكونًا فرعيًا لـ AppComponent أو ميزة كبيرة.
دعونا نتحدث قليلاً عن الميزات. بادئ ذي بدء ، ما هي الميزة؟ سأحاول بكلماتي الخاصة. الميزة هي وحدة برامج مستقلة كاملة تمامًا ومنطقيًا تحل مشكلة مستخدم معينة ، مع تبعيات خارجية محددة بوضوح ، وهي سهلة الاستخدام نسبيًا مرة أخرى في برنامج آخر. يمكن أن تكون الميزات كبيرة وصغيرة. قد تحتوي الميزات على ميزات أخرى. ويمكنهم أيضًا استخدام أو تشغيل ميزات أخرى من خلال تبعيات خارجية محددة بوضوح. إذا أخذنا تطبيقنا (Kaspersky Internet Security for Android) ، فيمكن اعتبار الميزات مكافحة فيروسات ، ومكافحة السرقة ، وما إلى ذلك.

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

الآن قائمة "لماذا"


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

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

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

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

مشاكل الحياة


مهمة مثال


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


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


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

الحلول المحتملة


كيف نطبق هذا المثال من حيث DI؟ هناك عدة خيارات.

الخيار الأول


حدد ميزة شراء كمكون مستقل يعتمد فقط على AppComponent .


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

الخيار الثاني


نختار ميزة الشراء في المكون الفرعي ، والتي تعتمد على AppComponent. ويمكن جعل مكونات الماسح الضوئي ومكافحة السرقة مكونات فرعية من مكون الشراء.


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

الخيار الثالث


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

الطريقة الأولى
دعونا نضيف ميزات نطاقات Singleton إلى جميع التبعيات والاتصال بـ AppComponent .


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

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


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

ألم معماري


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

استنتاجات وسيطة


لذا ، ماذا نريد في النهاية؟ ما هي المشاكل التي نريد حلها؟ دعنا نذهب مباشرة إلى النقطة ، بدءًا من DI والانتقال إلى الهندسة المعمارية:

  • آلية DI مريحة تسمح لك باستخدام الميزات داخل الميزات الأخرى (في مثالنا ، نريد استخدام ميزة Shopping داخل Scanner و Anti-Theft) ، دون عكاز وألم.
  • أنحف AppComponent.
  • لا يجب أن تكون الميزات على دراية بتنفيذ الميزات الأخرى.
  • لا ينبغي الوصول إلى الميزات بشكل افتراضي لأي شخص ، أريد أن يكون لدي نوع من آلية التحكم الصارمة.
  • من الممكن إعطاء الميزة لتطبيق آخر مع الحد الأدنى من الإيماءات.
  • الانتقال المنطقي إلى الوحدات النمطية المتعددة وأفضل الممارسات لهذا التحول.

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

"العيش بطريقة جديدة"


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

تحسينات DI


لنبدأ بنفس DI.

رفض عدد كبير من المجالات


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

التخلي عن المكونات الفرعية لصالح تبعيات المكونات


بالفعل نقطة أكثر إثارة للاهتمام. مع المكونات الفرعية ، يبدو أن لديك تسلسل هرمي أكثر صرامة ، ولكن في نفس الوقت كنت قد ربطت يديك تمامًا ولا توجد طريقة للمناورة بطريقة أو بأخرى. بالإضافة إلى ذلك ، يعرف AppComponent جميع الميزات ، ويمكنك أيضًا الحصول على فئة DaggerAppComponent الضخمة المولدة.
مع تبعيات المكونات ، يمكنك الحصول على ميزة رائعة للغاية. في تبعيات المكونات ، لا يمكنك تحديد المكونات ، ولكن واجهات نظيفة (بفضل Denis و Volodya). بفضل هذا ، يمكنك استبدال أي تطبيقات واجهة تعجبك ، سيأكل Dagger كل شيء. حتى لو كان المكون بنفس النطاق هو هذا التطبيق:
@Component( dependencies = FeatureDependencies.class, modules = FeatureModule.class ) @PerFeature public abstract class FeatureComponent { // ... } public interface FeatureDependencies { SomeDependency someDependency(); } @Component( modules = AnotherFeatureModule.class ) @PerFeature public abstract class AnotherFeatureComponent implements FeatureDependencies { // ... } 


من تحسينات DI إلى التحسينات المعمارية


دعنا نكرر تعريف الميزات. الميزة هي وحدة برامج مستقلة كاملة تمامًا ومنطقيًا تحل مشكلة مستخدم معينة ، مع تبعيات خارجية محددة بوضوح ، والتي يسهل إعادة استخدامها نسبيًا في برنامج آخر. أحد التعبيرات الرئيسية في تعريف الميزة هو "مع تبعيات خارجية محددة بوضوح". لذلك ، دعنا نصف كل شيء نريده من العالم الخارجي للميزات ، سنصف في واجهة خاصة.
هنا ، دعنا نقول ، واجهة التبعية الخارجية لميزة التسوق:
 public interface PurchaseFeatureDependencies { HttpClientApi httpClient(); } 

أو واجهة التبعيات الخارجية لميزة الماسح الضوئي:
 public interface ScannerFeatureDependencies { DbClientApi dbClient(); HttpClientApi httpClient(); SomeUtils someUtils(); //       PurchaseInteractor purchaseInteractor(); } 

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

هناك مكون مهم آخر لميزة "نقية" وهو وجود واجهة برمجة تطبيقات واضحة ، والتي يمكن للعالم الخارجي من خلالها الوصول إلى الميزة.
فيما يلي ميزات واجهة برمجة التطبيقات للتسوق:
 public interface PurchaseFeatureApi { PurchaseInteractor purchaseInteractor(); } 

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

وإليك ميزات واجهة برمجة التطبيقات للماسح الضوئي:
 public interface ScannerFeatureApi { ScannerStarter scannerStarter(); } 

وعلى الفور أحضر واجهة وتنفيذ ScannerStarter :
 public interface ScannerStarter { void start(Context context); } @PerFeature public class ScannerStarterImpl implements ScannerStarter { @Inject public ScannerStarterImpl() { } @Override public void start(Context context) { Class<?> cls = ScannerActivity.class; Intent intent = new Intent(context, cls); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } 

إنه أكثر إثارة للاهتمام هنا. والحقيقة هي أن الماسح الضوئي ومكافحة السرقة ميزات مغلقة تمامًا ومعزولة. في المثال الخاص بي ، يتم تشغيل هذه الميزات في أنشطة منفصلة ، مع التنقل الخاص بهم ، وما إلى ذلك. وهذا يكفي بالنسبة لنا لبدء النشاط ببساطة هنا. يموت النشاط - تموت الميزة. يمكنك العمل على مبدأ "نشاط واحد" ، ومن ثم من خلال ميزات واجهة برمجة التطبيقات (API) ، على سبيل المثال ، تمرير FragmentManager وبعض رد الاتصال الذي تُبلغ من خلاله الميزة أنها أكملت. هناك العديد من الاختلافات.
يمكننا أيضًا أن نقول أن لدينا الحق في اعتبار ميزات مثل Scanner و Anti-Theft كتطبيقات مستقلة. على عكس ميزة الشراء ، التي تعد إضافة إلى ميزة لشيء ما ، فهي غير موجودة بشكل أو بآخر. نعم ، إنه مستقل ، ولكنه مكمل منطقي لميزات أخرى.

كما يمكنك أن تتخيل ، يجب أن تكون هناك نقطة تربط الميزات وتنفيذها والميزات الضرورية للتبعية. هذه النقطة هي مكون خنجر.
مثال على مكون ميزة الماسح الضوئي:
 @Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class // ScannerFeatureDependencies - api    }, dependencies = ScannerFeatureDependencies.class) @PerFeature // ScannerFeatureApi - api   public abstract class ScannerFeatureComponent implements ScannerFeatureApi { private static volatile ScannerFeatureComponent sScannerFeatureComponent; //   public static ScannerFeatureApi initAndGet( ScannerFeatureDependencies scannerFeatureDependencies) { if (sScannerFeatureComponent == null) { synchronized (ScannerFeatureComponent.class) { if (sScannerFeatureComponent == null) { sScannerFeatureComponent = DaggerScannerFeatureComponent.builder() .scannerFeatureDependencies(scannerFeatureDependencies) .build(); } } } return sScannerFeatureComponent; } //           public static ScannerFeatureComponent get() { if (sScannerFeatureComponent == null) { throw new RuntimeException( "You must call 'initAndGet(ScannerFeatureDependenciesComponent scannerFeatureDependenciesComponent)' method" ); } return sScannerFeatureComponent; } //    (   ) public void resetComponent() { sScannerFeatureComponent = null; } public abstract void inject(ScannerActivity scannerActivity); //         Moxy public abstract ScannerScreenComponent scannerScreenComponent(); } 


أعتقد أن لا شيء جديد بالنسبة لك.

الانتقال إلى الوحدات النمطية المتعددة


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

وانظر إلى بنية الحزمة في المثال:

الآن لنتحدث بعناية كل عنصر.

بادئ ذي بدء ، نرى أربع كتل كبيرة: التطبيق ، API ، Impl و Utils . في واجهات برمجة التطبيقات و Impl و Utils ، قد تلاحظ أن جميع الوحدات تبدأ إما في النواة أو في الميزة . فلنتحدث عنها أولاً.

الفصل في الجوهر والميزة


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

الآن حوالي أربع كتل كبيرة: التطبيق ، API ، Impl و Utils


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

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

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

نفذ
لدينا هنا قسم فرعي إلى اللب الأساسي والخاص بالميزة .
الوحدات في اللب الأساسية مستقلة تمامًا أيضًا. اعتمادهم الوحيد هو وحدة واجهة برمجة التطبيقات . على سبيل المثال ، ألق نظرة على build.gradle من Core-db-impl module :
 // bla-bla-bla dependencies { implementation project(':core-db-api') // bla-bla-bla } 

الآن حول ميزة الميزة . هناك بالفعل حصة الأسد في منطق التطبيق. يمكن أن تعرف وحدات مجموعة ميزة الميزات عن وحدات واجهة برمجة التطبيقات أو مجموعة Utils ، لكنها بالتأكيد لا تعرف أي شيء عن الوحدات الأخرى في مجموعة Impl .
كما نتذكر ، يتم تجميع جميع التبعيات الخارجية للميزة في التبعيات الخارجية. على سبيل المثال ، بالنسبة لميزة المسح ، يبدو هذا api كما يلي:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

وبناءً على ذلك ، سيكون build.gradle feature-scanner-impl هكذا :
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla } 

قد تسأل ، لماذا لا يوجد api التبعيات الخارجية في وحدة api؟ الحقيقة هي أن هذا هو تفاصيل التنفيذ. أي أنه تنفيذ معين يحتاج إلى بعض التبعيات المحددة. للماسح التبعي API هنا:


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

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

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

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

هناك أيضًا سؤال مثير للاهتمام حول نقل الميزات إلى تطبيق آخر. على سبيل المثال ، إذا أردنا نقل الماسحة الضوئية إلى تطبيق آخر ، فيجب علينا أيضًا أن ننقل بالإضافة إلى الوحدات النمطية : feature-scanner-api و : feature-scanner-impl والوحدات التي يعتمد عليها الماسح الضوئي ( : core-utils،: core-network- api،: core-db-api،: feature-buy-api ).
نعم لكن! أولاً ، جميع وحدات واجهة برمجة التطبيقات الخاصة بك مستقلة تمامًا ، ولا توجد سوى واجهات ونماذج بيانات. لا منطق. وهذه الوحدات منفصلة بشكل منطقي بوضوح ، و : الأدوات الأساسية عادة ما تكون وحدة مشتركة لجميع التطبيقات.
ثانيًا ، يمكنك جمع وحدات api في شكل aar وتسليمها من خلال المخضر إلى تطبيق آخر ، أو يمكنك توصيلها في شكل وحدة فرعية gig. ولكن سيكون لديك إصدار ، سيكون هناك تحكم ، سيكون هناك نزاهة.
وبالتالي ، فإن إعادة استخدام الوحدة (بتعبير أدق ، وحدة التنفيذ) في تطبيق آخر تبدو أبسط وأكثر وضوحًا وأمانًا.

التطبيق


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

بادئ ذي بدء ، دعنا نفكر في كيفية حدوث كل شيء من خلال التطبيق مع مثال الماسح الضوئي المحبوب بالفعل.
تذكر الميزة بسرعة:
الخيال العلمي للاعتمادات الخارجية هو:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

لذلك : تعتمد ميزة فحص الماسح الضوئي على الوحدات التالية:
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla } 


بناءً على ذلك ، يمكننا إنشاء مكون Dagger يقوم بتنفيذ api من التبعيات الخارجية:
 @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } 

لقد وضعت هذه الواجهة في ScannerFeatureComponent من أجل الراحة:
 @Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class }, dependencies = ScannerFeatureDependencies.class) @PerFeature public abstract class ScannerFeatureComponent implements ScannerFeatureApi { // bla-bla-bla @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } } 


الآن التطبيق. التطبيق يعرف عن جميع الوحدات التي يحتاجها ( الأساسية- ، الميزة- ، api ، impl ):
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-db-api') implementation project(':core-db-impl') implementation project(':core-network-api') implementation project(':core-network-impl') implementation project(':feature-scanner-api') implementation project(':feature-scanner-impl') implementation project(':feature-antitheft-api') implementation project(':feature-antitheft-impl') implementation project(':feature-purchase-api') implementation project(':feature-purchase-impl') // bla-bla-bla } 

بعد ذلك ، قم بإنشاء فصل مساعد. على سبيل المثال ، FeatureProxyInjector . سيساعد على تهيئة جميع المكونات بشكل صحيح ، ومن خلال هذه الفئة سننتقل إلى الميزات. دعنا نرى كيف تتم تهيئة مكون ميزة الماسح الضوئي:
 public class FeatureProxyInjector { // another... public static ScannerFeatureApi getFeatureScanner() { return ScannerFeatureComponent.initAndGet( DaggerScannerFeatureComponent_ScannerFeatureDependenciesComponent.builder() .coreDbApi(CoreDbComponent.get()) .coreNetworkApi(CoreNetworkComponent.get()) .coreUtilsApi(CoreUtilsComponent.get()) .purchaseFeatureApi(featurePurchaseGet()) .build() ); } } 

ظاهريًا ، نقدم واجهة الميزة ( ScannerFeatureApi ) ، وداخلنا فقط نقوم بتهيئة الرسم البياني لاعتماد التنفيذ بالكامل (عبر طريقة ScannerFeatureComponent.initAndGet (...) ).
DaggerPurchaseComponent_PurchaseFeatureDependenciesComponent هو تنفيذ PurchaseFeatureDependenciesComponent الذي تم إنشاؤه بواسطة Dagger ، والذي تحدثنا عنه أعلاه ، حيث نستبدل تنفيذ وحدات واجهة برمجة التطبيقات في المنشئ.
هذا كل شيء سحري. انظر المثال مرة أخرى.

بالحديث عن المثال . على سبيل المثال ، يجب أن نرضي أيضًا كل التبعيات الخارجية : ميزة الماسح الضوئي . ولكن بما أن هذا مثال ، يمكننا استبدال فئات وهمية.
كيف ستبدو:
 //     ScannerFeatureDependencies public class ScannerFeatureDependenciesFake implements ScannerFeatureDependencies { @Override public DbClientApi dbClient() { return new DbClientFake(); } @Override public HttpClientApi httpClient() { return new HttpClientFake(); } @Override public SomeUtils someUtils() { return CoreUtilsComponent.get().someUtils(); } @Override public PurchaseInteractor purchaseInteractor() { return new PurchaseInteractorFake(); } } //  -  Application-   public class ScannerExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); ScannerFeatureComponent.initAndGet( // ,     =) new ScannerFeatureDependenciesFake() ); } } 

ويتم تشغيل ميزة Scanner نفسها في المثال من خلال البيان ، حتى لا يتم حظر نشاط فارغ إضافي:
 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.scanner_example"> <application android:name=".ScannerExampleApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <!--   --> <activity android:name="com.example.scanner.presentation.view.ScannerActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 


خوارزمية الانتقال من أحادية إلى متعددة


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

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

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

  • إنشاء ميزات واجهة برمجة التطبيقات ، ووضعها في وحدة واجهة برمجة تطبيقات جديدة. أي ، لإنشاء وحدة نمطية بالكامل : ميزة الماسح الضوئي- واجهة برمجة التطبيقات مع جميع الواجهات.
  • إنشاء : ميزة الماسح الضوئي . نقل جميع الرموز المتعلقة بالميزة إلى هذه الوحدة. كل شيء تعتمد عليه ميزتك ، سوف يسلط الاستوديو الضوء على الفور.
  • تحديد تبعيات الميزات الخارجية. إنشاء واجهات مناسبة. تنقسم هذه الواجهات إلى وحدات api-modules منطقية. هذا هو ، في مثالنا ، إنشاء الوحدات النمطية : core-utils ،: core-network-api ،: core-db-api ،: feature-buy-api مع الواجهات المقابلة.
    أنصحك بالاستثمار الفوري في اسم الوحدات ومعناها. من الواضح أنه بمرور الوقت ، يمكن أن يتم تبديل الواجهات والوحدات قليلاً ، أو انهيارها ، وما إلى ذلك ، وهذا أمر طبيعي.
  • إنشاء واجهة برمجة تطبيقات للتبعيات الخارجية ( ScannerFeatureDependencies ). اعتمادًا على : ميزة الماسح الضوئي ، قم بتسجيل وحدات واجهة برمجة التطبيقات التي تم إنشاؤها حديثًا.
  • نظرًا لأن لدينا كل الإرث في التطبيق ، فإليك ما نقوم به. في التطبيق ، نقوم بتوصيل جميع الوحدات التي تم إنشاؤها للميزة (وحدة واجهة برمجة التطبيقات المميزة ، وحدة تطبيق الميزة ، وحدات واجهة برمجة تطبيقات التبعية الخارجية).
    نقطة فائقة الأهمية . بعد ذلك ، في التطبيق ، نقوم بإنشاء تطبيقات لجميع واجهات تبعية الميزات الضرورية (الماسح الضوئي في مثالنا). من المحتمل أن تكون عمليات التنفيذ هذه مجرد وكلاء من تبعيات api الخاصة بك إلى التنفيذ الحالي لهذه التبعيات في المشروع. عند تهيئة مكون الميزة ، استبدل بيانات التنفيذ.
    صعب في الكلمات ، هل تريد مثالاً؟ لذلك هو بالفعل! في الواقع ، يوجد شيء مشابه بالفعل في مثال الماسح الضوئي للميزات. مرة أخرى ، سأعطيها كودًا معدلاً قليلاً:
     //     ScannerFeatureDependencies  app- public class ScannerFeatureDependenciesLegacy implements ScannerFeatureDependencies { @Override public DbClientApi dbClient() { return new DbClientLegacy(); } @Override public HttpClientApi httpClient() { // -  // ,      return NetworkFabric.createHttpClientLegacy(); } @Override public SomeUtils someUtils() { return new SomeUtils(); } @Override public PurchaseInteractor purchaseInteractor() { return new PurchaseInteractorLegacy(); } } //  -   ScannerFeatureComponent.initAndGet( new ScannerFeatureDependenciesLegacy() ); 

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

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

نصائح إضافية


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

نقاء وحدة التطبيق
عند التبديل إلى التطبيق متعدد الوحدات ، سيكون لدينا الكثير جدًا ، ومن هناك ، سترتعد الميزات المميزة. من الممكن أنه في أثناء العمل ، سيكون عليك إجراء تغييرات على هذا الإرث ، لإنهاء شيء هناك ، حسنًا ، أو لديك إصدار فقط ، وأنك لا تصل إلى التخفيضات في الوحدات. في هذه الحالة ، تريد التطبيق ، ومعه كل التراث ، أن يعرف عن الميزات المميزة فقط من خلال واجهة برمجة التطبيقات ، لا توجد معرفة حول عمليات التنفيذ. ولكن التطبيق ، في الواقع، يجمع api- و impl الإضافية ، ولكن لأن التطبيق يعرف كل شيء.
في هذه الحالة ، يمكنك إنشاء وحدة خاصة: محول ، والذي سيكون مجرد نقطة اتصال لـ api و impl ، ومن ثم سيعرف التطبيق فقط عن api. أعتقد أن الفكرة واضحة. يمكنك مشاهدة مثال في فرع clean_app . سأضيف ذلك مع Moxy ، أو بالأحرى MoxyReflector ، هناك بعض المشاكل عند التقسيم إلى وحدات ، والتي اضطررت إلى إنشاء وحدة إضافية أخرى : stub-moxy-java . قليل من السحر حيث بدونه.
التعديل الوحيد. لن يعمل هذا إلا عندما يتم بالفعل نقل الميزة والاعتمادات ذات الصلة فعليًا إلى وحدات أخرى. إذا قمت بعمل ميزة ، ولكن التبعيات لا تزال موجودة في التطبيق ، كما هو الحال في الخوارزمية أعلاه ، فلن يعمل هذا.

خاتمة


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

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


All Articles