يوصي دليل هندسة تطبيقات Android الرسمي باستخدام فئات المستودع "لتوفير واجهة برمجة تطبيقات نظيفة حتى يتمكن باقي التطبيق من استرداد البيانات بسهولة." ومع ذلك ، في رأيي ، إذا كنت تستخدم هذا النمط في مشروعك ، فيضمن لك الوقوع في المستنقع في كود السباغيتي الفوضوي.
في هذه المقالة ، سأخبرك عن "نمط المستودع" وأشرح سبب كونه في الواقع مضادًا للنمط لتطبيقات Android.
مخزن
يوصي دليل هندسة التطبيقات المذكور أعلاه بالهيكل التالي لتنظيم منطق طبقة العرض التقديمي:
دور كائن المستودع في هذه البنية هو كما يلي:
تعالج وحدات المستودع عمليات البيانات. أنها توفر واجهة برمجة تطبيقات نظيفة بحيث يمكن لبقية التطبيق استرداد هذه البيانات بسهولة. إنهم يعرفون مكان الحصول على البيانات وما هي استدعاءات واجهة برمجة التطبيقات التي يتعين إجراؤها عند تحديثها. يمكنك التفكير في المستودعات كوسطاء بين مصادر البيانات المختلفة مثل النماذج الثابتة وخدمات الويب وذاكرة التخزين المؤقت.
بشكل أساسي ، يوصي الدليل باستخدام المستودعات لتلخيص مصدر البيانات في تطبيقك. تبدو معقولة جدًا وحتى مفيدة ، أليس كذلك؟
ومع ذلك ، دعونا لا ننسى أن الدردشة ليست رمي الحقائب (في هذه الحالة ، كتابة التعليمات البرمجية) ، ولكن للكشف عن الموضوعات المعمارية باستخدام مخططات UML - أكثر من ذلك. الاختبار الحقيقي لأي نمط معماري هو التنفيذ في التعليمات البرمجية ثم تحديد مزاياها وعيوبها. لذلك دعونا نجد شيئًا أقل تجريدًا لمراجعته.
المستودع في Android Architecture Blueprints v2
منذ حوالي عامين ، قمت بمراجعة "الإصدار الأول" من Android Architecture Blueprints. من الناحية النظرية ، كان من المفترض أن يطبقوا مثالًا نظيفًا للـ MVP ، لكن في الممارسة العملية ، أدت هذه المخططات إلى قاعدة رمز قذرة إلى حد ما. لقد احتوتوا على واجهات تسمى View and Presenter ، لكنهم لم يضعوا أي حدود معمارية ، لذلك لم يكن في الأساس MVP. يمكنك أن ترى مراجعة كود معين هنا .
منذ ذلك الحين ، قامت Google بتحديث المخططات المعمارية باستخدام Kotlin و ViewModel وغيرها من الممارسات "الحديثة" ، بما في ذلك المستودعات. تم بدء هذه المخططات المحدثة بـ v2.
دعنا نلقي نظرة على واجهة TasksRepository من مخططات v2:
interface TasksRepository {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
حتى قبل قراءة الكود ، يمكنك الانتباه إلى حجم هذه الواجهة - فهذه بالفعل مكالمة إيقاظ. قد يثير مثل هذا العدد من الأساليب في واجهة واحدة أسئلة حتى في مشاريع Android الكبيرة ، لكننا نتحدث عن تطبيق ToDo يحتوي على 2000 سطر فقط من التعليمات البرمجية. لماذا يحتاج هذا التطبيق البسيط نوعًا ما إلى فئة بسطح واجهة برمجة تطبيقات ضخم؟
المستودع كشيء الله
يتم تغطية إجابة السؤال من القسم السابق في أسماء طرق TasksRepository. يمكنني تقسيم أساليب هذه الواجهة تقريبًا إلى ثلاث مجموعات غير متداخلة.
مجموعة 1:
fun observeTasks(): LiveData<Result<List<Task>>>
fun observeTask(taskId: String): LiveData<Result<Task>>
المجموعة 2:
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
المجموعة 3:
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
الآن دعنا نحدد مجالات مسؤولية كل من المجموعات المذكورة أعلاه.
المجموعة 1 هي في الأساس تنفيذ لنمط Observer باستخدام منشأة LiveData. المجموعة 2 هي البوابة إلى مخزن البيانات بالإضافة إلى طريقتين
refreshمطلوبتين لأن مخزن البيانات البعيد مخفي خلف المستودع. تحتوي المجموعة 3 على طرق وظيفية تنفذ بشكل أساسي جزأين من منطق مجال التطبيق (إكمال المهمة والتفعيل).
إذن فهذه الواجهة الواحدة لها ثلاث مسؤوليات مختلفة. لا عجب أنها كبيرة جدا. وعلى الرغم من أنه يمكن القول إن وجود المجموعتين الأولى والثانية كجزء من واجهة واحدة مقبول ، فإن إضافة المجموعة الثالثة غير مبرر. إذا كان هذا المشروع بحاجة إلى مزيد من التطوير وأصبح تطبيق Android حقيقيًا ، فستنمو المجموعة الثالثة بما يتناسب بشكل مباشر مع عدد تدفقات المجال في المشروع. همم.
لدينا مصطلح خاص للفئات التي تشترك في العديد من المسؤوليات: الأشياء الإلهية. هذا هو مكافحة النمط على نطاق واسع في تطبيقات Android. يعد Activitie و Fragment من المشتبه بهم القياسيين في هذا السياق ، لكن الفئات الأخرى يمكن أن تتدهور إلى أشياء إلهية أيضًا. خاصة إذا كانت أسمائهم تنتهي بـ "Manager" ، أليس كذلك؟
انتظر ... أعتقد أنني وجدت اسمًا أفضل لـ TasksRepository:
interface TasksManager {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
الآن اسم هذه الواجهة يعكس مسؤولياتها بشكل أفضل بكثير!
مستودعات فقر الدم
هنا قد تسأل: "إذا سحبت منطق المجال من المستودع ، فهل سيحل ذلك المشكلة؟" حسنًا ، عد إلى "الرسم التخطيطي المعماري" من دليل Google.
إذا كنت
completeTaskتريد استخراج طرق ، على سبيل المثال ، من TasksRepository ، فأين ستضعها؟ وفقًا لـ "البنية" الموصى بها من Google ، ستحتاج إلى نقل هذا المنطق إلى أحد نماذج ViewModels الخاصة بك. لا يبدو هذا قرارًا سيئًا ، لكنه كذلك حقًا.
على سبيل المثال ، تخيل أنك تضع هذا المنطق في ViewModel واحد. بعد ذلك ، بعد شهر ، يريد مدير حسابك السماح للمستخدمين بإكمال المهام من شاشات متعددة (هذا مناسب لجميع مديري ToDo الذين استخدمتهم في حياتي). لا يمكن إعادة استخدام المنطق داخل ViewModel ، لذلك تحتاج إما إلى تكراره أو إعادته إلى TasksRepository. من الواضح أن كلا النهجين سيئان.
تتمثل الطريقة الأفضل في استخراج دفق المجال هذا في كائن مخصص ثم وضعه بين ViewModel والمستودع. ثم سيتمكن ViewModels المختلفة من إعادة استخدام هذا الكائن لتنفيذ هذا الموضوع المحدد. تُعرف هذه الكائنات باسم "حالات الاستخدام" أو "التفاعلات"... ومع ذلك ، إذا قمت بإضافة حالات استخدام إلى قاعدة التعليمات البرمجية الخاصة بك ، فستصبح المستودعات نموذجًا عديم الفائدة بشكل أساسي. مهما فعلوا ، سيكون مناسبًا بشكل أفضل لحالات الاستخدام. لقد غطى Gabor Varadi بالفعل هذا الموضوع في هذه المقالة ، لذلك لن أخوض في التفاصيل. أشترك في كل ما قاله تقريبًا عن "مستودعات فقر الدم".
ولكن لماذا تعتبر حالات الاستخدام أفضل بكثير من المستودعات؟ الجواب بسيط: حالات الاستخدام تغلف تدفقات منفصلة. وبالتالي ، بدلاً من مستودع واحد (لكل مفهوم مجال) ينمو تدريجياً إلى كائن إلهي ، سيكون لديك العديد من فئات حالات الاستخدام شديدة التركيز. إذا كان التدفق يعتمد على الشبكة والبيانات المخزنة ، يمكنك تمرير التجريدات المناسبة إلى فئة حالة الاستخدام وسيقوم "بالتحكيم" بين هذه المصادر.
بشكل عام ، يبدو أن الطريقة الوحيدة لمنع تدهور المستودعات إلى الطبقات الإلهية مع تجنب التجريدات غير الضرورية هو التخلص من المستودعات.
مستودعات خارج Android.
الآن قد تتساءل عما إذا كانت المستودعات من اختراع Google. لا ليسو كذلك. تم وصف نمط المستودع قبل وقت طويل من أن تقرر Google استخدامه في دليل الهندسة الخاص بها.
على سبيل المثال ، وصف مارتن فاولر المستودعات في كتابه ، أنماط هندسة تطبيقات المؤسسة. تحتوي مدونته أيضًا على مقال ضيف يصف نفس المفهوم. وفقًا لـ Fowler ، فإن المستودع هو مجرد غلاف حول طبقة التخزين يوفر واجهة استعلام ذات مستوى أعلى وربما تخزين مؤقت في الذاكرة. أود أن أقول أنه من وجهة نظر فاولر ، فإن المستودعات تتصرف مثل ORMs.
وصف إريك إيفانز أيضًا المستودعات في كتابه "التصميم المستند إلى المجال". هو كتب:
, , , — . , . , , .
لاحظ أنه يمكنك استبدال "المستودع" في الاقتباس أعلاه بـ "Room ORM" ولا يزال ذلك منطقيًا. لذلك ، في سياق التصميم المستند إلى المجال ، يكون المستودع عبارة عن ORM (يتم تنفيذه يدويًا أو باستخدام إطار عمل تابع لجهة خارجية).
كما ترى ، لم يتم اختراع المستودع في عالم Android. هذا هو نمط تصميم عاقل جدًا والذي تم بناء جميع أطر عمل إدارة ORM عليه. لاحظ ، مع ذلك ، ما هي المستودعات: لم يجادل أي من "الكلاسيكيات" في أن المستودعات يجب أن تحاول إزالة التمييز بين الوصول إلى الشبكة والوصول إلى قاعدة البيانات.
في الواقع ، أنا متأكد من أنهم سيجدون هذه الفكرة ساذجة ومهزومة للذات. لفهم السبب ، يمكنك قراءة مقال آخر ، هذه المرة بواسطة Joel Spolsky (مؤسس StackOverflow) ، بعنوان"قانون التجريدات المتسربة" . ببساطة: يختلف التواصل الشبكي كثيرًا عن الوصول إلى قاعدة البيانات إلى الملخص بدون تسريبات كبيرة.
كيف أصبح المستودع مضادًا للنمط في Android
فهل أساءت Google تفسير نمط المستودع وقدمت الفكرة الساذجة المتمثلة في تجريد الوصول إلى الشبكة فيه؟ اشك به.
لقد وجدت الرابط الأقدم لهذا المضاد في مستودع GitHub هذا ، وهو للأسف مورد شائع جدًا. لا أعرف ما إذا كان هذا النمط المضاد قد اخترعه هذا المؤلف المعين ، ولكن يبدو أن هذا الريبو هو الذي شاع الفكرة العامة داخل نظام Android البيئي. ربما حصل عليه مطورو Google من هناك أو من أحد المصادر الثانوية.
خاتمة
لذلك ، أصبح المستودع في Android مضادًا للنمط. يبدو جيدًا على الورق ، لكنه يصبح مشكلة حتى في التطبيقات التافهة ويمكن أن يؤدي إلى مشاكل حقيقية في المشاريع الكبيرة.
على سبيل المثال ، في مخطط Google آخر ، هذه المرة للمكونات المعمارية ، أدى استخدام المستودعات في النهاية إلى أحجار كريمة مثل NetworkBoundResource . ضع في اعتبارك أن نموذج المتصفح GitHub لا يزال تطبيقًا صغيرًا ~ 2 KLOC.
بقدر ما أستطيع أن أقول ، فإن "نمط المستودع" كما هو محدد في المستندات الرسمية غير متوافق مع التعليمات البرمجية النظيفة والقابلة للصيانة.
شكرًا على القراءة وكالعادة يمكنك ترك تعليقاتك وأسئلتك أدناه.
