وابستگی دایره‌ای در NestJS

پیش‌نیازها
برای دنبال کردن این آموزش، شما به موارد زیر نیاز دارید:
• نصب Node.js (نسخه >= 12) بر روی سیستم عامل خود. شما می‌توانید راهنمای نصب Node.js را مطالعه کنید.
• یک ویرایشگر کد مانند VSCode.
• آشنایی قبلی با مفاهیم Dependency Injection، providers، controllers و modules در NestJS.
وابستگی دایره‌ای بین ماژول‌ها
حال بیایید دقیق‌تر به خطایی که زمانی که OrdersModule و PaymentModule به یکدیگر وابسته‌اند نگاه کنیم. این خطا به شکل زیر ظاهر می‌شود:
Nest cannot create the PaymentModule instance.
The module at index [0] of the PaymentModule “imports” array is undefined.
در NestJS، ماژول‌ها می‌توانند به یکدیگر وابسته باشند، اما زمانی که دو ماژول مستقیماً یکدیگر را وارد کنند، وابستگی دایره‌ای ایجاد می‌شود که NestJS قادر به حل آن نیست. این به این معناست که هر ماژول منتظر است تا ماژول دیگر به‌طور کامل اولیه‌سازی شود تا خودش هم بتواند شروع به کار کند، که این باعث ایجاد بن‌بست می‌شود.
وقتی NestJS اپلیکیشن را راه‌اندازی می‌کند، ابتدا تمام ماژول‌ها و وابستگی‌های آن‌ها را شبیه‌سازی می‌کند، سپس نمونه‌های ماژول‌ها را ایجاد می‌کند.
در ابتدا، NestJS به آرایه imports در ماژول اصلی (مانند app.module.ts) نگاه می‌کند و شروع به حل ماژول‌های آن می‌کند. اگر در OrdersModule، PaymentModule وارد شده باشد، NestJS ابتدا باید PaymentModule را حل کند. اما وقتی به PaymentModule می‌رسد، می‌بیند که OrdersModule نیز در لیست واردات آن است. بنابراین، NestJS باید ابتدا OrdersModule را حل کند. این ایجاد یک حلقه بی‌پایان می‌کند، چرا که OrdersModule منتظر PaymentModule است و PaymentModule منتظر OrdersModule است. اینگونه یک بن‌بست به وجود می‌آید.
رفع خطای وابستگی دایره‌ای بین ماژول‌ها
برای حل این مشکل، NestJS از تابع کمکی forwardRef استفاده می‌کند که اجازه می‌دهد ماژول‌ها به‌طور دایره‌ای به یکدیگر وابسته شوند بدون اینکه باعث بن‌بست شوند.
در کد زیر، با استفاده از forwardRef، ماژول‌ها را به‌طور صحیح وارد می‌کنیم تا وابستگی دایره‌ای حل شود.
فایل payment.module.ts:
import { Module, forwardRef } from “@nestjs/common”;
import { PaymentService } from “./payment.service”;
import { PaymentController } from “./payment.controller”;
import { OrdersModule } from “../orders/orders.module”;
@Module({
  imports: [forwardRef(() => OrdersModule)],  // استفاده از forwardRef برای حل وابستگی دایره‌ای
  controllers: [PaymentController],
  providers: [PaymentService],
  exports: [PaymentService],
})
export class PaymentModule {}
فایل orders.module.ts:
import { Module, forwardRef } from “@nestjs/common”;
import { OrdersService } from “./orders.service”;
import { OrdersController } from “./orders.controller”;
import { PaymentModule } from “../payment/payment.module”;
@Module({
  imports: [forwardRef(() => PaymentModule)],  // استفاده از forwardRef برای حل وابستگی دایره‌ای
  controllers: [OrdersController],
  providers: [OrdersService],
  exports: [OrdersService],
})
export class OrdersModule {}
با استفاده از forwardRef، ما می‌گوییم که این ماژول‌ها باید بعداً وارد شوند و NestJS قادر خواهد بود که این وابستگی‌های دایره‌ای را حل کند.
حالا کد خود را ذخیره کرده و دوباره ترمینال را راه‌اندازی کنید. پس از این تغییرات، باید خطا برطرف شود و اپلیکیشن شما به درستی اجرا شود.
در کد بالا، forwardRef برای پوشش دادن وابستگی‌ها در هر دو ماژول استفاده شده است تا از بارگذاری مستقیم و فوری وابستگی‌های متقابل جلوگیری شود. در این حالت، وقتی NestJS فرآیند اولیه‌سازی ماژول‌ها را شروع می‌کند و forwardRef را در آرایه imports یکی از ماژول‌ها مشاهده می‌کند، بلافاصله تلاش نمی‌کند که هر دو ماژول را حل کند. بلکه وابستگی‌ها را شناسایی می‌کند اما حل آن‌ها را به تأخیر می‌اندازد. بنابراین، هر دو ماژول OrdersModule و PaymentModule به بافت اپلیکیشن بارگذاری می‌شوند بدون اینکه وابستگی‌های آن‌ها به‌طور کامل حل شوند تا از بن‌بست جلوگیری شود. پس از بارگذاری همه ماژول‌ها، NestJS به سراغ وابستگی‌های معوقه می‌رود و با استفاده از forwardRef وابستگی‌ها را حل می‌کند.
رفع خطای وابستگی دایره‌ای بین سرویس‌ها
در بخش قبلی، نحوه حل وابستگی دایره‌ای در سطح ماژول‌ها را یاد گرفتید. حالا در این بخش، وابستگی دایره‌ای که در سرویس‌ها ایجاد می‌شود را بررسی می‌کنیم.
در سناریوی جدید، در OrdersService یک سفارش ایجاد می‌کنید، آن را در پایگاه داده ذخیره می‌کنید و سپس بلافاصله متد processPayment را برای پردازش سفارش فراخوانی می‌کنید. در PaymentService، زمانی که متد processPayment فراخوانی می‌شود، بلافاصله متد updateOrderStatus را برای به‌روزرسانی وضعیت سفارش به وضعیت موفق یا هر وضعیتی که بخواهید، فراخوانی می‌کند.
در اینجا نحوه تزریق وابستگی‌ها در هر دو سرویس آورده شده است:
فایل order.service.ts:
interface IOrder {
  id: number;
  customerName: string;
  item: string;
  orderDate: Date;
  totalAmount: number;
}
@Injectable()
export class OrdersService {
  constructor(private readonly paymentService: PaymentService) {}
  async getAllOrders(): Promise<IOrder[]> {
    const mockOrders: IOrder[] = [
      {
        id: 1,
        customerName: ‘Taofiq’,
        item: ‘Airpod’,
        orderDate: new Date(),
        totalAmount: 900,
      },
    ];
    return mockOrders;
  }
  async createOrder(createOrderDTO: CreateOrderDTO): Promise<any> {
    // mock data to simulate order creation
    const newOrder: any = {
      id: Math.floor(Math.random() * 100) + 1,
      …createOrderDTO,
      orderDate: new Date(createOrderDTO.orderDate),
    };
    await this.paymentService.processPayment(newOrder.id);
    return newOrder;
  }
  async updateOrderStatus(orderId: string, status: string) {
    // update the order status here in the database
    console.log(`Order ${orderId} status updated to ${status}`);
  }
}
فایل payment.service.ts:
@Injectable()
export class PaymentService {
  constructor(private readonly ordersService: OrdersService) {}
  async processPayment(orderId: string) {
    // In a real scenario, you would interact with a payment gateway.
    console.log(`Processing payment for order ${orderId}`);
    const paymentSuccessful = true;
    if (paymentSuccessful) {
      // Once payment is successful, update the order status to “Paid”
      await this.ordersService.updateOrderStatus(orderId, ‘Paid’);
    }
  }
}
در اینجا، وقتی کد را ذخیره کرده و سرور را دوباره راه‌اندازی می‌کنید، با این خطا مواجه می‌شوید:
Nest cannot create the OrdersService instance.
The module at index [0] of the OrdersService “providers” array is undefined.
این خطا نشان می‌دهد که همچنان وابستگی دایره‌ای در سطح سرویس‌ها وجود دارد. برای رفع این مشکل، باید از forwardRef برای حل وابستگی دایره‌ای بین سرویس‌ها استفاده کنیم. به عبارت دیگر، هنگامی که OrdersService به PaymentService و برعکس وابسته است، باید همانند ماژول‌ها، این وابستگی را تأخیر بیندازیم تا از ایجاد بن‌بست جلوگیری شود.
در این حالت، مشابه روش استفاده از forwardRef در ماژول‌ها، باید در سرویس‌ها نیز از آن بهره ببریم.
این خطا به این دلیل اتفاق می‌افتد که سیستم تزریق وابستگی (Dependency Injection) در NestJS نمی‌تواند وابستگی دایره‌ای مستقیم بین OrdersService و PaymentService را حل کند.
وقتی اپلیکیشن شروع می‌شود، هر دو ماژول OrdersModule و PaymentModule مقداردهی اولیه می‌شوند و ثبت می‌شوند. سپس NestJS تلاش می‌کند تا PaymentService را مقداردهی کند و متوجه می‌شود که برای تزریق به آن به OrdersService نیاز دارد. NestJS به دنبال یک نمونه از OrdersService در محدوده PaymentModule می‌گردد که باعث ایجاد وابستگی دایره‌ای در سطح سرویس‌ها می‌شود.
برای حل این مشکل، می‌توانید از دکوراتور @Inject به همراه تابع forwardRef در سازنده‌های سرویس‌ها استفاده کنید. این کار به NestJS می‌گوید که حل وابستگی را همانطور که در سطح ماژول از forwardRef استفاده کردیم به تأخیر بیندازد.
نمونه کد:
فایل order.service.ts:
@Injectable()
export class OrdersService {
  constructor(
    @Inject(forwardRef(() => PaymentService))
    private readonly paymentService: PaymentService,
  ) {}
}
فایل payment.service.ts:
@Injectable()
export class PaymentService {
  constructor(
    @Inject(forwardRef(() => OrdersService))
    private readonly ordersService: OrdersService,
  ) {}
}
با استفاده از @Inject(forwardRef(() => ServiceName))، کانتینر DI در NestJS حل وابستگی‌های مشخص شده را تا زمانی که به آن نیاز نباشد به تأخیر می‌اندازد. حالا هر دو OrdersService و PaymentService می‌توانند بدون نیاز فوری به حل کامل وابستگی‌های یکدیگر مقداردهی اولیه شوند. این کار وابستگی دایره‌ای را در سطح سرویس‌ها شکسته و به NestJS اجازه می‌دهد که وابستگی‌ها را به‌طور موفقیت‌آمیز تزریق کند.
ماژول مشترک به عنوان یک راه‌حل جایگزین
فرض کنید در اپلیکیشن تجارت الکترونیکی شما، می‌خواهید فرآیند بازپرداخت را داشته باشید که در آن کاربران می‌توانند درخواست بازپرداخت سفارشات خود را در صورت عدم رضایت ثبت کنند. این ویژگی شامل ماژول‌های Order و Payment می‌شود.
• OrderService باید شرایط صلاحیت بازپرداخت سفارش را بررسی کند (مثلاً زمان‌بندی بازگشت یا اینکه آیا قبلاً بازپرداخت شده است یا خیر).
• PaymentService باید بازپرداخت را از طریق هر درگاه پرداختی که تصمیم دارید یکپارچه کنید پردازش کرده و به OrderService بگوید که وضعیت سفارش را به “بازپرداخت شده” تغییر دهد.
این حالت باعث ایجاد وابستگی دایره‌ای می‌شود زیرا OrderService به PaymentService برای پردازش بازپرداخت نیاز دارد و PaymentService به OrderService برای به‌روزرسانی وضعیت بازپرداخت نیاز دارد.
به جای حل وابستگی دایره‌ای با استفاده از تابع forwardRef، می‌توانید سرویس‌ها را از یکدیگر جدا کرده و از یک ماژول مشترک به نام RefundManagementModule استفاده کنید. این ماژول فرآیند بازپرداخت را به شرح زیر هماهنگ می‌کند:
فایل refund-management.service.ts:
import { Injectable } from ‘@nestjs/common’;
import { OrderService } from ‘./order.service’;
import { PaymentService } from ‘./payment.service’;
@Injectable()
export class RefundManagementService {
  constructor(
    private orderService: OrderService,
    private paymentService: PaymentService
  ) {}
  async processRefund(orderId: string) {
    const eligible = await this.orderService.checkRefundEligibility(orderId);
    if (!eligible) {
      throw new Error(‘Order not eligible for refund’);
    }
    const refundSuccessful = await this.paymentService.processRefund(orderId);
    if (refundSuccessful) {
      await this.orderService.updateOrderStatus(orderId, ‘Refunded’);
    }
  }
}
در اینجا، checkRefundEligibility شرایط صلاحیت برای بازپرداخت یک سفارش را بررسی می‌کند. اگر معیارهای صلاحیت پاس شود، به پردازش بازپرداخت برای آن سفارش از طریق متد processRefund در PaymentService ادامه می‌دهد. سپس این سرویس می‌تواند در ماژول RefundManagementModule صادر شده و در هر یک از OrdersService و PaymentModule وارد شود. با این کار، دیگر دو ماژول به یکدیگر وابسته نیستند.
نکته:
زمانی که در حال نوشتن اپلیکیشن‌های سروری خود هستید، باید تا حد امکان از وابستگی دایره‌ای اجتناب کنید. هنگامی که متوجه شدید که وابستگی دایره‌ای دارید، سعی کنید راه‌هایی برای بازسازی معماری اپلیکیشن خود پیدا کنید و کد خود را تقسیم کنید به‌طوری‌که نیازی به ارزیابی تنبل مانند متد forwardRef نداشته باشید. اگر دو سرویس در یک زمینه ماژول نیاز به یک متد دارند، می‌توانید آن را به یک پوشه مشترک منتقل کنید تا یک ارائه‌دهنده و ماژول utility مخصوص خود را داشته باشد.
استفاده از Madge برای شناسایی وابستگی‌های دایره‌ای
برای شناسایی وابستگی‌های دایره‌ای در اپلیکیشن خود می‌توانید از ابزار توسعه‌دهنده‌ای به نام Madge استفاده کنید. Madge یک گراف بصری از وابستگی‌های ماژول‌های شما تولید می‌کند و وابستگی‌های دایره‌ای را در کد شما پیدا می‌کند.
برای نصب Madge به عنوان وابستگی، از دستور npm i madge یا yarn add madge استفاده کنید و سپس دستور زیر را اجرا کنید:
npx madge –circular src/main.ts
یا
yarn madge –circular src/main.ts
متد circular() آرایه‌ای از تمام ماژول‌هایی که وابستگی دایره‌ای دارند را باز می‌گرداند. برای یادگیری روش‌های دیگر که می‌توانید هنگام استفاده از این پکیج فراخوانی کنید، به این لینک مراجعه کنید.
زمانی که این دستور را اجرا می‌کنید، باید خروجی مشابه زیر در ترمینال مشاهده کنید که به شما نشان می‌دهد فایل‌های دایره‌ای شما کجا هستند:
تزئین‌کننده @Inject یکی از اجزای اساسی سیستم تزریق وابستگی (DI) در NestJS است. عملکرد اصلی آن، تعریف صریح یک وابستگی است که باید به یک کلاس (معمولاً یک provider) تزریق شود.
نتیجه‌گیری
در این مقاله، به بررسی وابستگی دایره‌ای در NestJS، انواع آن و بهترین روش‌ها برای رفع این مشکل پرداختید. کد منبع کامل این آموزش را می‌توانید اینجا در گیت‌هاب پیدا کنید.
برای امتیاز به این نوشته کلیک کنید!
[کل: 0 میانگین: 0]

نظرات کاربران

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

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *