DIP (Dependency Inversion Principle) — المبدأ الخامس من SOLID الذي يحدد قواعد بناء التبعيات بين الوحدات: لا يجب أن تعتمد الوحدات عالية المستوى على الوحدات منخفضة المستوى، بل يجب أن يعتمد كلاهما على التجريدات. لا يجب أن تعتمد التجريدات على التفاصيل — يجب أن تعتمد التفاصيل على التجريدات. هذا المبدأ، الذي وصفه روبرت مارتن في Clean Architecture (2017)، هو أساس الهندسة المعمارية ضعيفة الاقتران. وفقاً لهذا الكتاب، مبدأ عكس التبعيات يزيل الروابط الصلبة بين طبقات التطبيق.
النقاط الرئيسية
DIP (Dependency Inversion Principle) هو مبدأ عكس التبعية الذي يقلب النظرة التقليدية لاتجاه التبعيات بين الوحدات. الوحدات عالية المستوى (منطق الأعمال) لا يجب أن تعتمد مباشرة على وحدات منخفضة المستوى (قاعدة بيانات، شبكة، واجهة مستخدم). بدلاً من ذلك، يعتمد كلا المستويين على تجريدات معرفة في الوحدة عالية المستوى.
الصيغة الرسمية لـ DIP تتضمن قاعدتين: أ — الوحدات عالية المستوى لا يجب أن تعتمد على وحدات منخفضة المستوى، بل يجب أن يعتمد كلاهما على التجريدات. ب — التجريدات لا يجب أن تعتمد على التفاصيل، التفاصيل يجب أن تعتمد على التجريدات. القاعدة الثانية ناتجة عن الأولى: إذا اعتمد التجريد على التفاصيل، فلا يمكن أن يكون أساساً مستقراً لوحدة عالية المستوى.
بدون DIP، تبدو الهندسة المعمارية النموذجية هكذا: BusinessLogic → DatabaseRepository — منطق الأعمال يعتمد مباشرة على مستودع محدد. مع DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic لا يعلم بوجود DatabaseRepository، إنه يعرف فقط واجهة DatabaseService التي يتم تنفيذها خارج منطق الأعمال.
العكس يعني أن تدفق التحكم وتدفق التبعيات يتجهان في اتجاهين متعاكسين. تدفق التحكم يتجه من الأعلى للأسفل: UI → ViewModel → UseCase → Repository. تدفق التبعيات يتجه من الأسفل للأعلى: Repository ينفذ واجهة معرفة في UseCase. Repository (مستوى منخفض) يعتمد على UseCase (مستوى عال).
هذا العكس هو الفرق الرئيسي بين DIP والفصل التقليدي إلى طبقات. في الهندسة المعمارية التقليدية متعددة الطبقات، كل طبقة تعتمد على الطبقة التي تحتها. في الهندسة المعمارية مع DIP، جميع الطبقات تعتمد على التجريدات، بينما تنفيذ هذه التجريدات يوجد في طبقة البنية التحتية، والتي “تُوصَّل” بالطبقات العليا عبر آليات DI.
آلية DIP يتم تنفيذها من خلال تعريف التجريدات في الوحدات عالية المستوى وتنفيذها في الوحدات منخفضة المستوى. الوحدة عالية المستوى تعلن عن واجهة للوظيفة التي تحتاجها. الوحدة منخفضة المستوى تنفذ هذه الواجهة. التجميع (wiring) يحدث في جذر التركيب (composition root) للتطبيق.
عملية إدخال DIP في كود موجود: استخراج واجهة للوحدة منخفضة المستوى، نقل هذه الواجهة إلى الوحدة عالية المستوى (أو إلى طبقة تجريد منفصلة)، إعادة كتابة اعتماد الوحدة عالية المستوى لاستخدام الواجهة، جعل الوحدة منخفضة المستوى تنفذ هذه الواجهة. بعد هذه الخطوات، اتجاه التبعية قد تغير إلى العكس.
DIP يتطلب وجود آلية جذر التركيب — نقطة في التطبيق حيث يتم إنشاء جميع التبعيات وربطها معاً. في Android هذا هو Application.get() أو مكون Hilt، في iOS — AppDelegate أو SceneDelegate. جذر التركيب هو المكان الوحيد الذي يعرف فيه الكود عن التطبيقات الملموسة.
DIP ينشئ حدوداً معمارية بين طبقات التطبيق. عندما يعتمد ViewModel على واجهة UserRepository، يتشكل حد بين طبقة العرض وطبقة المجال: ViewModel (طبقة العرض) لا يعرف من أين تأتي البيانات. هذا الحد يسمح بتغيير تنفيذ UserRepository (Room → REST → Mock) دون التأثير على ViewModel. كلما زادت هذه الحدود، كلما كان التطبيق أكثر مقاومة لتغييرات الأطر والمكتبات.
في هندسة Android الموصى بها من Google، يتم تنفيذ DIP عبر UseCases الموجودة في طبقة المجال والتي تعتمد على واجهات Repository. RepositoryImpl موجودة في طبقة البيانات وتنفذ هذه الواجهات. طبقة العرض (ViewModel) تعتمد على UseCases. اتجاه التبعيات يذهب من العرض إلى المجال، من المجال إلى البيانات — لكن لا توجد طبقة تعرف عن التطبيقات الملموسة لطبقة أخرى.
DIP و DI غالباً ما يتم الخلط بينهما، لكنهما مفهومان مختلفان. DIP هو مبدأ معماري (ماذا نفعل: نعتمد على التجريدات). DI هو نمط تنفيذ (كيف نفعل ذلك: تمرير التبعيات عبر المنشئ). DIP يجيب على سؤال “على ماذا يجب أن تستند الوحدات؟”، DI يجيب على “كيف تحصل الكائنات على تبعياتها؟”.
حقن التبعية (Dependency Injection) هو طريقة لحقن التبعيات في كائن عبر المنشئ أو الطريقة أو الخاصية. عندما تستقبل كلاس Kotlin واجهة Repository عبر منشئها — هذا هو DI. أما حقيقة أن كلاس ViewModel يعتمد على واجهة Repository وليس على تنفيذ محدد لـ RoomRepository — فهذا هو DIP. DI هو الأداة، DIP هو الهدف.
يمكنك اتباع DIP بدون إطار عمل DI: التجميع اليدوي للتبعيات في جذر التركيب هو أيضاً DI (DI يدوي). يمكنك استخدام إطار عمل DI (Dagger، Hilt، Koin) مع انتهاك DIP: إذا قام ViewModel بإنشاء كائن Repository مباشرة عبر new() — يتم انتهاك DIP، حتى لو كان الإطار مثبتاً. DIP هو قرار معماري، DI هو تفصيل تقني.
لنلق نظرة على مثال Android لتطبيق DIP على طبقة البيانات. بدون DIP، ViewModel ينشئ مباشرة RoomDatabase و DAO. مع DIP — ViewModel يعتمد على واجهة UserRepository، والتنفيذ الملموس RoomUserRepository يتم توفيره من الخارج.
// التجريد ينتمي إلى طبقة المجال (u0645ستوى عال)
interface UserRepository {
fun getUser(id: Int): User
}
// طبقة المجال تعتمد فقط على التجريد
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// التنفيذ في طبقة البيانات يعتمد على تجريد طبقة المجال
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// جذر التركيب
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
مثال iOS مع Application Coordinator وبروتوكول للملاحة:
// تجريد التنقل في طبقة المجال
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel يعتمد على التجريد، ليس على UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (طبقة UIKit) ينفذ بروتوكول طبقة المجال
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// كود التنقل UIKit
}
func navigateToLogin() {
// كود التنقل UIKit
}
}
النقطة الرئيسية: AuthViewModel (المجال) لا يعلم بوجود AppCoordinator (UIKit). إنه يعرف فقط بروتوكول AuthNavigation. إذا تم استبدال UIKit بـ SwiftUI غداً — AuthViewModel لا يتطلب تغييرات. DIP يجعل طبقة المجال مستقلة عن أطر ومكتبات واجهة المستخدم.
Hilt هو أداة DI القياسية لـ Android، الموصى بها من Google. مدمجة في Jetpack، تدعم ViewModel و Fragment و Service ومكونات Android الأخرى. Hilt تؤتمت إنشاء جذر التركيب من خلال التعليقات التوضيحية @Module و @Provides و @Inject. استخدام Hilt لا يضمن الامتثال لـ DIP — يجب تعريف واجهة UserRepository في طبقة المجال، وليس في طبقة البيانات.
Koin هو إطار عمل DI خفيف لـ Kotlin بدون توليد كود أو معالجة تعليقات توضيحية. DSL الخاص بـ Koin (module, single, factory) أسهل في التعلم، لكن التحقق من التبعيات يحدث في وقت التشغيل وليس في وقت الترجمة. Koin شائع في المشاريع متعددة المنصات (KMP) بفضل دعم iOS.
Dagger 2 هو سلف Hilt، لا يزال مستخدماً في المشاريع الكبيرة. Dagger يولد كود DI في وقت الترجمة، مما يوفر أقصى أداء وتشخيص للأخطاء في وقت البناء. Hilt مبني فوق Dagger ويوفر واجهة برمجة مبسطة. للمشاريع الجديدة، توصي Google بـ Hilt كإطار DI رئيسي.
وحدات DI يجب أن تتوافق مع الطبقات المعمارية وتُقسم إلى DomainModule و DataModule و PresentationModule. DomainModule يوفر فقط التجريدات و UseCases. DataModule يوفر تطبيقات للتجريدات. PresentationModule يربط ViewModels بـ UseCases. هذا التنظيم يضمن أن طبقة المجال تبقى مستقلة عن مكتبات البنية التحتية.
عند الانتقال بين أطر DI (مثلاً من Koin إلى Hilt)، هيكل DomainModule لا يتغير — تتغير فقط طرق الربط في DataModule و PresentationModule. DIP يضمن عزل منطق المجال، بينما إطار DI هو آلية ربط تقنية.
الأسئلة الشائعة
DIP ضروري على الحدود المعمارية — بين طبقات التطبيق (المجال → البيانات، العرض → المجال). داخل طبقة واحدة، قد يكون DIP مبالغاً فيه. على سبيل المثال، كلاس أدوات StringFormatter داخل طبقة المجال لا يتطلب واجهة — إذا لم يكن هناك سبب لاستبداله.
لا. DIP هو مبدأ: يجب أن تعتمد الوحدات على التجريدات. DI هو نمط: الكائن يتلقى التبعيات من الخارج بدلاً من إنشائها بنفسه. DI هو طريقة لتنفيذ DIP، لكن يمكن اتباع DIP بدون DI (عبر المصانع أو service locator). DI بدون DIP ممكن لكن ليس له قيمة معمارية.
الواجهات تنتمي إلى الوحدة التي تستخدمها، وليس إلى الوحدة التي تنفذها. UserRepository يُعلن في طبقة المجال ويُطبق في طبقة البيانات. هذه هي القاعدة الرئيسية لـ DIP: مالك التجريد هو المستهلك، وليس مزود التنفيذ.
DIP يجعل الاختبار ممكناً على طبقات معزولة. ViewModel الذي يعتمد على UserRepository (واجهة) يمكن اختباره مع تطبيق وهمي (mock) بدون قاعدة بيانات. بدون DIP، ViewModel سيعتمد على RoomUserRepository وسيتطلب إعداد قاعدة بيانات لكل اختبار. DIP + DI يوفران عزلاً كاملاً للوحدات أثناء الاختبار.
Hilt هو الخيار القياسي لمشاريع Android، الموصى به من Google. Koin هو بديل لمشاريع Kotlin Multiplatform. Dagger 2 للمشاريع الحالية حيث الهجرة إلى Hilt غير مبررة. اختيار إطار لا يلغي ضرورة اتباع DIP على المستوى المعماري.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.