يقرأ الكثير من المبرمجين عن “الأنماط المعمارية” (Architectural Patterns) (مثل المعمارية الطبقية، أو السداسية، إلخ) و"أنماط التصميم" (Design Patterns) (مثل المصنع Factory، أو المراقب Observer، إلخ)، لكنها غالبًا ما تبدو مفاهيمًا مجردة ومنفصلة عن المشاريع البسيطة. فيتساءلون: ما الداعي لكل هذا التعقيد؟
في الواقع، لن تدرك لماذا يُعد نمطٌ ما مفيدًا حتى تتعامل مع المشكلة التي جاء ليحلها.
هذا هو النهج الذي سنتبعه اليوم. سنبدأ بملف واحد “مسطّح” (flat) يحتوي على واجهة برمجة تطبيقات (API) بسيطة. ثم سنواجه تحديات من الواقع العملي تفرض علينا تغيير هذا التصميم البسيط، وتضطرنا إلى إعادة هيكلته (refactoring) خطوة بخطوة. وفي النهاية، سنكون قد “اكتشفنا” بأنفسنا وبتطور تدريجي مدى فائدة المعمارية الطبقية (Layered Architecture).
ملاحظة: هذا المقال مُبسط جدًا. لقد تجاهلنا عن قصد أمورًا مثل معالجة الأخطاء، وتسجيل الأحداث (logging)، والتحقق من البيانات (validation)، والإدارة المثلى لقواعد البيانات. تركيزنا الوحيد ينصب على المعمارية نفسها وسبب أهميتها.
جميع الأمثلة البرمجية مكتوبة بلغة Python، لكن المفهوم بحد ذاته ينطبق على أي لغة. ستجدون أيضًا نسخة بلغة Go في الرابط الموجود أسفل المقال.
أولًا: تطبيق “الملف الواحد”
لنبدأ بواجهة API بسيطة لإدارة جهات الاتصال، موجودة بالكامل في ملف واحد main.py. سنستخدم إطار العمل FastAPI وقاعدة بيانات SQLite.
| |
هذا التصميم يعمل جيدًا كنموذج أولي. لكن، ماذا يحدث عندما يكبر التطبيق؟
ثانيًا: المشكلة الأولى (تغيير قاعدة البيانات)
بعد العمل بهذه البنية المسطحة لفترة (حتى لو قُسِّمَت إلى عدة ملفات)، قررت شركتك أن تدعم قاعدة بيانات MongoDB. هنا تبدأ المعاناة الحقيقية. يظهر أمامنا احتمالان:
- استبدال SQLite بالكامل: كل استدعاء لقاعدة البيانات في المعالجات (handlers) أصبح الآن مرتبطًا بشكل وثيق بـ SQLite. للانتقال إلى MongoDB، سيتعين عليك مراجعة كل دالة، وإعادة كتابة كل استعلام ليناسب MongoDB، ثم إعادة اختبار التطبيق بالكامل. إذا كان تطبيقك يحتوي على مئات العمليات، سيصبح هذا كابوسًا: بطيئًا، وعرضة للأخطاء، ومستحيل الصيانة. وإذا قررت تغيير قاعدة البيانات لاحقًا، ستتكرر نفس المعاناة.
- دعم القاعدتين معًا والتبديل بينهما: قد يبدو السماح بالتبديل (مثلًا عبر متغير بيئة DB_TYPE) خيارًا مرنًا، لكنه يتحول سريعًا إلى فوضى. تخيل أنك تضع شروط if/else للتحقق من نوع قاعدة البيانات في كل معالج (handler)!
الحل: نمط المستودع (The Repository Pattern)
يمكننا حل هذا بفصل منطق قواعد البيانات بالكامل عن بقية أجزاء التطبيق.
الفكرة بشكل بسيط:
- ننشئ فئتين (classes) واحدة لـ SQLite والأخرى لـ MongoDB.
- نجعلهما تتشاركان نفس “الواجهة” (Interface)، أي أنهما تقدمان نفس مجموعة الدوال (methods) (مثل
get_all، إلخ). - نضيف دالة “مصنعية” (Factory Function) بسيطة (مثل
get_repository()) تقرأ الإعدادات (مثلًا من متغير بيئة) وترجع نسخة من المستودع المناسب.
بعد هذا التعديل، لن تعنى دوال المسارات (routes) بنوع قاعدة البيانات المستخدمة. فقط ستتحدث مع “الواجهة”، والتطبيق يقرر من خلف الكواليس أي قاعدة بيانات سيستخدم.
أولاً، نعرّف “واجهة” (interface) (باستخدام abc في Python) تصف ماذا نريد أن نفعل، وليس كيف.
| |
الآن، نقوم بإنشاء تطبيقات ملموسة (concrete implementations) لهذه الواجهة.
| |
والأمر نفسه لـ MongoDB:
| |
أخيرًا، ننشئ “مصنعًا” (factory) يمكن لـ FastAPI استخدامه. هذا المصنع يقرأ متغير البيئة ويمنحنا التطبيق الصحيح.
| |
الآن، انظر كيف أصبحت دوال المسارات في ملف main.py نظيفة ومبسطة:
| |
لم تعد دوال المسارات تحتوي على أي منطق متعلق بقاعدة البيانات. بل فقط تعتمد على الواجهة IContactRepository. يمكننا الآن تبديل قاعدة بياناتنا بمجرد تغيير متغير بيئة، دون أن نلمس كود دوال المسارات نهائيًا.
ثالثًا: المشكلة الثانية (إضافة “نقاط دخول” جديدة)
لاحقًا، يُطلب منك تشغيل نفس التطبيق على منصات مختلفة: كدالة على AWS Lambda، أو Azure Functions، أو GCP Cloud Functions، مع الإبقاء على FastAPI للتشغيل المحلي (on-prem).
لكن أين يوجد “منطق العمل” (Business Logic) الخاص بنا؟ فلنلقِ نظرة على الدالة list_full_name_initials.
| |
إذا أردنا بناء معالج (handler) لخدمة Lambda، سنضطر إلى نسخ ولصق هذا المنطق:
| |
إذا واصلت وضع منطق العمل داخل كل معالج (handler)، فسينتهي بك الأمر بتكرار نفس الكود في كل مكان.
هذا سيء جدًا. فمثلًا إذا احتجنا لتغيير طريقة حساب الأحرف الأولى من الاسم، سيتعين علينا تغييرها في مكانين (أو أكثر). بالتالي ستكون الصيانة والاختبار وتصحيح الأخطاء عملية مؤلمة وغير عملية.
الحل: طبقة الخدمة (The Service Layer)
الحل الطبيعي هو عزل “منطق العمل” ووضعه في فئة (class) مستقلة، سنطلق عليها “طبقة الخدمة” (Service Layer) (أو “طبقة العمل” Business Layer). وظيفة هذه الطبقة هي إدارة وتنفيذ المهام.
الأهم من ذلك، أن طبقة الخدمة الجديدة هذه ستعتمد على واجهة المستودع (Repository interface)، وليس على FastAPI أو أي اتصال مباشر بقاعدة البيانات.
| |
انظر الآن إلى ملف main.py. لقد أصبح صغيرًا وبسيطًا للغاية.
| |
وماذا عن معالج Lambda؟ أصبح بسيطًا بنفس القدر، ويعيد استخدام نفس الكود تمامًا!
| |
إذا غيّرنا منطق حساب الأحرف الأولى في ContactService، سيُطبّق التعديل تلقائيًا على تطبيق FastAPI، وعلى دالة Lambda، وعلى أي مكان آخر يستخدم هذه الخدمة.
رابعًا: الطبقات نشأت بشكل تلقائي
تهانينا! لقد خطوت للتو نحو تطبيق المعمارية الطبقية (Layered Architecture).
دون الدخول في نظريات معقدة، نجحنا بشكل تدريجي وتلقائي في فصل تطبيقنا إلى طبقات مميزة، لكل منها مسؤولية واحدة محددة:
طبقة العرض (Presentation Layer)
- تُعرف أيضًا بـ: Controllers, Handlers, Adapters.
- المسؤولية: التعامل مع مدخلات ومخرجات المستخدم (مثل HTTP، واجهة الأوامر CLI).
طبقة العمل (Business Layer)
- تُعرف أيضًا بـ: Services, Application, Use Cases.
- المسؤولية: احتواء قواعد العمل الأساسية (core business rules) وسير الإجراءات.
طبقة الوصول للبيانات (Data Access Layer)
- تُعرف أيضًا بـ: Repositories, DAO, Persistence.
- المسؤولية: التخاطب مع قواعد البيانات، وإخفاء تفاصيل تخزين البيانات.
طبقة النماذج (Models Layer)
- تُعرف أيضًا بـ: Entities, DTOs, Schemas
- المسؤولية: تحديد هياكل البيانات وقواعد التحقق الخاصة بها.
قد تحتاج أيضًا لإضافة طبقات أخرى عند الضرورة، على سبيل المثال:
- طبقة البنية التحتية (Infrastructure Layer): للمهام المساعدة التي تخدم كل الطبقات مثل تسجيل الأحداث (logging)، أو التخزين المؤقت (caching)، أو المصادقة (authentication)، أو قراءة الإعدادات.
- طبقة التكامل (Integration Layer): للتعامل مع الأنظمة الخارجية (مثل إرسال البريد الإلكتروني، أو استدعاء خدمات خارجية).
فهم تدفق الاعتماديات (Dependency Flow)
تتبع المعمارية الطبقية تدفقًا من الأعلى للأسفل في الاعتماديات (Dependencies):

يجب أن تتدفق الاعتماديات دائمًا إلى الأسفل، وليس العكس. على سبيل المثال، يجب ألّا تقوم طبقة المستودع (Repository) باستيراد أي شيء من FastAPI أو إرسال استجابات HTTP.
- طبقة العرض (مثل
main.pyأوlambda_handler.py):- تعرف عن: طبقة الخدمة (Service Layer).
- لا تعرف عن: كيفية عمل قاعدة البيانات.
- طبقة العمل (مثل
services/contact_service.py):- تعرف عن: واجهة المستودع (Repository Interface) والنماذج (Models).
- لا تعرف عن: FastAPI، أو HTTP، أو JSON، أو ما إذا كانت قاعدة البيانات SQLite أم Mongo.
- طبقة الوصول للبيانات (مثل
repository/sqlite.py):- تعرف عن: تفاصيل قاعدة البيانات (SQL، Mongo، إلخ).
- لا تعرف عن: منطق العمل (هي لا تعرف لماذا تطلب البيانات)، أو طبقة العرض.
الفوائد الكبيرة
- قابلية الاختبار (Testability): هو أحد أكبر المكاسب. فأنت لا تحتاج قاعدة بيانات حقيقية عند اختبار منطق عمل
ContactService! كل ما عليك هو تمرير مستودع “مزيف” (Mock Repository) لها.
| |
- قابلية الصيانة (Maintainability): التغييرات في طبقة نادرًا ما تؤثر على الطبقات الأخرى. هل تريد تغيير قاعدة البيانات من SQLite إلى PostgreSQL؟ كل ما عليك فعله هو تعديل ملف واحد (
repository/postgres.py) وتحديث المصنع (Factory). طبقة الخدمة وطبقة العرض لا تتغيران. - إعادة الاستخدام (Reusability): كما رأينا، يمكنك استخدام
ContactServiceنفسها في تطبيق FastAPI، أو دالة Lambda، أو حتى في برنامج يعمل من سطر الأوامر (CLI)، كل ذلك دون أي تغيير في منطق العمل. - قابلية التوسع (Scalability): مع نمو النظام، تتيح لك إضافة طبقات جديدة (مثل “البنية التحتية”) الحفاظ على فصل الاهتمامات (separation of concerns) وإبقاء النظام سهل الإدارة.
- قابلية القراءة (Readability): كل طبقة لها غرض واضح ومحدد، مما يجعل فهم المشروع وتتبعه أسهل.
تحسينات أخرى
ستحتاج غالبًا إلى تغييرات إضافية عند العمل على تطبيق حقيقي، مثل:
- إنشاء مستودعات وخدمات متخصصة (مثل
UserRepository,OrderService). - حقن (Inject) المستودعات المحددة التي تحتاجها كل خدمة (لتقييد الصلاحيات).
- إضافة المزيد من “التجريد” (Abstraction) عند الضرورة. في مثالنا، كانت الواجهة
IContactRepositoryضرورية لأنها حلت مشكلة “تبديل قاعدة البيانات”. لكننا لم ننشئ واجهةIContactServiceلطبقة الخدمةContactServiceلأننا نملك تطبيقًا واحدًا فقط لها. (بعض المدارس المعمارية تفضل تجريد كل شيء دائمًا، لكننا لن ندخل في هذا النقاش هنا. هذا المقال يعرض حالة استخدمت التجريد وأخرى لم تستخدمه). - تجريد عملية الاتصال بقاعدة البيانات، بحيث لا تضطر المستودعات لفتح الاتصال وإغلاقه في كل دالة.
- إضافة طبقة (Domain) منفصلة للنماذج (
models.py) لتستخدمها جميع الطبقات.
خاتمة
المعمارية (Architecture) هي مجموعة من الأنماط التي تطورت كحلول عملية لمشاكل شائعة ومؤلمة. في المرة القادمة التي تبدأ فيها مشروعًا، قد لا تحتاج لكل هذه الطبقات من البداية. لكن بمعرفتك للمعاناة التي تحلها هذه الأنماط، ستعرف تمامًا متى ولماذا يجب عليك تطبيقها.
يمكنك استكشاف المثال الكامل هنا (ملحوظة: هناك اختلاف بسيط بين نسختي Python و Go بغاية زيادة التنوع في المثال):
