طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 27 · الوحدة 13: التنظيم وسهولة الصيانة والتوسع

هيكلة التطبيق وأنماط التصميم

كود يفهمه زميلك، ويتغير دون أن ينكسر ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تفصل التطبيق إلى طبقات بمسؤوليات واضحة.
  • تطبّق أنماط الخدمة والمستودع وحقن الاعتماديات والمراقِب.
  • توحّد الأخطاء بصنف AppError ومعالج واحد.
  • تختبر طبقة الخدمة بمزيفات، وتنظّم مشروعًا كاملًا حسب الميزات.

١لماذا نهتم بالهيكلة؟

الكود يُقرأ أكثر مما يُكتب، ويعدّله أشخاص لم يكتبوه. التطبيق الجيد التنظيم: يسهل العثور فيه على الشيء، ويُغيَّر جزء منه دون أن ينكسر غيره، ويُختبر كل جزء وحده. المبدأ الحاكم: فصل المسؤوليات، فلكل ملف سبب واحد للتغيير.

الطبقةتعرفلا تعرفمثال تغيير يخصها وحدها
المسارات Routesالمسار ← المتحكمقواعد العملتغيير /orders إلى /v2/orders
المتحكم Controllerreq و res ورموز الحالةSQL وقواعد العملتغيير شكل JSON الراجع
الخدمة Serviceقواعد العمل: المخزون، والأسعار، والصلاحياتHTTP و Expressخصم 10% للطلاب
المستودع Repository / Modelكيف تُحفظ البيانات وتُقرأHTTP وقواعد العملالانتقال من SQLite إلى PostgreSQL

٢تطبيق بطبقات وأنماط

هذا خادم طلبات شراء صغير، لكنه مبني كما تُبنى التطبيقات الكبيرة. تنقّل بين الملفات الستة، ولاحظ أن قاعدة «الكمية غير متوفرة» تعيش في الخدمة وحدها:

// server.js: نقطة التشغيل فقطconst { createApp } = require('./app');const PORT = process.env.PORT || 3000;createApp().listen(PORT, () => console.log('يعمل على ' + PORT));
النمطأين في الكودفائدته
طبقة الخدمة Service Layerservices/orderService.jsقواعد العمل في مكان واحد، يستدعيها HTTP أو Socket.io أو مهمة مجدولة
المستودع Repositoryrepositories/memoryRepo.jsتبديل التخزين دون لمس الخدمة
حقن الاعتماديات DIcreateOrderService({ repo, events })الخدمة تستلم ما تحتاجه بدل أن تنشئه، فنمرر لها نسخًا مزيفة في الاختبار
المصنع FactorycreateApp() و create…()إنشاء نسخ مستقلة مهيأة
سلسلة المسؤوليةالوسطاء في Expressكل وسيط يعالج أو يمرر
المراقِب Observerevents.emit('order.placed')إضافة ردود فعل (بريد، إشعار) دون تعديل الخدمة

٣الاختبار: ثمرة الهيكلة

لأن الخدمة لا تعرف Express ولا قاعدة البيانات، نختبرها مباشرة بمستودع مزيّف في أجزاء من الثانية. هذا ملف اختبار يعمل بـ node test.js؛ شغّله وانظر النتائج في الطرفية، ثم غيّر قاعدة في الخدمة (مثل حساب الإجمالي) وأعد التشغيل لترى الاختبار يفشل:

// server.js: نقطة التشغيل فقطconst { createApp } = require('./app');const PORT = process.env.PORT || 3000;createApp().listen(PORT, () => console.log('يعمل على ' + PORT));
نوع الاختباريختبرأدوات شائعة
وحدة Unitدالة أو خدمة وحدها بمزيفاتnode:test، Vitest، Jest
تكامل Integrationالمسار كاملًا مع قاعدة بيانات اختبارSupertest مع createApp()
طرف لطرف E2Eالمستخدم يضغط في متصفح حقيقيPlaywright، Cypress
مكوّنات Reactما يظهر ويحدث عند التفاعلVitest + Testing Library

٤تنظيم المشروع الكامل

في الواجهة، التنظيم حسب الميزة Feature أوضح من التنظيم حسب نوع الملف حين يكبر المشروع: كل ما يخص «الطلبات» في مجلد واحد:

shop/
├── client/                      ← React + Vite
│   └── src/
│       ├── features/
│       │   ├── orders/          OrderList.jsx, OrderForm.jsx, useOrders.js, ordersApi.js
│       │   └── auth/            LoginForm.jsx, AuthContext.jsx, useAuth.js
│       ├── components/          Button.jsx, Modal.jsx   ← عامة قابلة لإعادة الاستخدام
│       ├── lib/api.js           ← fetch موحّد (الدرس 18)
│       └── App.jsx
├── server/                      ← Express
│   ├── routes/  controllers/  services/  repositories/ (أو models/)
│   ├── middleware/              auth.js, validate.js
│   ├── config/                  index.js ← يقرأ process.env في مكان واحد
│   ├── app.js  server.js  errors.js
│   └── tests/
├── .env.example
└── README.md
ممارسةلماذا
أسماء واضحة ومتسقة (useOrders و orderService)الاسم يغني عن التعليق
دوال قصيرة بمهمة واحدةأسهل في القراءة والاختبار
ESLint و Prettierأسلوب موحد للفريق، وكشف الأخطاء مبكرًا
Git: فروع ورسائل وصفية ومراجعة كودتاريخ مفهوم، وأخطاء أقل
README فيه خطوات التشغيلزميلك يشغّل المشروع في دقائق

توقّع وتحقّق

أين تُكتب قاعدة «لا يُباع أكثر من المخزون»؟

قواعد العمل.

ما فائدة حقن الاعتماديات؟

DI.

لماذا يُفصل app.js (createApp) عن server.js (listen)؟

الاختبار.

نمط Observer في الدرس ممثل بـ:

الأحداث.

الخلاصة

تحدٍّ برمجي: تفكيك المتحكم السمين

متقدم

كل منطق التسجيل في المقررات محشور في المسار. انقل قواعد العمل إلى خدمة، مع بقاء السلوك كما هو.

المطلوب:
  • أنشئ services/enrollService.js يصدّر enroll(studentId, courseId) تحوي القواعد: المقرر موجود (وإلا AppError 404)، ولم يمتلئ (وإلا 409)، والطالب غير مسجل مسبقًا (وإلا 409).
  • المسار في server.js يستدعي الخدمة فقط، ولا يحوي كلمة capacity.
  • الأخطاء تمر على errorHandler الجاهز في errors.js.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. POST /api/courses/1/enroll {"studentId": 10}
  2. POST /api/courses/1/enroll {"studentId": 10}
  3. POST /api/courses/1/enroll {"studentId": 11}
  4. POST /api/courses/1/enroll {"studentId": 12}
  5. POST /api/courses/5/enroll {"studentId": 10}
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • التسجيل الأول ينجح
  • التكرار ← «مسجل مسبقًا»
  • … بالحالة 409
  • الامتلاء ← «المقرر ممتلئ»
  • مقرر غير موجود ← 404
  • الخدمة تصدّر enroll وترمي AppError
  • المسار لا يحوي قواعد العمل (capacity)

💡 تلميح 1
في الخدمة: throw new AppError(409, 'المقرر ممتلئ') بدل res.status.
💡 تلميح 2
في Express 5 الخطأ المرمي داخل المعالج يصل إلى errorHandler تلقائيًّا.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const { errorHandler } = require('./errors');const { enroll } = require('./services/enrollService');const app = express();app.use(express.json()); app.post('/api/courses/:id/enroll', (req, res) => {  res.status(201).json(enroll(req.body.studentId, Number(req.params.id)));}); app.use(errorHandler);app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس