cover

چگونه یک تشریح کامل برای v3 AsyncAPI بنویسیم؟

وقتی از API صحبت می‌کنیم، معمولاً منظورمان APIهایی است که در اینترنت پیدا می‌شوند و احتمالاً چیزی شبیه به REST را پیاده‌سازی کرده‌اند و از طریق HTTP سرو می‌شوند. این تعریف محدود از APIها با رشد و تنوع اقتصاد API، گسترده‌تر شده و اکنون شامل سبک‌های معماری و مکانیزم‌های انتقال دیگر نیز می‌شود. در این زمینه، مشخصه AsyncAPI مدت‌هاست که خبر بزرگی به حساب می‌آید و ما قبلاً هم در بلاگ درباره این زبان توصیف APIهای ناهمزمان نوشته‌ایم.

نسخه ۳.۰ AsyncAPI در دسامبر ۲۰۲۳ منتشر شد و در اکوسیستم ابزارها به‌خوبی مورد استقبال قرار گرفته است. ما قبلاً در مطلبی جداگانه درباره تغییرات نسخه ۳.۰ نوشته بودیم و در آنجا به ارزش دیدگاه عملیات‌محور (operation-orientated view) در نسخه ۳.۰ اشاره کردیم که Object Operation را از Channels جدا می‌کند. این جداسازی امکان ایجاد یک نمای مبتنی بر مورد استفاده (use case view) از AsyncAPI را فراهم می‌کند که در آن عملیات‌ها مبنای توصیف و برآورده کردن نیازهای مصرف‌کنندگان API هستند.

در این مطلب، با یک مثال عملی از یک مورد استفاده واقعی، تغییرات مهم نسخه ۳.۰ را بررسی می‌کنیم و نشان می‌دهیم این تغییرات چطور می‌توانند روش طراحی API موجود شما را بهبود دهند. از این مورد استفاده برای ساخت یک نمونه‌ای از توصیف AsyncAPI استفاده خواهیم کرد تا مزایای این رویکرد را روشن‌تر کنیم.

تمرکز بر موارد استفاده (Use Cases)

ابتدا بپرسیم: چرا باید روی موارد استفاده تمرکز کنیم؟

شاید این ایده که در سیستم‌های پیام‌رسانی روی موارد استفاده تمرکز کنیم عجیب یا انتزاعی به نظر برسد. بیشتر سیستم‌های پیام‌رسانی موجود پیام‌هایی را پیاده‌سازی می‌کنند که برای فراخوانی توابع کسب‌وکاری یا فنی هستند. اما AsyncAPI همان امکاناتی را که OpenAPI برای رویکرد «طراحی‌اول» (design-first) فراهم می‌کند، در اختیار ما می‌گذارد. بسیاری از کارشناسان جامعه اعلام کرده‌اند که دقیقاً همین نمای مبتنی بر مورد استفاده چیزی است که از یک زبان توصیف API می‌خواهند. مصرف‌کنندگان API و ذی‌نفعان آن‌ها به‌ندرت روی URIها یا صف‌های پیام تمرکز می‌کنند؛ آن‌ها بیشتر به این توجه دارند که API واقعاً چه کاری انجام می‌دهد و چه چیزی را «باید» انجام دهند تا با سرویس موردنظر یکپارچه شوند.

Object Operation در شکل جدید و جدا شده‌اش دقیقاً این نوع تفکر را پشتیبانی می‌کند. این شیء صرفاً برای توصیف موارد استفاده ساخته نشده، اما ساختار و انتزاع آن امکان توصیف راحت‌تر موارد استفاده را می‌دهد. می‌توانیم از ایدهٔ ساخت توصیف API بر اساس مورد استفاده برای نشان دادن نحوه نوشتن توصیف AsyncAPI با نسخه ۳.۰ استفاده کنیم.

مثال فرضی ما بر پایهٔ سفارش دارو با استفاده از پیام‌های Fast Healthcare Interoperability Resources (FHIR) است. FHIR یک استاندارد ساخته‌شده توسط HL7 (سازمان بین‌المللی استانداردسازی) برای ارسال و دریافت داده‌های سلامت الکترونیک و فراخوانی عملیات مرتبط با مراقبت‌های بهداشتی است. موارد استفاده در حوزه سلامت بسیار زیادند و استفاده از تعاریف پیام مبتنی بر استاندارد نقطه شروع فوق‌العاده‌ای برای ساخت توصیف AsyncAPI فراهم می‌کند — به‌ویژه در بازارهایی مثل ایالات متحده که پراکندگی زیادی بین ارائه‌دهندگان خدمات سلامت وجود دارد. استانداردسازی واقعاً به قابلیت همکاری (interoperability) کمک می‌کند و خدمات مختلف را کنار هم می‌آورد تا بهتر به بیماران خدمت کنند.

مورد استفاده ما برای سفارش دارو شامل دو عملیات است:

  • بازیابی اطلاعات بیمار و سوابق دارویی: این عملیات به پزشک اجازه می‌دهد بیماری‌های قبلی و داروهای فعلی بیمار را بررسی کند.
  • سفارش داروی جدید برای بیمار: پزشک می‌تواند داروی مورد نیاز بیمار را سفارش دهد.

در این مورد استفاده مراحل دیگری هم هست، اما همین دو مرحله امکانات کافی برای طراحی API با AsyncAPI v3.0 را فراهم می‌کنند. در این مطلب فقط بخش‌هایی از توصیف AsyncAPI را می‌آوریم، اما می‌توانید مثال کامل را ببینید و با AsyncAPI Studio آن را دقیق‌تر بررسی کنید.

ایجاد پیام‌های استاندارد

اولین قدم برای ساخت سرویس سفارش دارو، افزودن ساختارهای پیام بر اساس استاندارد FHIR است.

در پیام‌رسانی بین سیستم‌هایی که FHIR را پذیرفته‌اند و از پلتفرم‌های پیام‌رسانی ناهمزمان استفاده می‌کنند، مفهومی به نام «Bundle» وجود دارد که چندین درخواست یا پیام را با هم ارسال می‌کند. بنابراین ما یک نمایه از درخواست اطلاعات بیمار و «MedicationStatement» را در یک payload واحد ایجاد کردیم. Schema Objectهای مربوطه را با استفاده از خاصیت resourceType و شیء oneOf طراحی کردیم، مثلاً:

Schema Objectها تقریباً همان معناشناسی نسخه‌های قبلی AsyncAPI 2.x و OpenAPI را دارند. در مثال ما این‌ها در Components Object قرار گرفته‌اند تا در Message Objectهای مختلف قابل استفاده مجدد باشند. در مجموع چهار Schema ساختیم (درخواست و پاسخ برای هر دو عملیات) و سپس Message Objectهایی که به هر کدام ارجاع می‌دهند و می‌توانند معناشناسی پیام شامل traitهای مشترک مثل headerها را حمل کنند. یک header به نام health-system-id ساختیم که شناسه سطح بالای سیستم فرستنده را نشان می‌دهد.
YAML
- properties:
    resourceType:
      type: "string"
      enum: ["Patient"]
YAML
components:
  messages:
    patientMedicationStatementRequest:
      summary: Request message for patient and current medication data
      title: Patient and Medication Request
      payload:
        $ref: "#/components/schemas/patientMedicationStatementRequestPayload"
      traits:
        - $ref: "#/components/messageTraits/healthSystemHeaders"
    patientMedicationStatementResponse:
      summary: Response message for patient and current medication data
      title: Patient and Medication Response
      payload:
        $ref: "#/components/schemas/patientMedicationStatementRequestPayload"
      traits:
        - $ref: "#/components/messageTraits/healthSystemHeaders"
    patientMedicationRequest:
      summary: Request message for patient and current medication data
      title: Patient and Medication Request
      payload:
        $ref: "#/components/schemas/patientMedicationStatementRequestPayload"
      traits:
        - $ref: "#/components/messageTraits/healthSystemHeaders"
    patientMedicationResponse:
      summary: Request message for patient and current medication data
      title: Patient and Medication Request
      payload:
        $ref: "#/components/schemas/patientMedicationStatementRequestPayload"
      traits:
        - $ref: "#/components/messageTraits/healthSystemHeaders"
  schemas:
    patientMedicationStatementRequestPayload:
      type: "object"
      # Remainder of Schema Object definitions
  messageTraits:
    healthSystemHeaders:
      headers:
        type: object
        properties:
          health-system-id:
            description: Health system provider ID as UUID
            type: string
            pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$

این ساختارها برای کاربران فعلی AsyncAPI کاملاً آشنا خواهند بود. توجه: برای کوتاه شدن مطلب، ساختار پیام‌ها را بسیار ساده کرده‌ایم. سند اصلی JSON Schema FHIR بسیار جامع‌تر است.

افزودن کانال‌های ارتباطی

با آماده شدن پیام‌ها، Channel Objectها را ساختیم. کانال‌ها مکانیزم انتقال پیام‌ها را توصیف می‌کنند و یک صف یا topic خاص را به ساختارهای پیامی که پشتیبانی می‌کند مرتبط می‌کنند. در نسخه ۳.۰ تغییرات مهمی در Channel Object ایجاد شده: خاصیت‌های publish و subscribe حذف شده‌اند و Operation Object به یک خاصیت سطح بالا منتقل شده است.

با حذف Operationها، Channel Object بسیار سبک‌تر و متمرکز بر پیاده‌سازی فنی شده است. در مثال ما می‌بینید که کانال یک address (صف فرضی در سیستم پیام‌رسانی ما) تعریف می‌کند و از طریق خاصیت messages پیام‌های پشتیبانی‌شده را به کانال متصل می‌کند:

YAML
channels:
  patientMedicationStatements:
    description: Channel for requesting patient record and medication statement
    address: health.patient.medication.statement
    messages:
      patientMedicationStatementRequest:
        $ref: "#/components/messages/patientMedicationStatementRequest"
      patientMedicationStatementResponse:
        $ref: "#/components/messages/patientMedicationStatementResponse"
  patientMedicationRequest:
    description: Channel for ordering medication for a patient
    address: health.patient.medication.request
    messages:
      patientMedicationRequest:
        $ref: "#/components/messages/patientMedicationRequest"
      patientMedicationResponse:
        $ref: "#/components/messages/patientMedicationResponse"

در نسخه ۳.۰، شیء Channels تمرکز بسیار بیشتری روی جنبه‌های فنی دارد و این باعث می‌شود طراحان API بتوانند روی Operation Object تمرکز کنند و قابلیت استفاده مجدد با این انتزاع افزایش یابد.

توصیف عملیات‌ها

آخرین مرحله طراحی، ساخت Operation Objectها است که به مصرف‌کننده توصیف AsyncAPI می‌گوید برای یکپارچه شدن با API «چه کاری باید انجام دهد». چون نام Operation Object یک شناسه است، همه را با پیشوند order-medication/ نام‌گذاری کردیم تا گروه‌بندی شوند (می‌توانستیم از Tag Object هم استفاده کنیم).

دو Operation ساختیم که دقیقاً با دو عملیات مورد استفاده سفارش دارو مطابقت دارند:

YAML
operations:
  order-medication/get-patient-and-medication-statement:
    summary: Retrieve the patient record and current medication for the patient
    action: send
    channel:
      $ref: "#/channels/patientMedicationStatements"
    messages:
      - $ref: "#/channels/patientMedicationStatements/messages/patientMedicationStatementRequest"
    reply:
      address:
        location: "$message.header#/replyTo"
      channel:
        $ref: "#/channels/patientMedicationRequest"
      messages:
        - $ref: "#/channels/patientMedicationRequest/messages/patientMedicationResponse"
  order-medication/make-medication-request:
    summary: Send a medication request on-behalf of the patient
    action: send
    channel:
      $ref: "#/channels/patientMedicationRequest"
    messages:
      - $ref: "#/channels/patientMedicationRequest/messages/patientMedicationRequest"
    reply:
      address:
        location: "$message.header#/replyTo"
      channel:
        $ref: "#/channels/patientMedicationRequest"
      messages:
        - $ref: "#/channels/patientMedicationRequest/messages/patientMedicationResponse"

این عملیات‌ها به Channelهای ساخته‌شده ارجاع می‌دهند و پیام‌های آن‌ها را استفاده می‌کنند. همچنین از قابلیت جدید Operation Reply Object در نسخه ۳.۰ استفاده کردیم که الگوی بسیار رایج request/reply را پشتیبانی می‌کند. در مثال ما یک صف پاسخ پویا (dynamic reply queue) با Runtime Expression تعریف شده که سیستم پاسخ‌دهنده باید مقدار header replyTo را برای ارسال پاسخ استفاده کند.

توصیف‌های AsyncAPI مؤثر

جمع‌بندی یک توصیف AsyncAPI نسخه ۳.۰ با تمرکز روی یک مورد استفاده خاص، نکته مهمی را درباره ساختار جدید مشخصه نشان می‌دهد: افزودن Operation Object باعث جداسازی دغدغه‌ها (separation of concerns) بین توصیف پیام، انتقال و عملیات شده است.

علاوه بر امکان توصیف موارد استفاده، مزایای مهم دیگری هم دارد:

  • ثبات (Consistency): Operation Objectها در طول چرخه حیات API ثابت می‌مانند و امضای یکسانی دارند؛ بنابراین مدیریت آن‌ها آسان‌تر است چون درگیر معناشناسی انتقال نیستند.
  • قابلیت حمل (Portability): جداسازی Operation از Channel باعث می‌شود Channelها یک‌بار تعریف شوند و بارها (حتی در توصیف‌های مختلف AsyncAPI) استفاده شوند.
  • مالکیت (Ownership): تیم‌های مختلف می‌توانند بخش‌های مربوط به خود را جداگانه مدیریت کنند؛ مثلاً تیم عملیات فنی Channelها و Servers را تعریف کند و تیم طراحی API روی Operationها کار کند. این امکان رویکرد خط لوله (pipeline) و اتوماسیون را فراهم می‌کند.

ما در این مثال از رویکرد «پایین به بالا» (bottom-up) را دنبال کردیم: ابتدا ساختار payloadهای موجود از استاندارد باز را استفاده کردیم و در آخر Operationها را ساختیم. البته می‌توانید رویکرد خودتان را داشته باشید — مثلاً ابتدا Operationها را طراحی کنید و بعد جزئیات پیام‌رسانی را تکمیل کنید. رویکرد مبتنی بر مورد استفاده نتایج ملموسی دربارهٔ آنچه واقعاً می‌خواهید رابط شما انجام دهد، ارائه می‌دهد.

تنها نقطه ضعف فعلی در توصیف موارد استفاده با AsyncAPI، بیان ترتیب و وابستگی‌ها است. AsyncAPI این اطلاعات را از طریق Operation Object منتقل نمی‌کند (فقط با description و نام‌گذاری). اما قدم بعدی در مشخصه Arazzo از OpenAPI Initiative خواهد بود. پشتیبانی از AsyncAPI در نسخه ۱.۱.۰ (اواسط ۲۰۲۵) در نقشه راه قرار دارد.

Arazzo امکانات بسیار غنی‌تری برای توصیف موارد استفاده انتها به انتها، شامل وابستگی‌ها، مقادیر پویا بین عملیات‌ها و شرایط خطا خواهد داشت. این همراه با Operation Object جدا شده، بهبود تجربه توسعه‌دهنده از طریق توصیف‌های غنی موارد استفاده برای APIهای ناهمزمان را کاملاً عملی می‌کند.

منظور از ریشه‌های اصلی ای‌پی‌آی دریفت (API Drift) چیست؟
چگونه تسترهای تضمین کیفیت بایستی از APIها بهره ببرند؟

دیدگاهتان را بنویسید

سبد خرید
علاقه‌مندی‌ها
مشاهدات اخیر
دسته بندی ها