أنا لست من أشد المعجبين بالأطر الكبيرة مثل NestJS؛ لقد أحببت دائمًا حرية إنشاء برنامجي بالطريقة التي أريدها مع البنية التي أقررها بطريقة خفيفة الوزن. لكن الشيء الذي أعجبني عند اختبار NestJS هو حقن التبعية.
حقن التبعية (DI) هو نمط تصميم يسمح لنا بتطوير تعليمات برمجية مقترنة بشكل غير محكم عن طريق إزالة مسؤولية إنشاء التبعيات وإدارتها من فصولنا. يعد هذا النمط ضروريًا لكتابة التطبيقات القابلة للصيانة والاختبار والقابلة للتطوير. في النظام البيئي لـ TypeScript، تبرز TSyringe كحاوية حقن تبعية قوية وخفيفة الوزن تعمل على تبسيط هذه العملية.
TSyringe عبارة عن حاوية حقن تبعية خفيفة الوزن لتطبيقات TypeScript/JavaScript. تحتفظ به Microsoft على GitHub (https://github.com/microsoft/tsyringe)، ويستخدم أدوات تزيين للقيام بحقن المُنشئ. بعد ذلك، يستخدم حاوية Inversion of Control لتخزين التبعيات بناءً على رمز مميز يمكنك استبداله بمثيل أو قيمة.
قبل الغوص في TSyringe، دعنا نستكشف بإيجاز ما هو حقن التبعية وسبب أهميته.
حقن التبعية هو أسلوب يتلقى فيه الكائن تبعياته من مصادر خارجية بدلاً من إنشائها بنفسه. يقدم هذا النهج العديد من الفوائد:
أولاً، لنقم بإعداد Tsyringe في مشروع TypeScript الخاص بك:
npm install tsyringe reflect-metadata
في tsconfig.json، تأكد من أن لديك الخيارات التالية:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
استيراد البيانات الوصفية العاكسة عند نقطة دخول التطبيق الخاص بك:
import "reflect-metadata";
نقطة الدخول لتطبيقك هي، على سبيل المثال، تخطيط الجذر في Next.js 13، أو يمكن أن يكون الملف الرئيسي في تطبيق Express صغير.
لنأخذ المثال من المقدمة ونضيف سكر TSyringe:
لنبدأ بالمحول.
// @/adapters/userAdapter.ts import { injectable } from "tsyringe" @injectable() class UserAdapter { constructor(...) {...} async fetchByUUID(uuid) {...} }
هل لاحظت مصمم الديكور @injectable()؟ إنها إخبار TSyringe أنه يمكن حقن هذه الفئة في وقت التشغيل.
لذلك تستخدم خدمتي المحول الذي أنشأناه للتو. دعونا نضيف هذا المحول إلى خدمتي.
// @/core/user/user.service.ts import { injectable, inject } from "tsyringe" ... @injectable() class UserService { constructor(@inject('UserAdapter') private readonly userAdapter: UserAdapter) {} async fetchByUUID(uuid: string) { ... const { data, error } = await this.userAdapter.fetchByUUID(uuid); ... } }
لقد استخدمت هنا أيضًا مصمم الديكور @injectable لأنه سيتم حقن الخدمة في فئة الأوامر الخاصة بي، لكنني أضفت أيضًا مصمم الديكور @inject في معلمات المُنشئ. يخبر هذا الديكور TSyringe بإعطاء المثيل أو القيمة التي يحتوي عليها للرمز المميز UserAdapter لخاصية userAdapter في وقت التشغيل.
وأخيرًا وليس آخرًا، جذر النواة الخاصة بي: فئة الأوامر (غالبًا ما تسمى بشكل خاطئ حالة الاستخدام).
// @/core/user/user.commands.ts import { inject } from "tsyringe" ... @injectable() class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } }
في هذه المرحلة، أخبرنا TSyringe بما سيتم حقنه وما سيتم حقنه في المُنشئ. لكننا مازلنا لم نصنع حاوياتنا لتخزين التبعيات. يمكننا أن نفعل ذلك بطريقتين:
يمكننا إنشاء ملف باستخدام سجل حقن التبعية الخاص بنا:
// @/core/user/user.dependencies.ts import { container } from "tsyringe" ... container.register("UserService", {useClass: UserService}) // associate the UserService with the token "UserService" container.register("UserAdapter", {useClass: UserAdapter}) // associate the UserAdapter with the token "UserAdapter" export { container }
ولكن يمكننا أيضًا استخدام @registry Decorator.
// @/core/user/user.commands.ts import { inject, registry, injectable } from "tsyringe" ... @injectable() @registry([ { token: 'UserService', useClass: UserService }, { token: 'UserAdapter', useClass: UserAdapter }, ]) export class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } } container.register("UserCommands", { useClass: UserCommands}) export { container }
كلتا الطريقتين لهما إيجابيات وسلبيات، ولكن في نهاية المطاف، إنها مسألة ذوق.
الآن بعد أن امتلأت حاويتنا بتبعياتنا، يمكننا الحصول عليها من الحاوية حسب الحاجة باستخدام طريقة الحل الخاصة بالحاوية.
import { container, UserCommands } from "@/core/user/user.commands" ... const userCommands = container.resolve("UserCommands") await userCommands.fetchByUUID(uuid) ...
هذا المثال بسيط جدًا حيث أن كل فئة تعتمد فقط على فئة أخرى، ولكن خدماتنا يمكن أن تعتمد على الكثير، كما أن حقن التبعية سيساعد حقًا في الحفاظ على كل شيء مرتبًا.
ولكن انتظر! لا تتركني هكذا! ماذا عن الاختبارات؟
يمكن أن تساعدنا حقننا أيضًا في اختبار الكود الخاص بنا عن طريق إرسال كائنات وهمية مباشرة إلى تبعياتنا. دعونا نرى مثال التعليمات البرمجية:
import { container, UserCommands } from "@/core/user/user.commands" describe("test ftw", () => { let userAdapterMock: UserAdapterMock let userCommands: UserCommands beforeEach(() => { userAdapterMock = new UserAdapter() container.registerInstance("UserAdapter", userAdapter) userCommands = container.resolve ("UserCommands") }); ... });
يحتوي الآن رمز UserAdapter على نموذج وهمي سيتم إدراجه في الفئات التابعة.
استخدام الرموز المميزة للتسمية: بدلاً من استخدام سلسلة حرفية لرموز الحقن، قم بإنشاء رموز مميزة:
export const USER_REPOSITORY_TOKEN = Symbol("UserRepository");
الحاويات ذات النطاق: استخدم الحاويات ذات النطاق للتبعيات ذات نطاق الطلب في تطبيقات الويب.
لا تفرط في استخدام DI : ليس كل شيء يحتاج إلى الحقن. استخدم DI للمخاوف الشاملة والتبعيات القابلة للتكوين.
إذا وصلت إلى هذا الحد، أود أن أشكرك على القراءة. أتمنى أن تجد هذه المقالة مفيدة. تذكر أن تأخذ في الاعتبار دائمًا الاحتياجات المحددة لمشروعك عند تنفيذ حقن التبعية والأنماط المعمارية.
الإعجابات والتعليقات هي أفضل الطرق للتحسين.
تعليمات سعيدة!
تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.
Copyright© 2022 湘ICP备2022001581号-3