اذهب إلى المحتوى

السؤال

نشر

كيف تصمم واجهة API لتطبيقات الهاتف المحمول قابلة للتوسع؟

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

في هذه المرحلة تصبح واجهة برمجة التطبيقات API جزءًا أساسيًا من تصميم النظام، وليس مجرد وسيلة لإرسال البيانات واستقبالها.

يمكن أن تعمل الواجهة البرمجية بشكل صحيح في مشروع صغير، ومع ذلك تسبب مشكلات كبيرة عند التوسع إذا لم تُصمم منذ البداية بطريقة تسمح بتطور التطبيق دون كسر الإصدارات الموجودة منه.

في هذا المقال سنبني تصورًا عمليًا لتصميم API لتطبيقات الهاتف المحمول، مع التركيز على تنظيم نقاط النهاية، إدارة الإصدارات، التعامل مع الأخطاء، المصادقة، الصفحات، التخزين المؤقت، ومراقبة الأداء.

ما الذي يجعل API مناسبة لتطبيقات الهاتف المحمول؟

تطبيق الهاتف يختلف عن العميل الموجود على الخادم نفسه.

الهاتف يعمل على شبكة قد تكون بطيئة أو غير مستقرة، وقد يستخدم المستخدم إصدارًا قديمًا من التطبيق لعدة أشهر بعد صدور إصدار أحدث. لذلك لا يكفي أن تكون الاستجابة صحيحة من الناحية البرمجية، بل يجب أن تكون الواجهة البرمجية مستقرة وقابلة للتطور.

لنفترض أن لدينا تطبيقًا يعرض قائمة المنتجات.

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

GET /api/products

وتعيد:

{ "products": [ { "id": 101, "name": "Product A", "price": 250 }, { "id": 102, "name": "Product B", "price": 320 } ] }

هذا التصميم يبدو بسيطًا، لكنه يترك عدة أسئلة مهمة:

ماذا يحدث إذا أصبحت المنتجات بالآلاف؟

كيف يحصل التطبيق على الصفحة الثانية؟

ماذا يحدث إذا تغير شكل المنتج؟

كيف يعرف التطبيق أن الطلب فشل؟

كيف يتعامل التطبيق مع إصدار قديم من API؟

ماذا يحدث إذا احتاجت بعض البيانات إلى مصدر آخر؟

هذه الأسئلة هي التي تحدد جودة تصميم API.

ابدأ بتصميم الموارد وليس الشاشات

من الأخطاء الشائعة تصميم API انطلاقًا من شاشات التطبيق.

فمثلًا قد يكون لدينا شاشة باسم:

ProductDetailsScreen

ثم ننشئ نقطة نهاية:

GET /getProductDetails

لكن API الأفضل أن تعكس الموارد والعمليات التي يجريها النظام، وليس أسماء مكونات واجهة المستخدم.

يمكن مثلًا استخدام:

GET /products/101

للحصول على منتج محدد.

وعند إنشاء منتج:

POST /products

وعند تحديثه:

PUT /products/101

أو:

PATCH /products/101

بحسب طبيعة التحديث.

وعند حذفه:

DELETE /products/101

بهذا تصبح الواجهة البرمجية أكثر وضوحًا، كما يصبح من الأسهل إعادة استخدام الموارد في أكثر من شاشة أو عميل.

استخدم HTTP Methods بطريقة واضحة

لا تحتاج كل العمليات إلى استخدام POST.

من الأفضل أن يكون معنى الطلب واضحًا من خلال HTTP Method والمسار معًا.

مثلًا:

GET /products

لجلب المنتجات.

POST /products

لإنشاء منتج.

PATCH /products/101

لتعديل جزء من بيانات المنتج.

DELETE /products/101

لحذف المنتج.

هذا التنظيم يجعل API أسهل للفهم والاختبار، ويقلل من الحاجة إلى إنشاء عشرات المسارات التي تختلف فقط في أسمائها.

لا ترسل كل البيانات في كل طلب

مع نمو النظام، قد يحتوي المورد الواحد على عشرات الحقول.

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

قد يحتوي المنتج في قاعدة البيانات مثلًا على:

id name description price cost supplier inventory created_at updated_at internal_notes

لكن التطبيق العام قد يحتاج إلى:

{ "id": 101, "name": "Product A", "price": 250 }

تقليل البيانات المنقولة له فائدتان واضحتان:

تقليل استهلاك الشبكة.

تقليل الوقت اللازم لمعالجة الاستجابة.

وهذا مهم بصورة خاصة في تطبيقات الهاتف لأن سرعة الشبكة ليست مضمونة دائمًا.

استخدم Pagination عند التعامل مع القوائم

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

بدلًا من:

GET /products

الذي يعيد آلاف العناصر، يمكن استخدام Pagination:

GET /products?page=2&limit=20

وتكون الاستجابة مثلًا:

{ "data": [ { "id": 101, "name": "Product A" } ], "pagination": { "page": 2, "limit": 20, "total": 250 } }

لكن هناك نقطة مهمة: لا ينبغي افتراض أن page وlimit هما الحل الوحيد لكل الحالات.

في البيانات التي تتغير باستمرار، يمكن أن تكون Cursor-based Pagination أكثر ملاءمة.

مثلًا:

GET /products?limit=20&cursor=eyJpZCI6MTAwfQ

ويعيد الخادم مؤشرًا للصفحة التالية:

{ "data": [], "next_cursor": "eyJpZCI6MTIwfQ" }

هذا الأسلوب يمكن أن يكون أكثر استقرارًا عند التعامل مع مجموعات بيانات كبيرة ومتغيرة باستمرار.

صمم نظام أخطاء يمكن للتطبيق فهمه

من أكثر الأجزاء أهمية في API طريقة التعامل مع الأخطاء.

إرجاع:

{ "error": "Something went wrong" }

لا يساعد التطبيق كثيرًا.

الأفضل أن تكون الاستجابة منظمة بحيث يستطيع التطبيق اتخاذ القرار المناسب.

مثلًا:

{ "error": { "code": "PRODUCT_NOT_FOUND", "message": "The requested product does not exist." } }

مع استخدام HTTP Status Code مناسب:

404 Not Found

أما إذا كانت البيانات المرسلة غير صحيحة:

400 Bad Request

وإذا لم يكن المستخدم مصرحًا له بالوصول:

401 Unauthorized

وإذا كان المستخدم معروفًا لكن ليس لديه الصلاحية المطلوبة:

403 Forbidden

أما الخطأ الداخلي غير المتوقع في الخادم فيمكن أن يستخدم:

500 Internal Server Error

الفكرة الأساسية هنا أن التطبيق لا ينبغي أن يعتمد على قراءة رسالة الخطأ لمعرفة نوع المشكلة، بل يجب أن يعتمد على رمز منظم يمكن التعامل معه برمجيًا.

افصل بين رسالة المستخدم وتفاصيل الخطأ الداخلية

ليس من الجيد إرسال تفاصيل قاعدة البيانات أو Stack Trace إلى تطبيق الهاتف.

مثلًا، لا ينبغي أن يحصل المستخدم على:

SQLSTATE[23000]: Integrity constraint violation...

يمكن بدلًا من ذلك إرسال:

{ "error": { "code": "DUPLICATE_EMAIL", "message": "The email address is already registered." } }

بينما يحتفظ الخادم بالتفاصيل التقنية الكاملة في سجلات النظام.

بهذا يحصل المستخدم على رسالة مفهومة، ويحصل المطور على معلومات تشخيصية مناسبة دون كشف تفاصيل داخلية للنظام.

صمم المصادقة كجزء من المعمارية

تطبيقات الهاتف التي تتعامل مع حسابات المستخدمين تحتاج إلى آلية واضحة للمصادقة.

بدلًا من إرسال اسم المستخدم وكلمة المرور في كل طلب، يمكن استخدام نظام يعتمد على رموز وصول Access Tokens بعد إتمام تسجيل الدخول.

مثلًا:

Authorization: Bearer <access-token>

لكن وجود Token لا يعني أن API أصبحت آمنة تلقائيًا.

يجب التفكير في:

مدة صلاحية الرمز.

آلية تحديثه.

إبطال الجلسات.

صلاحيات المستخدم.

حماية نقاط النهاية.

التعامل مع الأجهزة الموثوقة.

عدم تسجيل الرموز السرية في Logs.

كما يجب أن يتم الاتصال عبر HTTPS حتى لا تنتقل بيانات المصادقة عبر اتصال غير مشفر.

لا تربط API مباشرة بقاعدة البيانات

من الأفضل ألا تكون نقطة النهاية مسؤولة عن تنفيذ استعلامات قاعدة البيانات ومنطق الأعمال والاستجابة HTTP في المكان نفسه.

تصور مبسط مثل:

Mobile App ↓ API Controller ↓ Service Layer ↓ Repository / Data Access ↓ Database

يساعد هذا الفصل على جعل كل طبقة مسؤولة عن جزء محدد.

فعلى سبيل المثال:

Controller

يتعامل مع HTTP Request وResponse.

Service

يحتوي على منطق الأعمال.

Repository

يتعامل مع الوصول إلى البيانات.

وبهذا يصبح من الأسهل اختبار منطق الأعمال أو تغييره دون إعادة كتابة كل نقطة نهاية.

انتبه إلى Versioning

واحدة من أكبر المشكلات التي تظهر مع نمو التطبيقات هي تغيير API بطريقة تكسر الإصدارات القديمة من التطبيق.

لنفترض أن الإصدار الأول يعيد:

{ "name": "Ahmed", "phone": "01000000000" }

ثم قرر الخادم تغيير الحقل:

{ "name": "Ahmed", "phone_number": "01000000000" }

إذا كان التطبيق القديم ينتظر phone، فقد تتوقف إحدى وظائفه.

المشكلة أن المستخدم ليس بالضرورة يقوم بتحديث التطبيق فور صدور نسخة جديدة.

لذلك يجب التفكير في التوافق مع الإصدارات السابقة منذ البداية.

يمكن استخدام إصدار واضح للواجهة:

/api/v1/products

ثم عند وجود تغيير غير متوافق:

/api/v2/products

لكن Versioning ليس مجرد إضافة رقم إلى الرابط؛ المهم هو وضع سياسة واضحة لمعرفة متى يصبح التغيير Breaking Change، وكم من الوقت ستظل النسخة القديمة مدعومة.

اجعل الاستجابات قابلة للتطور

من الأفضل تجنب تصميم Response شديد التعقيد منذ البداية.

مثلًا:

{ "result": { "items": [], "meta": {} } }

قد يكون مناسبًا إذا كان هناك سبب واضح لاستخدام هذا الهيكل.

لكن إضافة طبقات غير ضرورية تجعل التعامل مع API أكثر تعقيدًا بالنسبة إلى تطبيق الهاتف.

الأهم هو وجود بنية متسقة.

إذا كانت نقطة نهاية تعيد:

{ "data": [] }

فمن الأفضل ألا تستخدم نقطة أخرى:

{ "results": [] }

دون سبب.

الاتساق يقلل كمية المنطق المطلوب داخل التطبيق.

استخدم Caching عندما يكون مناسبًا

ليست كل البيانات بحاجة إلى طلبها من الخادم في كل مرة.

بعض البيانات تتغير نادرًا ويمكن تخزينها مؤقتًا لفترة محددة.

يمكن أن يكون ذلك على مستوى الخادم أو الشبكة أو التطبيق، بحسب طبيعة البيانات.

لكن Caching يحتاج إلى سياسة واضحة.

السؤال ليس:

كيف أخزن البيانات مؤقتًا؟

بل:

متى تصبح البيانات المخزنة قديمة، وكيف أعرف أنني بحاجة إلى نسخة جديدة؟

بالنسبة إلى بيانات تتغير باستمرار، قد يكون التخزين المؤقت الطويل سببًا في عرض معلومات قديمة.

أما البيانات التي تتغير نادرًا، فيمكن أن يقلل Caching عدد الطلبات ويحسن زمن الاستجابة.

لا تجعل التطبيق يعتمد على ترتيب الحقول

عند التعامل مع JSON، يجب أن يعتمد التطبيق على أسماء الحقول وليس على ترتيب ظهورها.

مثلًا:

{ "id": 101, "name": "Product A", "price": 250 }

يجب ألا يفترض التطبيق أن id سيكون دائمًا أول عنصر.

JSON Object ليس جدولًا مرتبًا بالمعنى الذي ينبغي أن تعتمد عليه التطبيقات.

الأهم هو وجود Contract واضح بين العميل والخادم.

وثّق API منذ البداية

توثيق API ليس خطوة تجميلية.

إذا كانت الواجهة ستستخدم من تطبيق Android وتطبيق iOS ولوحة تحكم ويب، فالتوثيق يصبح ضروريًا.

يجب أن يعرف المطور:

عنوان نقطة النهاية.

HTTP Method.

Parameters.

Headers.

Authentication.

شكل Request.

شكل Response.

Status Codes.

أمثلة على الأخطاء.

يمكن استخدام أدوات مثل OpenAPI لوصف الواجهة بطريقة معيارية يمكن قراءتها من البشر والأدوات البرمجية.

كلما كانت API موثقة بشكل أفضل، قل الوقت اللازم لفهمها عند إضافة مطور جديد أو تطبيق جديد إلى النظام.

راقب API بعد إطلاق التطبيق

اختبار API قبل الإطلاق لا يكفي.

بعد وصول التطبيق إلى المستخدمين، يجب مراقبة:

زمن الاستجابة.

معدل الأخطاء.

عدد الطلبات.

أكثر نقاط النهاية استخدامًا.

معدلات فشل المصادقة.

أخطاء الخدمات الخارجية.

استهلاك الموارد في الخادم.

على سبيل المثال، إذا كان متوسط زمن الاستجابة جيدًا، لكن بعض الطلبات تستغرق عدة ثوانٍ، فإن المتوسط وحده قد يخفي المشكلة.

لذلك من المفيد النظر إلى توزيعات زمن الاستجابة، مثل:

p50 p95 p99

فـ p95 مثلًا يعطي تصورًا عن الزمن الذي يقع تحته 95% من الطلبات، ويساعد على اكتشاف التجارب البطيئة التي لا تظهر بوضوح عند النظر إلى المتوسط فقط.

اختبر حالات الفشل وليس النجاح فقط

قد يختبر المطور الطلب الناجح:

GET /products/101

لكن API الحقيقية ستتعامل أيضًا مع:

Product does not exist Unauthorized request Invalid parameters Expired token Rate limit exceeded Server error Network timeout

لذلك يجب أن تشمل الاختبارات الحالات الطبيعية وحالات الفشل.

ومن المفيد اختبار ما يحدث عندما يتلقى تطبيق الهاتف استجابة غير متوقعة أو عندما تتأخر الاستجابة أو ينقطع الاتصال قبل اكتمال الطلب.

هذا النوع من الاختبارات يكشف مشكلات لا تظهر عند اختبار المسار الناجح فقط.

كيف تبدو بنية API جيدة؟

يمكن تلخيص تصميم API لتطبيق هاتف محمول في مجموعة من المبادئ:

1. Resources واضحة 2. HTTP Methods مستخدمة بشكل صحيح 3. Responses متسقة 4. Error Handling منظم 5. Pagination للقوائم الكبيرة 6. Authentication واضحة 7. API Versioning مدروس 8. Business Logic منفصل عن HTTP 9. Documentation واضحة 10. Monitoring بعد الإطلاق

هذه ليست قائمة تنفيذ إلزامية لكل مشروع، وإنما نقاط ينبغي تقييمها بحسب طبيعة النظام وحجمه.

الخلاصة

تصميم API لتطبيقات الهاتف المحمول ليس مجرد إنشاء مجموعة من روابط HTTP التي تعيد JSON.

الواجهة البرمجية تمثل عقدًا بين تطبيق الهاتف والخدمات الخلفية، ولذلك يجب أن تكون واضحة وقابلة للتطور وقادرة على التعامل مع الأخطاء والتغير في البيانات والإصدارات المختلفة من التطبيق.

عند تصميم API، من المفيد التفكير في دورة حياة التطبيق كاملة:

التصميم → التطوير → الاختبار → الإطلاق → المراقبة → التوسع

فالتصميم الذي ينجح مع مئات المستخدمين قد يحتاج إلى تغييرات كبيرة عندما يصل النظام إلى عدد أكبر بكثير من المستخدمين. وكلما أُخذت قابلية التطور والتوافق مع الإصدارات السابقة ومراقبة الأداء في الحسبان منذ البداية، أصبح تطوير النظام على المدى الطويل أكثر قابلية للإدارة.

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

Recommended Posts

لا توجد أي إجابات على هذا السؤال بعد

انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أجب على هذا السؤال...

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.

  • إعلانات

  • تابعنا على



×
×
  • أضف...