أنماط التقسيم الشائعة

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

مبدأ التماسك العالي ومبدأ الاقتران المنخفض

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

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

أنواع الوحدات

تعتمد طريقة تنظيم الوحدات بشكل أساسي على بنية التطبيق. في ما يلي بعض الأنواع الشائعة للوحدات التي يمكنك تقديمها في تطبيقك أثناء اتّباع بنية التطبيق المقترَحة.

وحدات البيانات

عادةً ما تحتوي وحدة البيانات على مستودع ومصادر بيانات وفئات نماذج. المسؤوليات الأساسية الثلاث لوحدة البيانات هي:

  1. تلخيص جميع البيانات ومنطق الأعمال لنطاق معيّن: يجب أن تكون كل وحدة بيانات مسؤولة عن معالجة البيانات التي تمثّل نطاقًا معيّنًا. ويمكنها التعامل مع العديد من أنواع البيانات طالما أنها مرتبطة ببعضها البعض.
  2. عرض المستودع على أنه واجهة برمجة تطبيقات خارجية: يجب أن تكون واجهة برمجة التطبيقات العامة لوحدة البيانات مستودعًا لأنها مسؤولة عن عرض البيانات لبقية التطبيق.
  3. إخفاء جميع تفاصيل التنفيذ ومصادر البيانات من الخارج: يجب ألا يمكن الوصول إلى مصادر البيانات إلا من خلال المستودعات من الوحدة نفسها. وتظل مخفية عن الخارج. ويمكنك فرض ذلك باستخدام الكلمة الرئيسية private أو internal الخاصة بمستوى الظهور في Kotlin.
الشكل 1. عيّنة لوحدات البيانات ومحتواها

وحدات الميزات

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

الشكل 2. يمكن تعريف كل علامة تبويب في هذا التطبيق كميزة.

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

الشكل 3. نماذج لوحدات الميزات ومحتواها

وحدات التطبيق

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

الشكل 4. الرسم البياني للتبعية الخاص بوحدات نكهة المنتج *التجريبية* و *الكاملة*.

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

الشكل 5. الرسم البياني للتبعية على تطبيق Wear OS

الوحدات الشائعة

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

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

وحدات تجريبية

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

حالات الاستخدام للوحدات التجريبية

تعرض الأمثلة التالية المواقف التي يكون فيها تنفيذ وحدات الاختبار مفيدًا بشكل خاص:

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

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

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

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

الشكل 6. يمكن استخدام وحدات الاختبار لعزل الوحدات التي ستعتمد على بعضها بعضًا.

اتصال الوحدة النمطية

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

الشكل 7. يكون التواصل المباشر ثنائي الاتجاه بين الوحدات أمرًا مستحيلاً بسبب التبعيات الدورية. من الضروري استخدام وحدة توسّط لتنسيق تدفق البيانات بين وحدتَين مستقلتَين أخريين.

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

navController.navigate("checkout/$bookId")

تتلقّى وجهة الدفع معرّف كتاب كوسيطة تستخدمها لجلب معلومات حول الكتاب. يمكنك استخدام مؤشر الحالة المحفوظة لاسترداد وسيطات التنقّل داخل ViewModel لميزة الوجهة.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      …
}

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

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

الشكل 8. وحدتا ميزات تعتمدان على وحدة بيانات مشتركة.

عكس التبعية

يحدث قلب التبعية عندما تنظم التعليمة البرمجية بحيث يكون التجريد منفصلاً عن التطبيق الملموس.

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

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

الشكل 9. بدلاً من الوحدات عالية المستوى التي تعتمد على الوحدات المنخفضة المستوى مباشرةً، تعتمد الوحدات العالية المستوى ووحدات التنفيذ على وحدة التجريد.

مثال

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

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

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

إضافة رموز التبعية

ربما تتساءل الآن عن كيفية ارتباط وحدة الميزات بوحدة التنفيذ. الإجابة هي Pendingency Injection (إدخال التبعية). لا تقوم وحدة الميزات بإنشاء مثيل قاعدة البيانات المطلوب مباشرة. بدلاً من ذلك، فإنه يحدد التبعيات التي يحتاجها. ويتم بعد ذلك توفير هذه التبعيات خارجيًا، عادةً في وحدة التطبيق.

releaseImplementation(project(":database:impl:firestore"))

debugImplementation(project(":database:impl:room"))

androidTestImplementation(project(":database:impl:mock"))

المزايا

تتمثل فوائد فصل واجهات برمجة التطبيقات وعمليات تنفيذها في ما يلي:

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

حالات الفصل

من المفيد فصل واجهات برمجة التطبيقات عن عمليات التنفيذ في الحالات التالية:

  • إمكانات متنوعة: إذا كان بإمكانك تنفيذ أجزاء من نظامك بعدة طرق، تتيح لك واجهة برمجة التطبيقات الواضحة إمكانية تبادل عمليات التنفيذ المختلفة. على سبيل المثال، قد يكون لديك نظام عرض يستخدم OpenGL أو Vulkan، أو نظام فوترة متوافق مع Play أو واجهة برمجة تطبيقات الفوترة داخل الشركة.
  • تطبيقات متعددة: إذا كنت تطور تطبيقات متعددة ذات إمكانات مشتركة لأنظمة أساسية مختلفة، يمكنك تحديد واجهات برمجة تطبيقات مشتركة وتطوير عمليات تنفيذ محددة لكل نظام أساسي.
  • الفِرق المستقلة: يتيح الفصل للمطوّرين أو الفِرق المختلفة العمل على أجزاء مختلفة من قاعدة الرموز في الوقت نفسه. يجب أن يركز المطورون على فهم عقود واجهة برمجة التطبيقات واستخدامها بشكل صحيح. لا يحتاجون إلى القلق بشأن تفاصيل تنفيذ الوحدات الأخرى.
  • قاعدة رموز برمجية كبيرة: عندما يكون قاعدة الترميز كبيرة أو معقّدة، يؤدي فصل واجهة برمجة التطبيقات عن عملية التنفيذ إلى تسهيل إدارة الرمز. يتيح لك تقسيم قاعدة التعليمات البرمجية إلى وحدات أكثر دقة ومفهومًا وقابلة للصيانة.

كيفية التنفيذ

لتنفيذ عكس التبعية، اتبع الخطوات التالية:

  1. إنشاء وحدة تجريدية: يجب أن تحتوي هذه الوحدة على واجهات برمجة التطبيقات (الواجهات والنماذج) التي تحدد سلوك الميزة.
  2. إنشاء وحدات التنفيذ: يجب أن تعتمد وحدات التنفيذ على وحدة واجهة برمجة التطبيقات وأن تنفّذ سلوك التجريد.
    بدلاً من الوحدات عالية المستوى التي تعتمد على الوحدات منخفضة المستوى مباشرةً، تعتمد الوحدات العالية المستوى ووحدات التنفيذ على وحدة التجريد.
    الشكل 10. تعتمد وحدات التنفيذ على وحدة التجريد.
  3. إنشاء وحدات عالية المستوى تعتمد على وحدات التجريد: اجعل الوحدات تعتمد على وحدات التجريد بدلاً من الاعتماد مباشرةً على طريقة تنفيذ معيّنة. لا تحتاج الوحدات عالية المستوى إلى معرفة تفاصيل التنفيذ، لكنها تحتاج فقط إلى العقد (API).
    تعتمد الوحدات عالية المستوى على التجريدات وليس على التنفيذ.
    الشكل 11. تعتمد الوحدات العالية المستوى على التجريدات وليس على التنفيذ.
  4. توفير وحدة التنفيذ: أخيرًا، عليك توفير التنفيذ الفعلي لتبعياتك. يعتمد التنفيذ المحدّد على إعداد مشروعك، إلا أنّ وحدة التطبيق عادةً ما تكون مكانًا مناسبًا لتنفيذ ذلك. لتقديم عملية التنفيذ، حدِّدها على أنّها تبعية لصيغة الإصدار التي اخترتها أو مجموعة مصادر اختبار.
    توفر وحدة التطبيق التنفيذ الفعلي.
    الشكل 12. وتوفِّر وحدة التطبيق التنفيذ الفعلي.

أفضل الممارسات العامة

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

الحفاظ على اتساق الضبط

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

عرِّفها قدر الإمكان

يجب أن تكون الواجهة العامة للوحدة بسيطة وأن تعرض الأساسيات فقط. يجب ألا يؤدي ذلك إلى تسريب أي تفاصيل من عملية التنفيذ إلى الخارج. قم بتوسيع نطاق كل شيء إلى أصغر نطاق ممكن. استخدِم نطاق مستوى الرؤية private أو internal في Kotlin لجعل وحدة التعريفات بخصوصية. عند الإعلان عن التبعيات في وحدتك، يُفضَّل استخدام implementation بدلاً من api. يكشف الأخير التبعيات المتعددة لمستهلكي الوحدة. قد يؤدي استخدام التنفيذ إلى تحسين وقت الإنشاء لأنه يقلل من عدد الوحدات التي تحتاج إلى إعادة بنائها.

تفضيل وحدات Kotlin وJava

تتوفّر ثلاثة أنواع أساسية من الوحدات المتوافقة مع "استوديو Android":

  • وحدات التطبيق هي نقطة دخول إلى تطبيقك. ويمكن أن تحتوي على رمز المصدر والموارد ومواد العرض وAndroidManifest.xml. ناتج وحدة التطبيق هو حزمة تطبيق Android (AAB) أو حزمة تطبيق Android (APK).
  • تتضمّن وحدات المكتبة المحتوى نفسه المضمَّن في وحدات التطبيق. ويتم استخدامها بواسطة وحدات Android الأخرى كتبعية. نتيجة وحدة مكتبة هي أرشيف Android (AAR) متطابق من حيث البنية مع وحدات التطبيق، إلا أنه يتم تجميعها في ملف أرشيف Android (AAR) يمكن استخدامه لاحقًا بواسطة وحدات أخرى باعتباره ملفًا تابعًا. تتيح وحدة المكتبة إمكانية تغليف نفس المنطق والموارد وإعادة استخدامها عبر العديد من وحدات التطبيق.
  • لا تحتوي مكتبات Kootlin وJava على أيّ موارد أو مواد عرض أو ملفات بيانات لنظام التشغيل Android.

وبما أنّ وحدات Android مزوَّدة بتكاليف علوية، يُفضَّل استخدام نوعَي Kotlin أو Java قدر الإمكان.