مصمّم الواجهات: التصميم قبل الكود
درس 8 من 12
8مصمّم الواجهات: التصميم قبل الكود
مشروع بلا سطر كود واحد: تحوّل فكرة بسيطة إلى عقد مكتوب يفهمه أي مطوّر، متطلبات بلغة عادية، ثم موارد وأدوار، ثم جدول مسارات، ثم ملف OpenAPI صالح تتحقّق منه بنفسك، والذكاء الاصطناعي شريك محاورة لا كاتب إملاء.
ماذا تشاهد في الرسم: الفكرة تصير متطلبات، والمتطلبات موارد ومسارات، والمسارات ملف OpenAPI يمرّ على المدقّق قبل أي سطر كود.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
قبل أن يبني المقاول البيت، يتفق مع صاحبه على مخطط مكتوب: كم غرفة، وأين الأبواب. الواجهة البرمجية تحتاج نفس الشيء: عقدًا مكتوبًا يقول ما الطلبات المسموحة وما الردود. في هذا المشروع تكتب هذا العقد قبل أي سطر كود، بمساعدة الذكاء الاصطناعي.
تصميم الواجهة أولًا design-first: متطلبات بلغة عادية تتحول إلى موارد ونقاط نهاية ومخططات بيانات ورموز خطأ في ملف OpenAPI بصيغة YAML. تتحقق من صحته بأداة فحص، فيصير مرجعًا للواجهة الأمامية والخلفية والاختبارات معًا.
المتطلبات قبل البداية
المحادثة الموجّهة (الجزء 3)
هذا المشروع كله محادثة. من يعرف كيف يعطي سياقًا ويطلب شكل مخرج محدّدًا سيخرج بجدول جاهز، ومن يسأل سؤالًا عامًا سيخرج بكلام عام.
التحقّق قبل القبول (الجزء 7)
المساعد يخترع حقولًا ومسارات تبدو معقولة. المدقّق هو من يحكم، لا الثقة.
أساسيات HTTP
الأفعال GET وPOST وPUT وDELETE ورموز الحالة. لا تحتاج أكثر من ذلك هنا.
قراءة YAML
مسافات بادئة تحدّد التداخل، لا أقواس. لا تحتاج حفظ الصياغة، لكن تحتاج أن تميّز الخطأ حين يشير إليه المدقّق.
المفاهيم الأساسية
هذا المشروع يختلف عن كل ما سبقه في المسار: لا كود فيه إطلاقًا. مخرجاته ملفان فقط: جدول مسارات، وملف openapi.yaml. والمهارة المقصودة ليست كتابة YAML، بل أن تتفق مع الآخرين على ماذا ستفعل الواجهة قبل أن يكتب أحد سطرًا.
1لماذا نصمّم قبل أن نكتب
الواجهة البرمجية (API) أشبه بقائمة الطعام في مطعم: قائمة معلنة بما يستطيع برنامج آخر أن يطلبه من برنامجك، وبأي شكل يصل الطلب ويعود الرد. وكتابة هذه القائمة قبل البناء توفّر جدالًا طويلًا لاحقًا، لثلاثة أسباب ملموسة:
عقد بين فريقين
فريق الواجهة الأمامية يبدأ العمل على المواصفة بينما الخلفية ما زالت تُبنى، بدل أن ينتظر أسابيع.
تغيير على ورق لا على كود
تعديل سطر في المواصفة يستغرق دقيقة؛ وتعديل مسار بعد أن بنت عليه ثلاثة تطبيقات يستغرق أسبوعًا.
ملف تقرؤه الآلات
من نفس الملف تُولَّد صفحة توثيق تفاعلية، وعملاء جاهزون، واختبارات تتحقّق من الردود.
2المتطلبات بلغة عادية
قبل أي مسار أو حقل، أربعة أسئلة تُجاب بجمل عادية:
| السؤال | مثال من مكتبة الحرم |
|---|---|
| الهدف التجاري | تمكين الطلاب من حجز الكتب قبل الحضور إلى المكتبة. |
| من سيستدعيها | تطبيق جوال للطلاب، ولوحة تحكّم لأمين المكتبة. |
| حالات الاستخدام | «كمستخدم أريد أن أرى الكتب المتاحة»، «أن أحجز كتابًا»، «أن ألغي حجزي». |
| ما ليس منها | لا دفع، ولا غرامات تأخير، ولا توصيل. تحديد ما لا تفعله يمنع تضخّم التصميم. |
3مفهوم الواجهة: الموارد والأدوار والمصادقة
المورد هو «نوع الشيء» الذي تتيح الواجهة قراءته أو تغييره، وهو دائمًا اسم. في مثالنا: كتاب، حجز، عضو. والقاعدة العملية: من ثلاثة إلى ستة موارد تكفي تمامًا.
| القرار | الخيارات | متى تختار كلًا منها |
|---|---|---|
| المصادقة | بلا مصادقة | واجهة قراءة عامة فقط، أو تجربة تعلّم |
مفتاح API key | نص سرّي يعرّف المستدعي؛ بسيط ويكفي لخدمة داخلية | |
| تسجيل دخول ثم رمز | حين تحتاج معرفة أي مستخدم لا أي تطبيق | |
| الأدوار | زائر / عضو / أمين | حين تختلف الصلاحيات، وتظهر في عمود «يحتاج صلاحية؟» |
4من الموارد إلى المسارات
اسم المورد يصير مسارًا، والفعل يسكن في المنهج لا في المسار. والعمليات الأربع (CRUD) تتوزّع هكذا:
| العملية | المنهج | المسار | ملاحظة |
|---|---|---|---|
| إنشاء | POST | /books | يُرجع 201 مع ترويسة Location |
| قراءة الكل | GET | /books | الفلاتر في سلسلة الاستعلام: ?available=true |
| قراءة واحد | GET | /books/{id} | 404 إن لم يوجد |
| استبدال كامل | PUT | /books/{id} | يرسل الكائن كاملًا |
| تعديل جزئي | PATCH | /books/{id} | يرسل الحقول المتغيّرة فقط |
| حذف | DELETE | /books/{id} | 204 بلا جسم |
واجعلها ثمانية مسارات على الأقل تغطّي العمليات الأربع على موردين على الأقل. فخطّط لذلك من البداية بدل أن تكتشفه في آخر خطوة.
/getBooks و/createBook و/books/delete/3 كلها خاطئة: الفعل مكرّر مرتين. الصحيح GET /books وPOST /books وDELETE /books/3. وكذلك الجمع للمجموعات: /books لا /book.5بنية ملف OpenAPI
الملف أقسام قليلة، وما هو مطلوب منها أقل مما تتوقّع:
| القسم | ماذا يصف | مطلوب؟ |
|---|---|---|
openapi | رقم نسخة المعيار، مثل 3.0.3 | نعم |
info | اسم واجهتك ونسختها، ولاحظ أنها نسخة واجهتك لا نسخة المعيار | نعم (مع title وversion) |
servers | عناوين الخوادم | لا |
paths | كل مسار بمناهجه ومعاملاته وأجسامه واستجاباته | عمليًا نعم |
components/schemas | أشكال البيانات القابلة لإعادة الاستخدام | لا، لكنها تمنع التكرار |
components/securitySchemes | طرق المصادقة وأي عملية تحتاجها | لا |
6المخططات و$ref: اكتب الشكل مرة واحدة
المخطّط (schema) هو شكل قطعة البيانات: ما حقولها وما أنواعها. وبدل تكرار تعريف «كتاب» في ستة أماكن، تعرّفه مرة في components/schemas وتشير إليه بـ$ref: '#/components/schemas/Book'. تغيير واحد يسري على الملف كله.
وحيلة مفيدة: افصل ما يرسله العميل عمّا يُرجعه الخادم. العميل لا يرسل id لأن الخادم هو من يولّده:
NewBook: # ما يرسله العميل
type: object
required: [title, author]
properties:
title: { type: string }
author: { type: string }
Book: # ما يُرجعه الخادم
allOf:
- $ref: '#/components/schemas/NewBook'
- type: object
properties:
id: { type: integer }
available: { type: boolean }وأربعة مخططات على الأقل، ومنها مخطّط Error للأخطاء، وهو أكثر ما يُنسى.
7الاستجابات الوهمية: جرّب التصميم قبل وجود كود
الاستجابة الوهمية (mock) مثال مكتوب يدويًا لما سترجعه الواجهة. فائدتها أنك ترى شكل الرد بعينيك وتسأل: «هل هذا ما يتوقّعه المستخدم فعلًا؟» قبل أن يُبنى أي شيء. تُضاف بحقل example تحت الاستجابة:
example:
- id: 1
title: "The Pragmatic Programmer"
author: "Hunt & Thomas"
available: true8كيف تحاور المساعد في هذا المشروع تحديدًا
الفرق بين من يخرج بتصميم جيد ومن يخرج بنصّ مرتّب هو أسلوب الطلب:
| بدل أن تقول | قل |
|---|---|
| «صمّم لي واجهة» | «هذا مجالي، وهؤلاء مستخدموه، وهذه حالات استخدامه. اقترح من 3 إلى 6 موارد» |
| «اكتب لي المسارات» | «أعطني جدول Markdown بأعمدة: المنهج، المسار، الغرض، يحتاج صلاحية؟» |
| «هل هذا صحيح؟» | «أين يفشل هذا التصميم إذا تضاعف عدد المستخدمين؟ وما البديل؟» |
| «أصلح الخطأ» | «هذا نص الخطأ من المدقّق حرفيًا: … ما السطر المسبّب ولماذا؟» |
كيف يعمل؟
ثلاث صور: رحلة المشروع من الفكرة إلى الملف، ثم كيف يتحوّل مورد واحد إلى خمسة مسارات، ثم حلقة الحوار التي تفصل التصميم الجيد عن النص المرتّب.
شكل 7، رحلة المشروع كاملة: من فكرة بجملة إلى عقد مُتحقَّق منه. لاحظ الحلقة الوحيدة في الرسم: بين المدقّق والإصلاح، وهي التي تضمن أن الناتج صالح لا مجرد جميل.
شكل 8، مورد واحد وخمس عمليات. المسار يبقى اسمًا في كل الأسطر، والذي يتغيّر هو المنهج وحده.
شكل 9، حلقة الحوار: السياق أولًا، ثم الفهم، ثم التحقّق بأداة. القفز من الجواب إلى الاعتماد مباشرة هو ما ينتج تصميمًا لا تستطيع الدفاع عنه.
الحل خطوة بخطوة
سنصمّم واجهة مكتبة الحرم كاملة، ثم نمرّرها على مدقّق OpenAPI حقيقي. كل ما تحته نُفِّذ فعلًا، بما فيه رسائل الخطأ.
الخطوة 1، المسار من الفكرة إلى الملف
ست خطوات، آخرها هي التي تحسم
المتطلبات. «واجهة تتيح لطلاب الحرم حجز الكتب قبل الحضور.» مستخدموها: تطبيق جوال للطلاب ولوحة لأمين المكتبة. وما ليس منها: لا دفع ولا غرامات.
المفهوم. ثلاثة موارد: Book وReservation وMember. ودوران: عضو وأمين. والمصادقة: رمز Bearer، القراءة مفتوحة، وكل تغيير يحتاج رمزًا.
جدول المسارات. ثمانية مسارات تغطّي العمليات الأربع على موردين، وهو الحد الأدنى الذي يطلبه المشروع بالضبط.
الملف. نحوّل الجدول إلى openapi.yaml بمخططات مربوطة بـ$ref ومثال واحد على الأقل.
التحقّق. نمرّره على مدقّق حقيقي. الملف إما صالح أو لا، ولا رأي لأحد في ذلك، لا لك ولا للمساعد.
الكسر المتعمّد. نُدخل خمسة أخطاء شائعة واحدًا واحدًا ونقرأ ماذا يقول المدقّق، حتى تتعرّف على الرسالة حين تقابلك فعلًا.
الخطوة 2، جدول المسارات (المخرَج الأول للمشروع)
| Method | Path | Purpose | Auth |
|--------|---------------------------------|-----------------------|------|
| GET | /books | List books | no |
| POST | /books | Add a book | yes |
| GET | /books/{bookId} | Get one book | no |
| PUT | /books/{bookId} | Replace a book | yes |
| DELETE | /books/{bookId} | Delete a book | yes |
| GET | /reservations | List reservations | yes |
| POST | /reservations | Reserve a book | yes |
| DELETE | /reservations/{reservationId} | Cancel a reservation | yes |اقرأ العمود الأخير: القراءة مفتوحة والكتابة مقيّدة. هذا القرار وحده هو «نموذج الصلاحيات» الذي سيسألك عنه أي مراجع.
الخطوة 3، ملف OpenAPI (المخرَج الثاني)
مختصر هنا، والمسارات المكرّرة مطويّة بـ... لتوفير المساحة:
openapi: 3.0.3
info:
title: Campus Library API
version: "1.0.0"
description: Reserve books in a small campus library.
servers:
- url: https://api.campus-library.example/v1
paths:
/books:
get:
summary: List books
parameters:
- name: available
in: query
required: false
schema:
type: boolean
responses:
"200":
description: A list of books
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Book'
example:
- id: 1
title: "The Pragmatic Programmer"
author: "Hunt & Thomas"
available: true
post:
summary: Add a book
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewBook'
responses:
"201":
description: Created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
"400":
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
"401":
description: Missing or invalid token
/books/{bookId}:
parameters:
- name: bookId
in: path
required: true
schema:
type: integer
get:
summary: Get one book
responses:
"200":
description: The book
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
"404":
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put: { summary: Replace a book, ... }
delete: { summary: Delete a book, ... }
/reservations:
get: { summary: List reservations, ... }
post: { summary: Reserve a book, ... }
/reservations/{reservationId}:
delete: { summary: Cancel a reservation, ... }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
NewBook:
type: object
required: [title, author]
properties:
title: { type: string }
author: { type: string }
Book:
allOf:
- $ref: '#/components/schemas/NewBook'
- type: object
properties:
id: { type: integer }
available: { type: boolean }
Error:
type: object
required: [error]
properties:
error: { type: string }الخطوة 4، التحقّق بمدقّق حقيقي
$ python3 check.py
VALID OpenAPI 3.0.3 spec ✓
endpoints: 8
GET /books
POST /books
GET /books/{bookId}
PUT /books/{bookId}
DELETE /books/{bookId}
GET /reservations
POST /reservations
DELETE /reservations/{reservationId}
schemas: ['NewBook', 'Book', 'Member', 'NewReservation', 'Reservation', 'Error']
security schemes: ['bearerAuth']ثمانية مسارات وستة مخططات: تجاوزنا الحد الأدنى في الاثنين. ولاحظ أن المدقّق لم يقل «تصميم جميل»، قال «صالح». الصلاحية شرط ضروري لا كافٍ، والجودة تبقى حكمك أنت.
الخطوة 5، خمسة أخطاء وما يقوله المدقّق فعلًا
1 info.version removed
'version' is a required property
2 $ref pointing at a schema that does not exist ($ref: '#/components/schemas/Buk')
'/components/schemas/Buk' does not exist within {'openapi': '3.0.3', ...}
3 an operation with no responses
'responses' is a required property
4 a bad parameter location (in: pathh)
{'name': 'bookId', 'in': 'pathh', ...} is not valid under any of the given schemas
5 required written as a string instead of a list (required: title)
{'type': 'object', 'required': 'title', ...} is not valid under any of the given schemasBuk وحدها تخبرك أن المشكلة في اسم مخطّط لا في بنية الملف.اعتقاد شائع × الحقيقة
| الاعتقاد | الحقيقة |
|---|---|
| «المشروع بلا كود، فهو أسهل.» | أصعب في الحقيقة: لا مترجم يخبرك أنك أخطأت في التصميم. المدقّق يفحص الصياغة فقط، أما «هل هذا المسار منطقي؟» فلا يجيب عنه إلا أنت. |
| «المساعد يكتب لي المواصفة وأسلّمها.» | ناقش وعدّل «بصوت عالٍ في المحادثة». مواصفة لا تستطيع الدفاع عن كل سطر فيها لن تنجو من أول سؤال. |
| «ما دام المدقّق قبله فالتصميم جيد.» | الصلاحية والجودة شيئان. POST /createBook يمرّ من المدقّق وهو خطأ تصميمي واضح. |
«النسخة في info.version هي نسخة OpenAPI.» | لا. info.version نسخة واجهتك، ونسخة المعيار في الحقل openapi في أعلى الملف. |
«$ref للتوفير فقط.» | ليس اختصارًا بل مصدر حقيقة واحد: تغيّر الشكل مرة فيسري على كل مكان، فتختفي حالة «حقل موجود هنا وناقص هناك». |
| «الاستجابات الوهمية خطوة تجميلية.» | هي أرخص اختبار للتصميم: ترى شكل الرد قبل بنائه، فتكتشف الحقل الناقص وأنت على ورق لا بعد ثلاثة أسابيع. |
جرّب بنفسك
اقترح عليك المساعد هذه المسارات الأربعة. أيّها الوحيد الصحيح حسب قواعد REST؟
القاعدة: المسار اسم والفعل في المنهج. الخيار الأول والثاني يكرّران الفعل داخل المسار. والرابع أسوأ: يستعمل GET لعملية تحذف بيانات، وGET يجب أن يكون آمنًا لا يغيّر شيئًا، وإلا فقد يحذف متصفّح أو وسيط بياناتك وهو «يخزّن الصفحة مؤقتًا». الخيار الثالث يجمع الاسم في المسار والفعل في المنهج.
بطاقات مراجعة سريعة
openapi.yaml. لا كود ولا ملاحظات منفصلة./books لا /book ولا /getBooks.info؟title وversion، والأخيرة نسخة واجهتك لا نسخة المعيار.$ref؟NewBook عن Book؟id والخادم هو من يولّده؛ فالمرسَل والمستقبَل شكلان مختلفان.تحقّق من فهمك
1. اقترح المساعد موردًا واحدًا اسمه Data يحمل كل شيء. ماذا تقول له؟
إن المورد يجب أن يمثّل «نوع شيء» واحدًا مفهومًا للمستخدم: كتاب، حجز، عضو. مورد باسم Data يعني أن كل مسار سيحتاج معاملات تشرح ماذا يقصد، فتضيع فائدة العناوين. اطلب منه تفكيكه إلى ثلاثة إلى ستة موارد بأسماء من لغة المجال نفسه.
2. ما الفرق العملي بين PUT وPATCH، ومتى يهمّك؟
PUT يستبدل الكائن كاملًا: ما لم ترسله يُحذف أو يعود لقيمته الافتراضية. وPATCH يعدّل الحقول المرسلة فقط. يهمّك حين يعدّل مستخدمان حقلين مختلفين في وقت متقارب: مع PUT يمحو الثاني تغيير الأول بلا قصد، ومع PATCH لا يحدث ذلك.
3. (اكتشف الخطأ) ملفك صالح، لكن كل الاستجابات تُرجع 200 وفيه حقل success. ما المشكلة؟
المشكلة أن رمز الحالة يكذب. الرمز هو ما تقرؤه الأدوات والوسائط أولًا، فإخفاء الفشل في الجسم يعطّل إعادة المحاولة والتخزين المؤقت والمراقبة. استعمل 201 للإنشاء و204 للحذف و400 لمدخل خاطئ و404 لمورد غير موجود، ودع الجسم يشرح لماذا لا ماذا.
4. لماذا يطلب المشروع مخطّط Error تحديدًا؟
لأن شكل الخطأ جزء من العقد تمامًا كشكل النجاح. بدونه يخمّن كل مطوّر شكل رسالة الخطأ فيختلف التعامل معها بين تطبيق وآخر. ومخطّط واحد مشترك يجعل كل الأخطاء تُقرأ بنفس الطريقة.
5. (قرار) المساعد أعطاك مواصفة كاملة من أول رسالة. تعتمدها؟
لا تعتمدها كما هي، ولا ترفضها لأنها من مساعد. مرّرها على المدقّق أولًا، ثم اقرأ كل مسار واسأل: هل يخدم حالة استخدام ذكرتها؟ وهل عمود الصلاحيات منطقي؟ واحذف ما لا تحتاجه، المواصفة الطموحة التي لا أحد يبنيها أسوأ من مواصفة صغيرة مكتملة. قيمتك في هذا المشروع هي القرارات التي تدافع عنها، لا عدد الأسطر.