تصميم واجهات برمجة التطبيقات للخدمات المصغرة

Azure DevOps

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

تصميم واجهة برمجة التطبيقات للخدمات المصغرة

من المهم التمييز بين نوعين من واجهة برمجة التطبيقات:

  • واجهات برمجة التطبيقات العامة التي تستدعيها تطبيقات العميل.
  • واجهات برمجة التطبيقات الخلفية المستخدمة للتواصل بين الخدمات.

تحتوي حالتا الاستخدام هاتين على متطلبات مختلفة إلى حد ما. يجب أن تكون واجهة برمجة التطبيقات العامة متوافقة مع تطبيقات العميل، عادة تطبيقات المتصفح أو تطبيقات الجوال الأصلية. في معظم الوقت، وهذا يعني أن واجهة برمجة التطبيقات العامة ستستخدم REST عبر HTTP. ومع ذلك، بالنسبة لواجهات برمجة التطبيقات الخلفية، تحتاج إلى أخذ أداء الشبكة في الاعتبار. اعتمادًا على دقة خدماتك، يمكن أن ينتج عن الاتصال بين الخدمات ارتفاع في نسبة استخدام الشبكة. يمكن أن تصبح الخدمات مرتبطة ب I/O بسرعة. ولهذا السبب، تصبح اعتبارات مثل سرعة إنشاء تسلسل وحجم الحمولة أكثر أهمية. تتضمن بعض البدائل الشائعة لاستخدام REST عبر HTTP gRPC وApache Avro وApache Thrift. تدعم هذه البروتوكولات التسلسل الثنائي وهي بشكل عام أكثر كفاءة من HTTP.

الاعتبارات

فيما يلي بعض الأشياء التي يجب التفكير فيها عند اختيار كيفية تنفيذ واجهة برمجة التطبيقات.

REST مقابل RPC. ضع في اعتبارك المقايضات بين استخدام واجهة نمط REST مقابل واجهة نمط RPC.

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

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

بالنسبة لواجهة RESTful، فإن الخيار الأكثر شيوعاً هو REST عبر HTTP باستخدام JSON. بالنسبة لواجهة نمط RPC، هناك العديد من أطر العمل الشائعة، بما في ذلك gRPC وApache Avro وApache Thrift.

الكفاءة. ضع في اعتبارك الكفاءة من حيث السرعة والذاكرة وحجم الحمولة. عادة ما تكون الواجهة المستندة إلى gRPC أسرع من REST عبر HTTP.

لغة تعريف الواجهة (IDL). يتم استخدام IDL لتعريف الأساليب والمعلمات وقيم الإرجاع لواجهة برمجة التطبيقات. يمكن استخدام IDL لإنشاء التعليمات البرمجية للعميل ورمز التسلسل ووثائق واجهة برمجة التطبيقات. يمكن أيضاً استهلاك عناوين IDLs بواسطة أدوات اختبار واجهة برمجة التطبيقات مثل Postman. تحدد أطر العمل مثل gRPC وAvro وThrift مواصفات IDL الخاصة بها. لا يحتوي REST عبر HTTP على تنسيق IDL قياسي، ولكن الخيار الشائع هو OpenAPI (المعروف سابقاً باسم Swagger). يمكنك أيضًا إنشاء واجهة برمجة تطبيقات HTTP REST بدون استخدام لغة تعريف رسمية، ولكن بعد ذلك ستفقد مزايا إنشاء الكود واختباره.

إنشاء تسلسل. كيف يتم تسلسل العناصر عبر السلك؟ تتضمن الخيارات التنسيقات المستندة إلى النص (JSON في المقام الأول) والتنسيقات الثنائية مثل المخزن المؤقت للبروتوكول. تكون التنسيقات الثنائية بشكل عام أسرع من التنسيقات المستندة إلى النص. ومع ذلك، تتمتع JSON بمزايا من حيث إمكانية التشغيل التفاعلي، لأن معظم اللغات وأطر العمل تدعم تسلسل JSON. تتطلب بعض تنسيقات التسلسل مخططاً ثابتاً، وبعضها يتطلب تحويل ملف تعريف مخطط برمجياً. في هذه الحالة، ستحتاج إلى دمج هذه الخطوة في عملية الإنشاء الخاصة بك.

دعم إطار العمل واللغة. يتم دعم HTTP في كل إطار عمل ولغة تقريباً. تحتوي كل من gRPC وAvro وThrift على مكتبات تعليمات برمجية ل C++، وC#، وJava، وPython. يدعم كل من Thrift وgRPC أيضاً Go.

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

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

ومع ذلك، إذا اخترت REST عبر HTTP، يجب عليك إجراء اختبار الأداء والتحميل في وقت مبكر من عملية التطوير، للتحقق ما إذا كان يعمل بشكل جيد بما يكفي للسيناريو الخاص بك.

تصميم واجهة برمجة تطبيقات RESTful

هناك العديد من الموارد لتصميم واجهات برمجة تطبيقات RESTful. فيما يلي بعض النصائح التي قد تجدها مفيدة:

فيما يلي بعض الاعتبارات المحددة التي يجب وضعها في الاعتبار.

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

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

  • بالنسبة للعمليات ذات الآثار الجانبية، ضع في اعتبارك جعلها غير فعالة وتنفيذها كأساليب PUT. سيؤدي ذلك إلى تمكين عمليات إعادة المحاولة الآمنة ويمكن أن يحسن المرونة. تناقش مقالة اتصالات Interservice هذه المشكلة بمزيد من التفصيل.

  • يمكن أن يكون لأساليب HTTP دلالات غير متزامنة، حيث يقوم الأسلوب بإرجاع استجابة على الفور، ولكن الخدمة تنفذ العملية بشكل غير متزامن. في هذه الحالة، يجب أن يرجع الأسلوب رمز استجابة HTTP 202، والذي يشير إلى قبول الطلب للمعالجة، ولكن لم تكتمل المعالجة بعد. لمزيد من المعلومات، راجع نمطAsynchronous Request-Reply.

تعيين REST إلى أنماط DDD

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

export class Location {
    readonly latitude: number;
    readonly longitude: number;

    constructor(latitude: number, longitude: number) {
        if (latitude < -90 || latitude > 90) {
            throw new RangeError('latitude must be between -90 and 90');
        }
        if (longitude < -180 || longitude > 180) {
            throw new RangeError('longitude must be between -180 and 180');
        }
        this.latitude = latitude;
        this.longitude = longitude;
    }
}

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

مثال آخر هو نمط المستودع، الذي يضمن أن أجزاء أخرى من التطبيق لا تجري قراءات مباشرة أو يكتب إلى مخزن البيانات:

رسم تخطيطي لمستودع الطائرات بدون طيار.

ومع ذلك، في بنية الخدمات المصغرة، لا تشترك الخدمات في نفس قاعدة التعليمات البرمجية ولا تشارك مخازن البيانات. بدلاً من ذلك، يتصلون من خلال واجهات برمجة التطبيقات. ضع في اعتبارك الحالة التي تطلب فيها خدمة Scheduler معلومات حول طائرة بدون طيار من خدمة Drone. تحتوي خدمة الطائرات بدون طيار على نموذجها الداخلي ل drone، يتم التعبير عنها من خلال التعليمات البرمجية. ولكن Schedular لا يرى ذلك. بدلاً من ذلك، فإنه يحصل على تمثيل كيان خدمة drone — ربما كائن JSON في استجابة HTTP.

هذا المثال مثالي للصناعات الجوية والطائرات.

رسم تخطيطي لخدمة الطائرات بدون طيار.

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

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

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

  • تعين التجميعات بشكل طبيعي إلى الموارد في REST. على سبيل المثال، سيتم عرض تجميع التسليم كمورد بواسطة واجهة برمجة تطبيقات التسليم.

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

  • الكيانات لها هويات فريدة. في REST، تحتوي الموارد على معرفات فريدة في شكل عناوين URL. إنشاء عناوين URL للمورد تتوافق مع هوية مجال الكيان. قد يكون التعيين من عنوان URL إلى هوية المجال معتماً للعميل.

  • يمكن الوصول إلى الكيانات التابعة لمجموع عن طريق التنقل من الكيان الجذر. إذا اتبعت مبادئ HATEOAS، يمكن الوصول إلى الكيانات التابعة عبر ارتباطات في تمثيل الكيان الأصل.

  • نظراً لأن كائنات القيمة غير قابلة للتغيير، يتم تنفيذ التحديثات عن طريق استبدال كائن القيمة بأكمله. في REST، نفذ التحديثات من خلال طلبات PUT أو PATCH.

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

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

مفهوم DDD مكافئ REST مثال
التجميع المورد { "1":1234, "status":"pending"... }
الهوية عنوان URL https://delivery-service/deliveries/1
الكيانات التابعة الارتباطات { "href": "/deliveries/1/confirmation" }
تحديث كائنات القيمة PUT أو PATCH PUT https://delivery-service/deliveries/1/dropoff
المستودع المجموعة https://delivery-service/deliveries?status=pending

تعيين إصدار واجهة برمجة التطبيقات

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

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

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

رسم تخطيطي يوضح خيارين لدعم تعيين الإصدار.

إن الرسم يتكون من جزئين. تعرض "الخدمة تدعم إصدارين" كل من عميل الإصدار الأول وعميل الإصدار الثاني يشيران إلى خدمة واحدة. يظهر "النشر جنباً إلى جنب" عميل الإصدار الأول الذي يشير إلى خدمة v1، وعميل الإصدار الثاني الذي يشير إلى خدمة v2.

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

عند تغيير تطبيق الخدمة، من المفيد وضع علامة على التغيير باستخدام إصدار. يوفر الإصدار معلومات مهمة عند استكشاف الأخطاء وإصلاحها. يمكن أن يكون مفيداً جداً لتحليل السبب الجذري لمعرفة أي إصدار من الخدمة تم استدعاؤه بالضبط. ضع في اعتبارك استخدام الإصدار الدلالي لإصدارات الخدمة. يستخدم تعيين الإصدار الدلالي تنسيق MAJOR.MINOR.PATCH. ومع ذلك، يجب على العملاء تحديد واجهة برمجة التطبيقات فقط بواسطة رقم الإصدار الرئيسي، أو ربما الإصدار الثانوي إذا كانت هناك تغييرات كبيرة (ولكن غير فاصلة) بين الإصدارات الثانوية. بمعنى آخر، من المعقول للعملاء الاختيار بين الإصدار 1 والإصدار 2 من واجهة برمجة التطبيقات، ولكن ليس لتحديد الإصدار 2.1.3. إذا سمحت بهذا المستوى من النقاوة، فإنك تخاطر بالإضطرار إلى دعم انتشار الإصدارات.

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

تصميم عمليات متكررة

تكون العملية خاملة إذا كان من الممكن استدعاؤها عدة مرات دون إحداث آثار جانبية إضافية بعد الاتصال الأول. يمكن أن تكون Idempotency استراتيجية مرونة مفيدة، لأنها تسمح لخدمة المنبع باستدعاء عملية بأمان عدة مرات. لمناقشة هذه النقطة، راجع المعاملات الموزعة.

تنص مواصفات HTTP على أن أساليب GET و PUT وDELETE يجب أن تكون غير فعالة. أساليب POST غير مضمونة لتكون متكررة. إذا كان أسلوب POST ينشئ مورداً جديداً، فلا يوجد ضمان بشكل عام بأن هذه العملية غير فعالة. تحدد المواصفات التكرار بهذه الطريقة:

يعتبر أسلوب الطلب "غير فعال" إذا كان التأثير المقصود على خادم طلبات متطابقة متعددة مع هذا الأسلوب هو نفس تأثير طلب واحد من هذا القبيل. (RFC 7231)

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

  • بالنسبة لأسلوب POST، يمثل URI مورداً رئيسياً للكيان الجديد، مثل المجموعة. على سبيل المثال، لإنشاء تسليم جديد، قد يكون /api/deliveriesURI. ينشئ الخادم الكيان ويعين له URI جديداً، مثل /api/deliveries/39660. يتم إرجاع URI هذا في عنوان الموقع للاستجابة. في كل مرة يرسل فيها العميل طلباً، سيقوم الخادم بإنشاء كيان جديد باستخدام URI جديد.

  • بالنسبة لأسلوب PUT، يحدد URI الكيان. إذا كان هناك بالفعل كيان مع URI هذا، يستبدل الخادم الكيان الموجود بالإصدار في الطلب. إذا لم يكن هناك كيان مع URI هذا، يقوم الخادم بإنشاء كيان. على سبيل المثال، افترض أن العميل يرسل طلب PUT إلى api/deliveries/39660. بافتراض عدم وجود تسليم مع URI هذا، يقوم الخادم بإنشاء واحد جديد. الآن إذا أرسل العميل نفس الطلب مرة أخرى، سيحل الخادم محل الكيان الموجود.

فيما يلي تنفيذ خدمة التسليم لأسلوب PUT.

[HttpPut("{id}")]
[ProducesResponseType(typeof(Delivery), 201)]
[ProducesResponseType(typeof(void), 204)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
    logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
    try
    {
        var internalDelivery = delivery.ToInternal();

        // Create the new delivery entity.
        await deliveryRepository.CreateAsync(internalDelivery);

        // Create a delivery status event.
        var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
        await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);

        // Return HTTP 201 (Created)
        return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
    }
    catch (DuplicateResourceException)
    {
        // This method is mainly used to create deliveries. If the delivery already exists then update it.
        logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);

        var internalDelivery = delivery.ToInternal();
        await deliveryRepository.UpdateAsync(id, internalDelivery);

        // Return HTTP 204 (No Content)
        return NoContent();
    }
}

من المتوقع أن تنشئ معظم الطلبات كياناً جديداً، لذلك يستدعي CreateAsync الأسلوب بشكل متفائل على كائن المستودع، ثم يعالج أي استثناءات مورد مكرر عن طريق تحديث المورد بدلاً من ذلك.

الخطوات التالية

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