عندما تتوقف MVVM و MVC عن الإنقاذ، وتتحول الاختبارات إلى جحيم
هل سبق لك أن كتبت تطبيقًا بلغة Swift، وبعد ستة أشهر كان إضافة ميزة واحدة يكسر ثلاث ميزات أخرى، وكان عليك إعادة كتابة الاختبارات بالكامل لأن قاعدة البيانات تغيرت من Core Data إلى Realm؟ إذا كانت الإجابة بنعم، فأنت لست وحدك. الهندسات المعمارية الطبقية التقليدية (MVC, MVVM) تعمل بشكل رائع في البداية، لكنها تتحول إلى "فوضى" مع نمو المشروع.
سأريكم الهندسة السداسية (المعروفة أيضًا باسم Ports & Adapters) - وهو نهج جاء من عالم Java و DDD، ولكنه يتناسب تمامًا مع Swift. سنقوم بتحليلها بأمثلة حقيقية، بدون "User" و "Manager" المجردين. سترى كيف تفصل منطق الأعمال عن قاعدة البيانات، واجهة المستخدم، والشبكة بحيث يمكن تغييرها دون لمس نواة التطبيق.
هذه ليست نظرية أخرى - إنها ممارسة يمكنك تطبيقها في مشروعك اليوم.
ما هي الهندسة السداسية؟ بكلمات بسيطة
تخيل شكلًا سداسيًا (ومن هنا جاء الاسم). في المنتصف - منطق أعمالك النقي (المجال). على الحواف - المنافذ (الواجهات) التي يتواصل من خلالها العالم الخارجي مع النواة. المحولات هي تطبيقات محددة: قاعدة البيانات، API، واجهة المستخدم، نظام الملفات.
القاعدة الأساسية: النواة لا تعرف شيئًا عن العالم الخارجي. لا عن UIKit، ولا عن URLSession، ولا عن Core Data. إنها تعرف فقط واجهاتها (المنافذ).
مثال من الحياة الواقعية: تخيل متجرًا إلكترونيًا. النواة تعرف أن هناك "سلة تسوق" و "إتمام الطلب". لا يهمها من أين تأتي البيانات - من WebSocket، من JSON محلي، أو من لوحة المفاتيح. تتصل المحولات بالمنافذ وتترجم الإشارات الخارجية إلى أوامر مجال.
لماذا يجب استخدام هذا في مشاريع Swift؟
- قابلية الاختبار - تختبر المجال بدون قاعدة بيانات حقيقية أو شبكة. فقط تستبدل المحولات بـ mocks.
- المرونة - أردت الانتقال من Firebase إلى خادمك الخاص؟ تغير محولًا واحدًا، وليس نصف التطبيق.
- قابلية القراءة - منطق الأعمال غير مبعثر عبر ViewControllers. إنه موجود في مكان واحد.
- الاستقلال عن الأطر - نواتك لا تستورد UIKit أو Combine أو Alamofire.
هيكل المشروع على الهندسة السداسية في Swift
دعنا ننتقل مباشرة إلى الكود. لنفترض أننا نكتب تطبيقًا لإدارة المهام (Todolist). إليك كيف سيكون شكل هيكل المجلدات:
App/├── Domain/│ ├── Entity/│ │ └── Task.swift│ ├── UseCase/│ │ └── CreateTaskUseCase.swift│ └── Port/│ ├── TaskRepository.swift│ └── TaskOutputPort.swift├── Adapter/│ ├── Persistence/│ │ └── CoreDataTaskRepository.swift│ ├── Network/│ │ └── APITaskRepository.swift│ └── Presentation/│ └── TaskListViewModel.swift└── Main/ └── AppDelegate.swiftلاحظ: Domain لا يستورد أي شيء باستثناء Foundation. Adapter يمكنه استيراد CoreData، UIKit، Alamofire - أي شيء.
الخطوة 1: كيانات المجال والمنافذ
لنبدأ بالأهم - نموذج المجال. هذا هو struct أو enum يصف كيان الأعمال. لا NSManagedObject، لا Codable.
// Domain/Entity/Task.swiftstruct Task { let id: UUID var title: String var isCompleted: Bool let createdAt: Date}الآن نحدد منفذ المستودع (منفذ الإخراج). هذا بروتوكول يصف كيف نحفظ ونحمل المهام.
// Domain/Port/TaskRepository.swiftprotocol TaskRepository { func fetchAll() async throws -> [Task] func save(task: Task) async throws func delete(taskID: UUID) async throws}و منفذ الإدخال - وهو ما يتفاعل من خلاله العالم الخارجي (UI، API) مع النواة. عادةً ما يكون UseCase.
// Domain/Us