تزریق وابستگی (Dependency Injection) یک تکنیک قدرتمند است که کدهای ماژولار و آزادانه را با مدیریت وابستگیهای یک برنامه کاربردی و تسهیل تزریق آنها به کلاسها ترویج میکند. در تایپ اسکریپت، پیاده سازی تزریق وابستگی گاهی اوقات می تواند دست و پا گیر و وقت گیر باشد. در این مقاله از آنوفل می خواهیم به صورت عمیق با تزریق وابستگی و انواع آن در تایپ اسکریپت آشنا شویم.
ابتدا بیاید با وابستگی آشنا شویم تا درک تزریق وابستگی برای ما آسان تر گردد.
وابستگی ها (Dependency) چیست؟
برای سهولت مرجع، ما یک وابستگی را به عنوان هر ماژولی که توسط ماژول ما استفاده می شود تعریف می کنیم. ما در مقاله تزریق وابستگی در مقابل وارونگی کنترل و وارونگی وابستگی به صورت عمیق و مفهومی به آن پرداختیم و می توانید ابتدا این مقاله را بررسی کنید و سپس برای پیاده سازی آن در تایپ اسکریپت و راهنمایی بیشتر به این مقاله مراجعه کنید.
بیایید به تابعی نگاه کنیم که دو عدد می گیرد و یک عدد تصادفی را در یک محدوده برمی گرداند:
const getRandomInRange = (min: number, max: number): number =>
Math.random() * (max - min) + min;
تابع به دو آرگومان بستگی دارد: min
و max
.
اما می بینید که تابع نه تنها به آرگومان ها، بلکه به تابع Math.random
نیز بستگی دارد. اگر Math.random
تعریف نشده باشد، تابع getRandomInRange
ما نیز کار نخواهد کرد. یعنی getRandomInRange
به عملکرد یک ماژول دیگر بستگی دارد. بنابراین Math.random
نیز یک وابستگی است.
بیایید به صراحت وابستگی را از طریق آرگومان ها عبور دهیم:
const getRandomInRange = (
min: number,
max: number,
random: () => number,
): number => random() * (max - min) + min;
اکنون این تابع نه تنها از دو عدد، بلکه از یک تابع تصادفی استفاده می کند که عدد را برمی گرداند. ما getRandomInRange
را به این صورت صدا می کنیم:
const result = getRandomInRange(1, 10, Math.random);
برای جلوگیری از عبور دائمی Math.random
، میتوانیم آن را به عنوان مقدار پیشفرض آخرین آرگومان تبدیل کنیم.
const getRandomInRange = (
min: number,
max: number,
random: () => number = Math.random
): number => random() * (max - min) + min;
این پیاده سازی اولیه وارونگی وابستگی (Dependency Inversion) است. ما تمام وابستگی هایی را که برای کار کردن نیاز دارد به ماژول خود منتقل می کنیم.
برای آشنایی با تزیق وابستگی در ری اکت نیز این مقاله را بررسی کنید.
چرا مورد نیاز است؟
راستی چرا Math.random
را در آرگومان قرار دهیم و از آنجا استفاده کنیم؟ استفاده از آن در داخل یک تابع چه اشکالی دارد؟ دو دلیل برای آن وجود دارد.
آزمایش پذیری
وقتی همه وابستگی ها به صراحت اعلام شده باشند، تست ماژول آسان تر است. ما می توانیم ببینیم که برای اجرای یک تست چه چیزی باید آماده شود. ما می دانیم که کدام قسمت ها بر عملکرد این ماژول تأثیر می گذارند، بنابراین می توانیم آنها را با چند پیاده سازی ساده یا پیاده سازی ماک در صورت نیاز جایگزین کنیم.
پیاده سازی های Mock تست را بسیار آسان تر می کنند و گاهی اوقات نمی توانید هیچ چیزی را بدون آنها تست کنید. همانطور که در مورد تابع getRandomInRange
، ما نمی توانیم نتیجه نهایی را که برمی گرداند آزمایش کنیم، زیرا ... تصادفی است.
/*
* We can create a mock function
* that will always return 0.1 instead of a random number:
*/
const mockRandom = () => 0.1;
/* Next, we call our function by passing the mock object as its last argument: */
const result = getRandomInRange(1, 10, mockRandom);
/*
* Now, since the algorithm within the function is known and deterministic,
* the result will always be the same:
*/
console.log(result === 1); // -> true
جایگزینی یک وابستگی با وابستگی دیگر
تعویض وابستگی ها در طول تست ها یک مورد خاص است. به طور کلی، ممکن است به دلایل دیگری بخواهیم یک ماژول را با ماژول دیگری تعویض کنیم.
اگر ماژول جدید مانند ماژول قبلی عمل می کند، می توانیم یکی را با دیگری جایگزین کنیم:
const otherRandom = (): number => {
/* Another implementation of getting a random number... */
};
const result = getRandomInRange(1, 10, otherRandom);
اما آیا می توانیم تضمین کنیم که ماژول جدید مانند ماژول قبلی رفتار خواهد کرد؟ بله، می توانیم، زیرا از نوع آرگومان عددی () =>
استفاده می کنیم. به همین دلیل است که ما از TypeScript استفاده می کنیم نه JavaScript. انواع و رابط ها پیوندهای بین ماژول ها هستند. اگر می خواهید تفاوت های بین تایپ اسکریپت و جاوااسکریپت را بهتر درک کنید این مقاله را بررسی فرمایید.
برای آشنایی با تزریق وابستگی در Node.js این مقاله را بررسی فرمایید.
وابستگی به انتزاعات
در نگاه اول، ممکن است این یک عارضه بیش از حد به نظر برسد. اما در واقع با این رویکرد:
ماژول ها کمتر به یکدیگر وابسته می شوند.
ما مجبوریم قبل از شروع نوشتن کد، رفتار را طراحی کنیم.
وقتی رفتار را از قبل طراحی می کنیم، از قراردادهای آبسترک استفاده می کنیم. تحت این قراردادها، ما ماژول ها یا آداپتورهای خودمان را برای نمونه های شخص ثالث طراحی می کنیم. این به ما امکان می دهد تا بدون نیاز به بازنویسی کامل، قسمت هایی از سیستم را جایگزین کنیم. این به ویژه زمانی مفید است که ماژول ها پیچیده تر از مثال های بالا هستند.
جاوااسکریپت چگونه کار می کند؟ آشنایی با Event Loop و Call Stack
تزریق وابستگی چیست؟
تزریق وابستگی (Dependency Injection) یک الگوی طراحی است که به ما این امکان را میدهد تا اجزا را با تزریق وابستگیهای آنها از منابع خارجی به جای ایجاد درونی آنها جدا کنیم. این رویکرد باعث تقویت اتصال شل، قابلیت استفاده مجدد و آزمایش پذیری در پایگاه کد ما می شود.
تزریق سازنده (Constructor Injection)
تزریق سازنده یکی از رایج ترین شکل های تزریق وابستگی است. این شامل تزریق وابستگی ها از طریق constructor
کلاس است. بیایید یک مثال را در نظر بگیریم:
class UserService {
constructor(private userRepository: UserRepository) {}
getUser(id: string) {
return this.userRepository.getUserById(id);
}
}
class UserRepository {
getUserById(id: string) {
// Retrieve user from the database
}
}
const userRepository = new UserRepository();
const userService = new UserService(userRepository);
در مثال بالا، کلاس UserService
به کلاس UserRepository
بستگی دارد. با عبور یک نمونه از UserRepository
از سازنده، وابستگی بین دو کلاس را ایجاد می کنیم. این رویکرد امکان مبادله آسان پیاده سازی های مختلف UserRepository
را فراهم می کند و کد ما را انعطاف پذیرتر و توسعه پذیرتر می کند.
تزریق پراپرتی (Property Injection)
یکی دیگر از رویکردهای تزریق وابستگی، تزریق پراپرتی است. در این روش وابستگی ها از طریق خصوصیات عمومی یک کلاس تزریق می شوند. بیایید یک مثال را ببینیم:
class AuthService {
private _userRepository!: UserRepository;
set userRepository(userRepository: UserRepository) {
this._userRepository = userRepository;
}
login(username: string, password: string) {
// Perform authentication using the injected UserRepository
}
}
const authService = new AuthService();
authService.userRepository = new UserRepository();
در مثال بالا، کلاس AuthService
یک userRepository
دارایی عمومی را اعلام می کند که می تواند با یک نمونه از UserRepository
تنظیم شود. این به ما اجازه می دهد تا پس از ایجاد شی AuthService
، وابستگی را تزریق کنیم. با این حال، توجه به این نکته مهم است که تزریق ویژگی می تواند وابستگی ها را در مقایسه با تزریق سازنده، کمتر قابل مشاهده و ردیابی کند.
کانتینر DI چیست؟
بیایید ببینیم که یک کانتینر تزریق وابستگی DI (یا ظرف IoC) دقیقاً چیست! فرض کنید یک کلاس User
داریم که باید با کلاس Database
کار کند. ایده اول می تواند این باشد:
class Database {
constructor(user: string, pass: string) { /* connect */ }
}
class User {
constructor() {
const database = new Database('test_user', 123456);
database.query();
}
}
const user = new User();
در این صورت، ما اصل وارونگی کنترل را زیر پا می گذاریم. این دو مسئله اساسی دارد:
هر گونه تغییر در شکل کلاس Database
بر کلاس User
تأثیر می گذارد (User
به شدت با کلاس Database
همراه است)
نوشتن یک تست واحد برای کلاس User سخت است.
چگونه رفع کنیم؟
برای رفع این مشکل، باید کلاس Database
را از بیرون پاس کنیم. بیایید ببینیم که چگونه در کد کار می کند:
class Database {
constructor(user: string, pass: string) { /* connect */ }
}
class User {
constructor(private database: Database) {}
}
const database = new Database('test_user', 123456);
const user = new User(database); // ✅
در حال حاضر، نه تنها کلاس User
در مورد تغییرات Database
تغییر نمی کند، بلکه می توانید تست های واحد را به راحتی با ارائه یک نمونه دیتابیس جعلی بنویسید.
بهترین الگوریتم های مرتب سازی در جاوااسکریپت
آیا باید از DI استفاده کنید؟
برای درک مزایای بالقوه و معاوضه استفاده از DI زمان بگذارید. شما مقداری کد زیرساختی می نویسید، اما در عوض کد شما کمتر جفت شده، منعطف تر و آزمایش آن آسان تر خواهد بود.
مزایای تزریق وابستگی
با استقبال از تزریق وابستگی، چندین مزیت را باز می کنیم که پایگاه کد ما را تا حد زیادی افزایش می دهد:
اتصال سست
تزریق وابستگی باعث تقویت جفت سست بین اجزاء می شود، زیرا آنها به انتزاعات به جای اجرای ملموس بستگی دارند. این ما را قادر میسازد تا وابستگیها را به راحتی عوض کنیم و نگهداری کد و مقیاسپذیری را تسهیل کنیم.
قابلیت استفاده مجدد
با تزریق وابستگی، ما میتوانیم اجزایی با حداقل وابستگی ایجاد کنیم، و آنها را در زمینههای مختلف بسیار قابل استفاده مجدد کنیم. با تزریق پیادهسازیهای خاصی از وابستگیها، میتوانیم رفتار یک جزء را بدون تغییر کد آن تنظیم کنیم.
آزمایش پذیری
تزریق وابستگی تست واحد را بسیار ساده می کند. با تزریق وابستگیهای ماک در طول تست، میتوانیم مؤلفهها را جدا کرده و رفتار آنها را بهطور مستقل تأیید کنیم. این منجر به مجموعه های تست قابل اعتمادتر و قابل نگهداری تر می شود.
انعطاف پذیری و توسعه پذیری
استفاده از تزریق وابستگی به ما این امکان را میدهد که بدون تغییر در پیادهسازی هسته، ویژگیهای جدید اضافه کنیم یا ویژگیهای موجود را تغییر دهیم. با تزریق وابستگیهای جدید یا اصلاح وابستگیهای موجود، میتوانیم عملکرد پایگاه کد خود را بدون ایجاد تغییرات قطعی گسترش دهیم.
نحوه نوشتن کد ساده تر در جاوااسکریپت
نتیجه
تزریق وابستگی یک تکنیک قدرتمند است که قابلیت نگهداری، تست پذیری و انعطاف پذیری کد را بهبود می بخشد. با استفاده از تزریق سازنده یا خاصیت، میتوانیم اجزای جفت شدهای را ایجاد کنیم که بسیار قابل استفاده مجدد هستند و آزمایش آن آسان است.
به عنوان توسعهدهندگان ارشد، پذیرش تزریق وابستگی در پروژههای TypeScript ما را قادر میسازد تا کدهای تمیزتر، ماژولارتر و قویتر بنویسیم. این مقیاسپذیری برنامههای ما را افزایش میدهد، همکاری مؤثر بین اعضای تیم را امکانپذیر میکند، و معرفی ویژگیها یا تغییرات جدید را ساده میکند.
بیایید به کاوش و استفاده از تزریق وابستگی در پروژههای خود ادامه دهیم تا کیفیت کد خود را بالا ببریم و نرمافزاری را ارائه دهیم که در تست زمان مقاومت کند.