يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.
خلوني أحكيلكم قصة صارت معي قبل كم سنة، قصة علّمتني درس ما بنساه بحياتي كمبرمج. كنت شغال على نظام مالي لأحد العملاء، نظام كبير ومعقد، وبصراحة، لما استلمته كان الكود عبارة عن “معكرونة سباغيتي” زي ما بنحكي. كل إشي فايت ببعضه: كود الواجهة الأمامية (UI) بستدعي مباشرة دوال في قاعدة البيانات، ومنطق العمل (Business Logic) متوزع شوي في الكنترولر، وشوي في الموديل، وشوي جوا استعلامات SQL طويلة ومُعقدة.
في يوم من الأيام، طلب مني العميل طلب “بسيط” من وجهة نظره. قال لي: “يا أبو عمر، بدنا نضيف بوابة دفع جديدة بجانب البوابة الحالية”. قلت في نفسي: “بسيطة، شو القصة؟ يومين شغل بالكثير”.
وهون بلش الكابوس. لما فتت على الكود عشان أضيف بوابة الدفع الجديدة، اكتشفت إنه منطق الدفع الحالي مشبوك بشكل مباشر في عشرات الأماكن. تغيير بسيط في دالة الدفع كان يتطلب تغيير في واجهة عرض الفواتير، وفي صفحة إتمام الطلب، وفي تقرير المبيعات، وحتى في كود إرسال الإيميلات! كل تغيير كان يكسر إشي في مكان ثاني. قضيت أسابيع بدل يومين، وأنا أتنقل بين الملفات زي المجنون، أصلّح مشكلة تطلعلي عشرة غيرها. حسيت حالي بغرق في وحل من الكود المترابط بشكل مرضي. وقتها وقفت مع حالي وقفة جدية وقلت: “لا يا أبو عمر، أكيد في طريقة أحسن من هيك. الشغل بالطريقة هاي مش بس مُتعب، هو طريق مسدود للمشروع ولكل اللي شغالين عليه”.-p>
ومن هنا بدأت رحلتي مع البحث عن حل جذري، رحلة انتهت عند مفهوم غيّر طريقة تفكيري في كتابة الكود تماماً: المعمارية السداسية (Hexagonal Architecture).
ما هو الكابوس الذي كنت أعيشه؟ (تشابك المنطق)
قبل ما نحكي عن الحل، خلينا نفصّل المشكلة أكتر. أغلبنا، خصوصاً في بداية مسيرتنا المهنية، بنبني تطبيقاتنا على معمارية الطبقات التقليدية (N-Tier Architecture) بشكلها المبسط:
- طبقة العرض (Presentation Layer): الواجهات الرسومية (UI)، واجهات برمجة التطبيقات (APIs).
- طبقة منطق العمل (Business Logic Layer): القواعد والشروط اللي بتحكم التطبيق.
- طبقة الوصول للبيانات (Data Access Layer): الكود المسؤول عن التعامل مع قاعدة البيانات.
نظرياً، هالحكي ممتاز. لكن عملياً، اللي بصير هو إنه الحدود بين هاي الطبقات بتصير ضبابية. طبقة منطق العمل بتبدأ تعتمد بشكل مباشر على تفاصيل قاعدة البيانات (مثلاً، استخدام كائنات Entity Framework أو Eloquent مباشرة داخلها)، والكنترولرات في طبقة العرض بتبدأ تحتوي على منطق عمل معقد. النتيجة؟
التبعية (Coupling) القاتلة: يصبح قلب التطبيق (منطق العمل) معتمداً بشكل كامل على التفاصيل الخارجية مثل نوع قاعدة البيانات، أو الـ Framework المستخدم، أو طريقة عرض البيانات. تغيير أي من هذه التفاصيل الخارجية يتطلب تغييراً في قلب التطبيق نفسه، وهذا هو تعريف الكارثة في عالم البرمجيات.
المنقذ: المعمارية السداسية (Ports and Adapters)
المعمارية السداسية، اللي ابتكرها Alistair Cockburn، هي نمط معماري بقلب المعادلة رأساً على عقب. الفكرة الأساسية بسيطة وعبقرية: “اجعل قلب التطبيق (منطق العمل) غبياً تماماً عن العالم الخارجي”.
تخيل قلب التطبيق الخاص بك على أنه “نواة” أو “جوهرة” ثمينة. هذه النواة تحتوي على كل قواعد العمل النقية، ولا تعرف أي شيء عن وجود قاعدة بيانات اسمها MySQL، أو بروتوكول اسمه HTTP، أو واجهة مستخدم مبنية بـ React. هي فقط تعرف كيف تنفذ المهام المطلوبة منها.
طيب، كيف بتتواصل هاي النواة مع العالم الخارجي؟ عن طريق ما يسمى بـ “المنافذ والمحولات” (Ports and Adapters).
قلب التطبيق (The Hexagon / Application Core)
هذا هو مركز الكون في تطبيقك. يحتوي على:
- كيانات المجال (Domain Entities): الكائنات التي تمثل مفاهيم العمل الأساسية (مثل User, Order, Product).
- حالات الاستخدام (Use Cases): الخدمات التي تنسق وتنفذ منطق العمل (مثل CreateUser, PlaceOrder).
نصيحة أبو عمر: هذا الجزء من الكود هو أثمن ما تملك. يجب أن يكون “Plain Old Objects” (سواء كنت تستخدم Java, C#, PHP, or TypeScript). لا يجب أن يحتوي على أي اعتمادية على أي مكتبة أو إطار عمل خارجي. لو قدرت تاخد هذا الجزء من الكود وتشغله على command-line application بدون أي تغيير، فأنت في الطريق الصحيح.
المنافذ (Ports)
المنافذ هي ببساطة عبارة عن Interfaces (واجهات برمجية) تُعرّف العقود (Contracts) بين قلب التطبيق والعالم الخارجي. هي زي الفيش الكهربائي في الحيط، ما بهمها شو الجهاز اللي رح تشبكه فيها (محمصة، شاحن، تلفزيون)، المهم إنه الجهاز يلتزم بشكل الفيش. يوجد نوعان من المنافذ:
- المنافذ القائدة (Driving/Inbound Ports): تمثل الطرق التي يمكن للعالم الخارجي من خلالها أن “يأمر” التطبيق بفعل شيء. عادة ما تكون هذه واجهات برمجية (Interfaces) لحالات الاستخدام (Use Cases). على سبيل المثال:
interface CreateUserUseCase. - المنافذ المقودة (Driven/Outbound Ports): تمثل ما “يحتاجه” التطبيق من العالم الخارجي لتنفيذ عمله. على سبيل المثال، التطبيق يحتاج لحفظ مستخدم في مكان ما، فيُعرّف منفذ اسمه
interface UserRepositoryيحتوي على دالة مثلsave(user). قلب التطبيق لا يعرف ولا يهتم بكيفية تنفيذ عملية الحفظ، هو فقط يعرف أنه بحاجة لهذه الوظيفة.
المحولات (Adapters)
المحولات هي التنفيذ الفعلي للمنافذ. هي التي تترجم بين لغة العالم الخارجي ولغة قلب التطبيق.
- المحولات القائدة (Driving/Inbound Adapters): هذه هي الأجزاء التي تستدعي المنافذ القائدة. مثال:
UserController(في إطار عمل مثل Laravel أو Express.js) يستقبل طلب HTTP، يستخرج البيانات منه، ثم يستدعيCreateUserUseCase.CLICommandيستقبل أوامر من الطرفية ويستدعي نفس الـCreateUserUseCase.
- المحولات المقودة (Driven/Outbound Adapters): هذه هي الأجزاء التي تنفذ المنافذ المقودة. مثال:
PostgresUserRepositoryهو كلاس يقوم بتنفيذ واجهةUserRepositoryعن طريق كتابة استعلامات SQL خاصة بـ PostgreSQL.MongoUserRepositoryهو كلاس آخر يقوم بتنفيذ نفس الواجهةUserRepositoryلكن باستخدام أوامر خاصة بـ MongoDB.InMemoryUserRepositoryهو كلاس ثالث يقوم بتنفيذ نفس الواجهة لكن بحفظ البيانات في الذاكرة فقط (مفيد جداً للاختبارات).
مثال عملي بالكود: لننقذ نظام تسجيل المستخدمين
لنفترض أن لدينا حالة استخدام بسيطة: تسجيل مستخدم جديد. لنرى كيف ستبدو في المعمارية السداسية (سأستخدم صيغة تشبه TypeScript لسهولة الفهم).
1. قلب التطبيق (The Core / Hexagon)
هذا الكود نقي تماماً، لا يعرف شيئاً عن الويب أو قواعد البيانات.
// domain/user.ts (كيان المجال)
export class User {
constructor(public id: string, public name: string, public email: string) {}
}
// ports/in/create-user.use-case.ts (المنفذ القائد)
import { User } from '../domain/user';
export interface CreateUserUseCase {
execute(name: string, email: string): Promise<User>;
}
// ports/out/user.repository.ts (المنفذ المقود)
import { User } from '../domain/user';
export interface UserRepository {
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
}
// use-cases/create-user.service.ts (تنفيذ حالة الاستخدام)
import { User } from '../domain/user';
import { CreateUserUseCase } from '../ports/in/create-user.use-case';
import { UserRepository } from '../ports/out/user.repository';
import { v4 as uuidv4 } from 'uuid'; // مكتبة لتوليد ID، تعتبر مكتبة مساعدة لا إطار عمل
export class CreateUserService implements CreateUserUseCase {
// لاحظ أننا نعتمد على "الواجهة البرمجية" وليس على التنفيذ الفعلي
constructor(private readonly userRepository: UserRepository) {}
async execute(name: string, email: string): Promise<User> {
const existingUser = await this.userRepository.findByEmail(email);
if (existingUser) {
throw new Error('User with this email already exists.');
}
const newUser = new User(uuidv4(), name, email);
await this.userRepository.save(newUser);
return newUser;
}
}
انظر إلى جمال هذا الكود! CreateUserService لا يعرف أي شيء عن العالم الخارجي، فقط يعرف أنه بحاجة لشيء يطبق عقد UserRepository.
2. المحولات (The Adapters)
هنا نكتب الكود “القبيح” الذي يتعامل مع التفاصيل الخارجية.
// adapters/in/web/user.controller.ts (محول قائد للويب)
import { CreateUserUseCase } from '../../../core/ports/in/create-user.use-case';
// هذا الكود خاص بإطار عمل مثل Express.js
export class UserController {
constructor(private readonly createUserService: CreateUserUseCase) {}
async createUser(req, res) {
try {
const { name, email } = req.body;
const user = await this.createUserService.execute(name, email);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ message: error.message });
}
}
}
// adapters/out/db/mongo-user.repository.ts (محول مقود لقاعدة بيانات MongoDB)
import { User } from '../../../core/domain/user';
import { UserRepository } from '../../../core/ports/out/user.repository';
import { UserModel } from './mongo-schema'; // هذا نموذج خاص بـ Mongoose مثلاً
export class MongoUserRepository implements UserRepository {
async findByEmail(email: string): Promise<User | null> {
const mongoUser = await UserModel.findOne({ email }).exec();
if (!mongoUser) return null;
return new User(mongoUser.id, mongoUser.name, mongoUser.email);
}
async save(user: User): Promise<void> {
const userModel = new UserModel(user);
await userModel.save();
}
}
الآن، تخيل أننا نريد تغيير قاعدة البيانات من MongoDB إلى PostgreSQL. هل نحتاج لتغيير أي شيء في CreateUserService؟ بالتأكيد لا! كل ما علينا فعله هو كتابة محول جديد:
// adapters/out/db/postgres-user.repository.ts
export class PostgresUserRepository implements UserRepository {
// ... تنفيذ findByEmail و save باستخدام مكتبة PostgreSQL
}
وبعدها، في مكان ما عند بداية تشغيل التطبيق (Composition Root)، نقوم فقط بتغيير المحول الذي يتم حقنه في الخدمة. هذا هو معنى المرونة الحقيقية.
نصائح عملية من خبرة أبو عمر
- ابدأ صغيراً: لا تحاول إعادة كتابة مشروعك بالكامل بهذه المعمارية دفعة واحدة. اختر ميزة جديدة أو وحدة صغيرة (Module) وطبق عليها هذا المفهوم. مع الوقت، سيبدأ الجزء “النظيف” من تطبيقك بالنمو.
- النواة مقدسة “ممنوع اللمس”: ضع قاعدة صارمة لك ولفريقك: مجلدات الـ Core (domain, use-cases, ports) يمنع منعاً باتاً أن تحتوي على
importلأي شيء من طبقة الـ Adapters أو أي إطار عمل. - فكر بالعقود أولاً: قبل كتابة أي محول (Adapter)، اكتب المنفذ (Port) الخاص به. اسأل نفسك: “ما هي الوظيفة التي أحتاجها من العالم الخارجي؟” وليس “كيف سأقوم بحفظ هذا في قاعدة البيانات؟”.
- الاختبار أصبح سهلاً جداً: أحد أكبر فوائد هذه المعمارية هي سهولة الاختبار. يمكنك اختبار منطق العمل بالكامل (Use Cases) بمعزل عن كل شيء، فقط عن طريق تمرير محولات وهمية (Mock Adapters) تحفظ البيانات في الذاكرة. هذا يجعل الاختبارات سريعة وموثوقة للغاية.
الخلاصة: من الفوضى إلى النظام 🧘♂️
التحول إلى المعمارية السداسية لم يكن مجرد تغيير تقني بالنسبة لي، بل كان تغييراً في العقلية. لقد أنقذتني من كابوس الصيانة والتطوير في المشاريع المعقدة، وحولت عملية إضافة الميزات أو تغيير التقنيات من مهمة مرعبة إلى عملية منظمة وواضحة.
قد تبدو هذه المعمارية معقدة في البداية، وتتطلب كتابة كود إضافي (Interfaces, Adapters)، لكن الفائدة على المدى الطويل لا تقدر بثمن: تطبيق قابل للصيانة، قابل للتطوير، وقابل للاختبار بسهولة. تطبيق لا يخاف من المستقبل ولا من تغيير التقنيات.
نصيحتي الأخيرة لكل مبرمج ومبرمجة: استثمروا وقتاً في فهم هذه المفاهيم. قد لا تحتاجها في كل مشروع صغير، لكن في اليوم الذي تعمل فيه على نظام كبير ومهم، ستكون هذه المعمارية هي طوق النجاة الذي سيحميك من الغرق. هيك الحكي الصح.
الله يوفقكم يا جماعة، وأي سؤال أنا حاضر.