تصميم واجهات برمجة الخدمات وكائنات نقل البيانات
تصميم واجهات برمجة الخدمات وكائنات نقل البيانات
تعيش الخدمة المصغّرة أو تموت تبعًا لجودة عقدها العام. كل نقطة نهاية (endpoint) تعرضها هي وعدٌ لكل خدمة أخرى تعتمد عليك — وعد سيكلّف جهدًا حقيقيًا كسره. يعلّمك هذا الدرس كيفية تصميم ذلك العقد بصورة مدروسة: أي دلالات HTTP تستخدم، وكيف تشكّل أجسام الطلبات والاستجابات باستخدام كائنات نقل البيانات (DTOs)، وكيف تتحقق من صحة المدخلات مبكرًا، وكيف تُصدر إصدارات API حتى لا يتحوّل التطوير إلى كارثة.
لماذا لا تستطيع الاستغناء عن كائنات DTO
الإغراء هو كشف كيان JPA مباشرةً. فهو موجود بالفعل؛ تضيف إليه تعليقات Jackson وتعيده من المتحكّم (controller). لا تفعل ذلك. كيانك مرتبط بمخطط قاعدة البيانات، والعلاقات ذات التحميل الكسول (lazy-loaded)، وتعليقات الثبات (persistence). تسريبه للعالم الخارجي ينشئ اقترانًا في الاتجاهين: يتعطّل كود المُستدعي حين تعيد تسمية عمود قاعدة بيانات، ويصبح مخطط قاعدة بياناتك مقيّدًا بما يتوقعه المستدعون الخارجيون.
الـ DTO كائن Java بسيط (سجل record أو فئة class) مهمته الوحيدة نقل البيانات عبر الحدود. يحتوي تحديدًا على الحقول التي يحتاجها المستدعي — لا أكثر. ولا يحتوي على تعليقات JPA، ولا منطق أعمال، ولا مراجع دائرية تتسبّب في تسلسل JSON لا نهائي.
سجلات Java كـ DTOs
تُعدّ سجلات Java 16+ الأسلوبَ الأنظف لكتابة DTOs في مشاريع Spring Boot 3 الحديثة. فهي ثابتة (immutable)، وتولّد equals/hashCode/toString تلقائيًا، وتدعمها Jackson بشكل أصلي دون أي إعداد إضافي.
لاحظ أن CreateProductRequest لا يحتوي على حقل id. يعيّن الخادم المعرّف؛ يجب ألا يوفّره المستدعي. هذا قرار عقدي مقصود.
التحقق من صحة الطلبات باستخدام Bean Validation
لا تثق أبدًا بالمستدعي. كل بيانات تصل عبر الشبكة يجب التحقق منها قبل أن تلمس منطق النطاق أو قاعدة البيانات. يدمج Spring Boot 3 التحقق من Jakarta Bean Validation (المعروف سابقًا بـ javax) افتراضيًا عبر تبعية spring-boot-starter-validation.
أضف قيود التحقق إلى DTO الطلب. ونظرًا لدعم السجلات، استخدم التحقق عبر المنشئ المضغوط:
ثم أضف @Valid إلى معامل الطريقة في المتحكّم. سيرفض Spring الطلب بخطأ HTTP 400 تلقائيًا إذا فشلت أي قيد:
MethodArgumentNotValidException في @RestControllerAdvice لإعادة شكل خطأ متسق وآمن — لا يسرّب أسماء الحقول الداخلية للمستدعين الخارجيين.
التعيين بين الكيانات وكائنات DTO
يجب على أحدهم الترجمة بين كيان JPA وكائنات DTO الخاصة بك. لديك ثلاثة خيارات: طرق تعيين يدوية في فئة الخدمة، أو طريقة مصنع ثابتة على DTO نفسها، أو مكتبة تعيين مثل MapStruct. بالنسبة لمعظم الخدمات، تكون طريقة مساعدة خاصة ثابتة داخل الخدمة واضحة ولا تتطلب أي تبعيات إضافية:
EntityNotFoundException أو رسالة خطأ SQL تخبر المهاجم بمخططك. التقط استثناءات النطاق في @RestControllerAdvice وأعد فقط رسالة عامة آمنة مع رمز حالة HTTP المناسب.
دلالات HTTP مهمة
طريقة HTTP التي تختارها جزء من عقدك. إساءة استخدامها يخلق ارتباكًا ويكسر التخزين المؤقت والوكلاء وأدوات الأمان. اتبع هذه الاتفاقيات بدقة:
- GET — قراءة فقط. يجب أن تكون آمنة (بلا آثار جانبية) وذات قيمة ثابتة (idempotent). لا تستخدم GET لتشغيل تعديل.
- POST — إنشاء مورد جديد. ليست ذات قيمة ثابتة؛ قد تنشئ الطلبات المكررة موارد مكررة. أعد
201 Createdمع رأسLocationيشير إلى المورد الجديد. - PUT — استبدال كامل لمورد موجود. ذات قيمة ثابتة. يرسل المستدعي الحالة الجديدة الكاملة.
- PATCH — تحديث جزئي. استخدمه حين يجب على المستدعين تغيير حقول معينة فقط.
- DELETE — حذف مورد. ذات قيمة ثابتة. أعد
204 No Contentعند النجاح.
استراتيجية إصدار API
تتطوّر الخدمات المصغّرة باستقلالية، والتغييرات الكاسرة تحدث. أكثر استراتيجيات الإصدار عملية للتواصل بين الخدمات هي إصدار مسار URL — ضع الإصدار في المسار الأساسي (مثل /api/v1/products). فهو مرئي، وسهل التوجيه على مستوى البوابة، ولا يحتمل الغموض في السجلات.
القاعدة الأساسية: لا تكسر الإصدارات الموجودة. أضف نقطة نهاية جديدة على /api/v2/products حين تحتاج إلى تغيير العقد. شغّل الإصدارين جنبًا إلى جنب حتى تهاجر جميع المستدعين، ثم أوقف v1 بعد فترة إشعار إهمال.
الخلاصة
تصميم API الخدمة الجيد يعني إنشاء عقد مستقر وبسيط وصادق. استخدم سجلات DTO مخصصة لأشكال الطلب والاستجابة؛ لا تكشف كيانات JPA أبدًا. تحقق من صحة جميع البيانات الواردة عند حدود المتحكّم باستخدام @Valid. عيّن بين الكيانات وكائنات DTO في طبقة خدمة رفيعة. استخدم أفعال HTTP الصحيحة ورموز الحالة. ضع إصدارًا لـ API منذ اليوم الأول بادئة /api/v1 حتى لا يتحوّل التطوير إلى طوارئ. في الدرس القادم ستشاهد كيف تستدعي خدمة خدمة أخرى باستخدام WebClient.