يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.
قبل سنوات، في بدايات مسيرتي المهنية، كنت أعمل مع فريق صغير على مشروع ناشئ واعد. كنا شعلة من الحماس، نكتب الكود ليل نهار، والهدف واحد: إطلاق المنتج بأسرع وقت ممكن. اخترنا وقتها قاعدة بيانات MongoDB لأنها كانت “تريند” وسريعة وسهلة للمشاريع الجديدة. كان كل شيء يسير على ما يرام، وكتبنا آلاف الأسطر من الكود، رابطين منطق العمل (Business Logic) مباشرةً بأوامر Mongoose (مكتبة التعامل مع MongoDB في Node.js).
وفي يوم مشؤوم، جاء المدير التقني بقرار “بسيط” من وجهة نظره: “يا شباب، قررنا ننتقل لقاعدة بيانات PostgreSQL”. صمت رهيب عمَّ الغرفة. نظرنا إلى بعضنا البعض، وفي عيوننا نظرة رعب حقيقية. أتذكر أني قلت لصاحبي بصوت خافت: “يا ويلي.. هاي كل الشغل بده إعادة كتابة من الصفر!”.
كانت المشكلة أن كود قاعدة البيانات متداخل ومُقترن بشدة مع كل جزء من التطبيق. كل دالة في منطق العمل كانت تحتوي على User.findOne(...) أو Product.save(). تغيير قاعدة البيانات لم يكن يعني تغيير بضعة ملفات، بل كان يعني الدخول إلى قلب التطبيق وإجراء عملية جراحية معقدة وخطيرة، ستستغرق شهورًا من العمل الشاق والممل. كان كابوسًا حقيقيًا تعلمت منه درسًا لن أنساه ما حييت.
هذه القصة المؤلمة هي مدخلنا اليوم للحديث عن طوق النجاة: المعمارية النظيفة (Clean Architecture).
ما هو “الاقتران المحكم” (Tight Coupling) وكيف يسبب الكوابيس؟
قبل أن نتحدث عن الحل، دعونا نفهم أصل المشكلة. المشكلة تسمى “الاقتران المحكم” أو “Tight Coupling”. تخيل أنك تبني سيارة، وبدلًا من تركيب المحرك ببراغي تسمح بفكّه وتغييره، قمت بلحامه مباشرةً بهيكل السيارة. ماذا سيحدث لو تعطل المحرك أو أردت ترقيته؟ ستحتاج إلى قص الهيكل وتدمير أجزاء من السيارة لتغييره. هذا بالضبط ما يحدث في البرمجيات.
عندما تكتب كودًا يعتمد مباشرة على تفاصيل خارجية مثل قاعدة بيانات معينة، أو واجهة برمجية (API) لخدمة خارجية، أو حتى إطار عمل (Framework) معين، فأنت تقوم “بلحام” هذه الأجزاء بقلب تطبيقك.
مثال على الكود السيء (الاقتران المحكم)
لنفترض أن لدينا دالة في تطبيق Express.js لجلب معلومات مستخدم. في الطريقة التقليدية المقترنة بشدة، قد يبدو الكود هكذا:
// user.controller.js
// هذا الكود مقترن بشدة مع Mongoose و MongoDB
import { Request, Response } from 'express';
import { UserModel } from './user.model.mongoose'; // اقتران مباشر بنموذج Mongoose
export const getUserById = async (req: Request, res: Response) => {
try {
const userId = req.params.id;
// منطق العمل ممزوج مباشرةً مع استدعاء قاعدة البيانات
const user = await UserModel.findById(userId).select('-password');
if (!user) {
return res.status(404).json({ message: 'المستخدم غير موجود' });
}
// منطق العرض مقترن مباشرةً بـ Express
return res.status(200).json(user);
} catch (error) {
console.error(error);
return res.status(500).json({ message: 'خطأ في الخادم' });
}
};
لاحظ الكارثة هنا: هذه الدالة تعرف كل شيء! تعرف عن Express (من خلال req و res)، وتعرف عن Mongoose (من خلال UserModel.findById)، وتعرف عن بنية قاعدة البيانات (select('-password')). لو أردنا تغيير قاعدة البيانات إلى PostgreSQL، سنحتاج لإعادة كتابة هذه الدالة بالكامل. ولو أردنا استخدام هذا المنطق في واجهة سطر أوامر (CLI) بدلًا من واجهة ويب، فسنواجه مشكلة أيضًا.
المعمارية النظيفة: ليست مجرد مصطلح رنان، بل هي طوق النجاة
المعمارية النظيفة، التي شاعت بفضل “العم بوب” (Robert C. Martin)، هي فلسفة لتنظيم الكود تهدف إلى تحقيق هدف بسيط ولكنه قوي: فصل الاهتمامات (Separation of Concerns). الفكرة هي بناء التطبيق على شكل طبقات، مثل طبقات البصل، حيث تكون القواعد الأساسية والأكثر أهمية في المركز، والتفاصيل والأدوات في الخارج.
القاعدة الذهبية في هذه المعمارية هي قاعدة الاعتمادية (The Dependency Rule):
الاعتماديات في الكود يجب أن تشير دائمًا إلى الداخل. لا يمكن لطبقة داخلية أن تعرف أي شيء عن طبقة خارجية.
هذا يعني أن منطق العمل (الطبقة الداخلية) يجب ألا يعرف أي شيء عن قاعدة البيانات أو واجهة المستخدم (الطبقات الخارجية). قاعدة البيانات هي “تفصيل”، واجهة المستخدم هي “تفصيل”. قلب التطبيق هو منطق العمل وقواعده.
طبقات المعمارية النظيفة (بشكل مبسط)
- الكيانات (Entities): هي كائنات العمل الأساسية في نظامك (مثل User, Product). لا تحتوي على أي كود يتعلق بأي إطار عمل أو قاعدة بيانات. مجرد بيانات وقواعد عمل عامة.
- حالات الاستخدام (Use Cases): هي التي تنفذ منطق العمل الخاص بالتطبيق (مثل CreateUser, GetProductDetails). هي التي تنسق تدفق البيانات من وإلى الكيانات.
- محولات الواجهات (Interface Adapters): هذا هو المكان الذي يتم فيه تحويل البيانات من الصيغة المناسبة لحالات الاستخدام إلى الصيغة المناسبة للأدوات الخارجية (قاعدة بيانات، واجهة ويب، …الخ) والعكس. هنا تعيش المتحكمات (Controllers) والمقدمات (Presenters) والمستودعات (Repositories).
- الأطر والأدوات (Frameworks & Drivers): هذه هي الطبقة الخارجية تمامًا. هنا توجد قاعدة البيانات (MongoDB, PostgreSQL)، إطار العمل (Express)، واجهة المستخدم (React, Angular)، إلخ. هذه كلها تفاصيل يمكن استبدالها.
كيف طبقت هذا المبدأ لإنقاذ مشروعي؟ (مثال عملي)
دعونا نعد بناء المثال السابق باستخدام مبادئ المعمارية النظيفة، والتي تُعرف أيضًا باسم “Ports and Adapters” (المنافذ والمحولات).
الخطوة الأولى: تعريف “المنفذ” (Port) عبر الواجهة (Interface)
بدلًا من الاعتماد على تنفيذ قاعدة بيانات معين، سنقوم بتعريف “عقد” أو “واجهة” (Interface) تصف ما يحتاجه تطبيقنا من طبقة البيانات. هذا العقد هو “المنفذ” (Port). لا يهتم هذا العقد بكيفية تنفيذ هذه العمليات، بل فقط بماهيتها.
// src/core/repositories/IUserRepository.ts
// هذا هو "المنفذ" أو العقد
// يصف ما نريده، وليس كيف نحصل عليه
import { User } from '../entities/User';
export interface IUserRepository {
findById(id: string): Promise;
findByEmail(email: string): Promise;
save(user: User): Promise;
}
لاحظ أن هذه الواجهة نقية تمامًا. لا يوجد فيها أي ذكر لـ MongoDB أو PostgreSQL.
الخطوة الثانية: بناء “المحولات” (Adapters) لكل قاعدة بيانات
الآن، سنقوم بإنشاء “محولات” (Adapters) تقوم بتنفيذ هذه الواجهة، كل محول مخصص لتقنية معينة.
المحول الأول: MongoDB Adapter
// src/infrastructure/repositories/MongoUserRepository.ts
import { IUserRepository } from '../../core/repositories/IUserRepository';
import { User } from '../../core/entities/User';
import { UserModel } from '../database/mongoose/user.model'; // نموذج Mongoose
export class MongoUserRepository implements IUserRepository {
async findById(id: string): Promise {
const userDoc = await UserModel.findById(id);
// تحويل من مستند Mongoose إلى كيان العمل النظيف
return userDoc ? new User(userDoc.id, userDoc.name, userDoc.email) : null;
}
async findByEmail(email: string): Promise {
// ... تنفيذ مشابه
}
async save(user: User): Promise {
// ... تنفيذ مشابه
}
}
المحول الثاني: PostgreSQL Adapter (افتراضي)
// src/infrastructure/repositories/PostgresUserRepository.ts
import { IUserRepository } from '../../core/repositories/IUserRepository';
import { User } from '../../core/entities/User';
import { db } from '../database/postgres/client'; // عميل Prisma أو أي ORM آخر
export class PostgresUserRepository implements IUserRepository {
async findById(id: string): Promise {
const userRecord = await db.user.findUnique({ where: { id } });
// تحويل من سجل قاعدة البيانات إلى كيان العمل النظيف
return userRecord ? new User(userRecord.id, userRecord.name, userRecord.email) : null;
}
// ... تنفيذ باقي الدوال
}
الخطوة الثالثة: استخدام “حالات الاستخدام” (Use Cases) بمعزل عن التفاصيل
الآن، “حالة الاستخدام” (Use Case) التي تحتوي على منطق العمل ستعتمد على الواجهة IUserRepository، وليس على أي تنفيذ محدد. سنستخدم تقنية “حقن التبعية” (Dependency Injection) لتمرير التنفيذ المطلوب.
// src/core/use-cases/GetUserById.ts
import { User } from '../entities/User';
import { IUserRepository } from '../repositories/IUserRepository';
export class GetUserByIdUseCase {
// نعتمد على الواجهة، وليس على التنفيذ!
constructor(private userRepository: IUserRepository) {}
async execute(userId: string): Promise {
// منطق عمل نقي وخالٍ من تفاصيل قاعدة البيانات
if (!userId) {
throw new Error('معرّف المستخدم مطلوب');
}
const user = await this.userRepository.findById(userId);
return user;
}
}
هذا الكود نظيف، قابل للاختبار بسهولة، والأهم من ذلك، لا يعرف شيئًا عن قاعدة البيانات المستخدمة.
لحظة الحقيقة: تبديل قاعدة البيانات بـ “كبسة زر”
الآن، كيف نغير قاعدة البيانات من MongoDB إلى PostgreSQL؟ الأمر بسيط جدًا ويتم في مكان واحد فقط، وهو المكان الذي يتم فيه تجميع أجزاء التطبيق معًا (يُعرف بـ Composition Root).
// src/main.ts أو app.ts
import { GetUserByIdUseCase } from './core/use-cases/GetUserById';
import { MongoUserRepository } from './infrastructure/repositories/MongoUserRepository';
import { PostgresUserRepository } from './infrastructure/repositories/PostgresUserRepository';
// ... إعدادات أخرى للتطبيق
// للاستخدام مع MongoDB
const userRepository = new MongoUserRepository();
// // *** للتبديل إلى PostgreSQL، كل ما عليك فعله هو تغيير هذا السطر! ***
// const userRepository = new PostgresUserRepository();
const getUserByIdUseCase = new GetUserByIdUseCase(userRepository);
// الآن يمكنك استخدام getUserByIdUseCase في المتحكم (Controller) الخاص بك
// وهو لا يعلم أي قاعدة بيانات يتم استخدامها في الخلفية
وهكذا، أصبح تغيير قاعدة بيانات بأكملها مجرد تغيير سطر واحد من الكود. لا حاجة للمس منطق العمل أو حالات الاستخدام على الإطلاق. هذا هو جمال وقوة المعمارية النظيفة.
نصائح من “الختيار” أبو عمر
- ابدأ بالواجهات (Interfaces): قبل كتابة أي كود للتواصل مع العالم الخارجي، اسأل نفسك: ما هو العقد الذي يحتاجه تطبيقي؟ عرّف هذا العقد كواجهة أولًا.
- حقن التبعية (Dependency Injection) هو صديقك الصدوق: سواء استخدمت مكتبة متخصصة أو قمت بتمرير الاعتماديات عبر الـ constructor يدويًا، فهذه هي الآلية التي تجعل فصل الطبقات ممكنًا.
- لا تخف من الطبقات الإضافية: قد يبدو الأمر في البداية وكأنه عمل إضافي وكتابة كود أكثر. وهذا صحيح. لكن صدقني، “الشغل النظيف بوخذ وقت بالأول، بس بريّحك لآخر العمر”. هذا الاستثمار الأولي سيوفر عليك شهورًا من الألم في المستقبل.
- المعمارية النظيفة ليست قانونًا صارمًا: هي مجموعة من المبادئ التوجيهية. يمكنك تكييفها لتناسب حجم مشروعك. لمشروع صغير جدًا قد تكون مبالغة، ولكن لأي تطبيق تنوي أن يعيش وينمو، فهي ضرورة لا غنى عنها.
الخلاصة: ابنِ بيتك على أساس متين 🧱
في النهاية، المعمارية النظيفة هي بمثابة بناء أساسات قوية لمنزلك. قد لا يراها الناس، وقد تستغرق وقتًا وجهدًا في البداية، ولكنها هي التي ستحمي منزلك من الزلازل والعواصف المستقبلية. تغيير قاعدة البيانات هو مجرد “زلزال” واحد من بين الكثير.
ماذا لو أردت تغيير خدمة إرسال الإيميلات؟ أو بوابة الدفع؟ أو حتى إطار عمل الويب نفسه؟ إذا بنيت تطبيقك على أساس متين من فصل الاهتمامات، ستكون هذه التغييرات سهلة ومباشرة، وستقضي وقتك في إضافة ميزات جديدة ومفيدة بدلًا من إعادة كتابة الكود القديم.
استثمر في معمارية برمجياتك، فهي استثمار في مستقبلك المهني وراحة بالك. بالتوفيق يا جماعة الخير! 🙏