Dependency Injection چیست و چرا در برنامهنویسی مهم است؟
یکی از سؤالهایی که هنگام یادگیری برنامهنویسی و مخصوصاً کار با پروژههای بزرگتر ممکن است با آن روبهرو شوید این است:
Dependency Injection چیست و چرا برنامهنویسان از آن استفاده میکنند؟
فرض کنید در حال ساخت یک فروشگاه اینترنتی هستید. برنامه شما برای نمایش محصولات به پایگاه داده نیاز دارد، برای ارسال پیامک به یک سرویس پیامکی و برای پرداخت سفارشها به یک درگاه پرداخت متصل میشود.
حالا تصور کنید هر کلاس در برنامه خودش این سرویسها را ایجاد کند.
در ابتدا شاید مشکلی به وجود نیاید، اما هرچه پروژه بزرگتر شود، وابستگی بین بخشهای مختلف برنامه بیشتر و مدیریت کدها سختتر میشود.
اینجاست که Dependency Injection یا تزریق وابستگی وارد ماجرا میشود.
Dependency Injection یکی از مفاهیم مهم در طراحی نرمافزار و یکی از تکنیکهای بسیار پرکاربرد در فریمورکهایی مانند ASP.NET Core است.
اما Dependency Injection دقیقاً چیست و چه مشکلی را حل میکند؟
Dependency Injection چیست؟
Dependency Injection (DI) یا تزریق وابستگی روشی برای مدیریت وابستگیهای یک کلاس است.
به زبان ساده، اگر یک کلاس برای انجام وظیفه خود به کلاس یا سرویس دیگری نیاز داشته باشد، بهجای اینکه خودش آن سرویس را بسازد، آن را از بیرون دریافت میکند.
برای مثال فرض کنید یک کلاس به پایگاه داده نیاز دارد.
روش ساده این است که کلاس خودش Database را ایجاد کند:
public class UserService
{
private Database _database;
public UserService()
{
_database = new Database();
}
}
در این حالت، UserService خودش تصمیم گرفته است که از چه Databaseای استفاده کند.
اما در Dependency Injection، وابستگی را از بیرون دریافت میکنیم:
public class UserService
{
private readonly Database _database;
public UserService(Database database)
{
_database = database;
}
}
حالا UserService دیگر مسئول ساختن Database نیست.
یک بخش دیگر از برنامه، وابستگی موردنیاز را ایجاد میکند و در اختیار UserService قرار میدهد.
یک مثال ساده؛ رستوران
برای درک بهتر Dependency Injection، یک رستوران را تصور کنید.
فرض کنید یک آشپز برای پخت غذا به مواد اولیه نیاز دارد.
آیا منطقی است که آشپز خودش از مزرعه خرید کند، کامیون تهیه کند، مواد اولیه را حمل کند و بعد شروع به آشپزی کند؟
احتمالاً نه!
مسئولیت آشپز، پخت غذا است.
مواد اولیه از جای دیگری تأمین میشوند و آشپز فقط آنها را دریافت میکند و از آنها استفاده میکند.
در برنامهنویسی نیز تقریباً همین اتفاق میافتد.
یک کلاس باید روی وظیفه اصلی خودش تمرکز کند، نه اینکه مسئول ساخت تمام چیزهایی باشد که برای انجام وظیفهاش نیاز دارد.
Dependency Injection کمک میکند این مسئولیتها از یکدیگر جدا شوند.
Dependency چیست؟
قبل از اینکه مفهوم Dependency Injection را بهتر بفهمیم، ابتدا باید بدانیم Dependency یعنی چه.
Dependency به معنی وابستگی است.
فرض کنید کلاس OrderService برای ثبت سفارش به PaymentService نیاز دارد.
در این حالت میگوییم:
OrderService
↓
PaymentService
یعنی OrderService به PaymentService وابسته است.
اگر OrderService بدون PaymentService نتواند کار خود را انجام دهد، PaymentService یک Dependency برای آن محسوب میشود.
حالا اگر این وابستگی از بیرون به OrderService داده شود، اتفاقی که افتاده است Dependency Injection نام دارد.
چرا Dependency Injection به وجود آمد؟
در پروژههای کوچک، ممکن است ایجاد وابستگیها داخل کلاسها مشکل خاصی ایجاد نکند.
اما پروژه را کمی بزرگتر تصور کنید.
یک فروشگاه آنلاین ممکن است سرویسهایی مانند موارد زیر داشته باشد:
• سرویس کاربران
• سرویس سفارشها
• سرویس پرداخت
• سرویس ارسال پیامک
• سرویس ایمیل
• سرویس محصولات
• سرویس تخفیف
• سرویس ثبت لاگ
• سرویس احراز هویت
اگر هر کلاس خودش این سرویسها را ایجاد کند، وابستگیها بهشدت زیاد میشوند.
مثلاً:
public class OrderService
{
private PaymentService _payment;
private SmsService _sms;
private EmailService _email;
public OrderService()
{
_payment = new PaymentService();
_sms = new SmsService();
_email = new EmailService();
}
}
حالا اگر بخواهیم روش پرداخت یا سرویس ارسال پیامک را تغییر دهیم، ممکن است مجبور شویم بخشهای مختلف برنامه را تغییر دهیم.
Dependency Injection برای حل همین مشکل بسیار مفید است.
Dependency Injection چگونه کار میکند؟
ایده اصلی بسیار ساده است:
کلاس بهجای ساختن وابستگی، آن را دریافت میکند.
مثلاً:
public class OrderService
{ private readonly IPaymentService _paymentService;
public OrderService(IPaymentService paymentService)
{
_paymentService = paymentService;
}
}
در اینجا OrderService خودش PaymentService را ایجاد نکرده است.
بلکه فقط گفته:
من برای انجام کارم به چیزی نیاز دارم که IPaymentService باشد.
این وابستگی از بیرون به کلاس تزریق میشود.
یک مثال واقعیتر در C#
فرض کنید در یک فروشگاه اینترنتی میخواهیم برای مشتری پیام ارسال کنیم.
میتوانیم یک Interface ایجاد کنیم:
public interface IMessageService
{
void Send(string message);
}
حالا میتوانیم یک سرویس برای ارسال پیامک داشته باشیم:
public class SmsService : IMessageService
{
public void Send(string message)
{
Console.WriteLine("SMS sent: " + message);
}
}
سپس سرویس سفارش را ایجاد میکنیم:
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(IMessageService messageService)
{
_messageService = messageService;
}
public void CreateOrder()
{
// ثبت سفارش
_messageService.Send("سفارش شما با موفقیت ثبت شد.");
}
}
نکته مهم اینجاست که OrderService نمیداند پیام دقیقاً چگونه ارسال میشود.
برای آن مهم نیست که پیام با SMS ارسال میشود، ایمیل است یا حتی یک Notification داخل اپلیکیشن.
فقط میداند چیزی به نام IMessageService دارد که میتواند پیام ارسال کند.
این یعنی کاهش وابستگی بین کلاسها.
Dependency Injection در ASP.NET Core
یکی از دلایلی که یادگیری Dependency Injection برای برنامهنویسان .NET اهمیت زیادی دارد، استفاده گسترده از آن در ASP.NET Core است.
ASP.NET Core یک سیستم داخلی برای مدیریت Dependency Injection دارد.
برای مثال میتوانیم سرویس خود را در برنامه ثبت کنیم:
builder.Services.AddScoped<IMessageService, SmsService>();
بعد هر جا که به IMessageService نیاز داشته باشیم، آن را در Constructor درخواست میکنیم:
public class OrderController : ControllerBase
{
private readonly IMessageService _messageService;
public OrderController(IMessageService messageService)
{
_messageService = messageService;
}
}
ASP.NET Core خودش SmsService مناسب را پیدا میکند و در اختیار OrderController قرار میدهد.
یعنی برنامهنویس لازم نیست در هر قسمت برنامه بنویسد:
new SmsService()
فریمورک مدیریت این وابستگی را بر عهده میگیرد.
سه روش رایج Dependency Injection
Dependency Injection را میتوان به روشهای مختلفی انجام داد، اما سه روش بیشتر شناخته شدهاند.
۱. Constructor Injection
رایجترین روش، مخصوصاً در ASP.NET Core، استفاده از Constructor است.
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
}
در این روش Dependency از طریق Constructor وارد کلاس میشود.
این روش معمولاً انتخاب مناسبی است، چون مشخص میکند کلاس برای انجام وظیفه خود دقیقاً به چه وابستگیهایی نیاز دارد.
۲. Property Injection
در این روش Dependency از طریق یک Property در اختیار کلاس قرار میگیرد.
برای مثال:
public class ProductService
{
public ILogger Logger { get; set; }
}
این روش در برخی شرایط کاربرد دارد، اما در بسیاری از پروژهها Constructor Injection انتخاب رایجتر و شفافتری است.
۳. Method Injection
در این روش Dependency مستقیماً به یک متد داده میشود:
public void ProcessOrder(IPaymentService paymentService)
{
paymentService.Pay();
}
یعنی فقط همان متدی که به Dependency نیاز دارد، آن را دریافت میکند.
Service Lifetime چیست؟
اگر با Dependency Injection در ASP.NET Core کار کنید، احتمالاً با مفهومی به نام Service Lifetime نیز روبهرو خواهید شد.
وقتی یک سرویس را در سیستم Dependency Injection ثبت میکنیم، باید مشخص کنیم این سرویس چه مدت و با چه محدودهای نگهداری شود.
سه حالت رایج عبارتاند از:
Transient
هر بار که سرویس درخواست شود، یک نمونه جدید ساخته میشود.
builder.Services.AddTransient<IMessageService, SmsService>();
برای سرویسهای سبک و بدون State معمولاً میتواند مناسب باشد.
Scoped
در هر Scope یک نمونه ایجاد میشود.
در برنامههای وب، معمولاً Scope مربوط به یک درخواست HTTP است.
builder.Services.AddScoped<IOrderService, OrderService>();
این نوع Lifetime در پروژههای ASP.NET Core بسیار رایج است.
Singletonیک نمونه از سرویس ایجاد میشود و همان نمونه در طول عمر برنامه مورد استفاده قرار میگیرد.
builder.Services.AddSingleton<ICacheService, CacheService>();
Singleton باید با دقت استفاده شود، مخصوصاً زمانی که سرویس دارای State یا وابستگیهای دیگر است.
Dependency Injection چه ارتباطی با Unit Test دارد؟
یکی از مهمترین مزایای Dependency Injection، تستپذیری بهتر کد است.
فرض کنید OrderService مستقیماً یک سرویس پرداخت واقعی ایجاد کند:
_payment = new PaymentService();
حالا برای تست کردن OrderService ممکن است واقعاً با سرویس پرداخت ارتباط برقرار شود؛ چیزی که در یک Unit Test اصلاً مناسب نیست.
اما با Dependency Injection میتوانیم یک سرویس آزمایشی به آن بدهیم.
مثلاً:
var fakePayment = new FakePaymentService();
var orderService = new OrderService(fakePayment);
در نتیجه میتوانیم رفتار OrderService را بدون اتصال به سیستم واقعی پرداخت بررسی کنیم.
این ویژگی در پروژههای بزرگ اهمیت بسیار زیادی دارد.
Dependency Injection چه مزایایی دارد؟
استفاده درست از Dependency Injection میتواند مزایای مهمی برای پروژه داشته باشد:
کاهش وابستگی بین کلاسها
کلاسها کمتر به پیادهسازیهای مشخص وابسته میشوند.
تستپذیری بهتر
میتوان وابستگیهای واقعی را با نمونههای آزمایشی جایگزین کرد.
نگهداری آسانتر کد
تغییر یک سرویس، معمولاً نیازمند تغییر بخشهای زیادی از برنامه نیست.
انعطافپذیری بیشتر
میتوان پیادهسازیهای مختلفی برای یک Interface ایجاد کرد.
رعایت اصل Single Responsibility
هر کلاس میتواند روی وظیفه اصلی خودش تمرکز کند و مسئول ساخت وابستگیهایش نباشد.
مدیریت بهتر پروژههای بزرگ
با افزایش تعداد سرویسها و کلاسها، مدیریت وابستگیها منظمتر میشود.
یک مثال جذاب؛ درگاه پرداخت
فرض کنید فروشگاه اینترنتی شما ابتدا فقط از یک درگاه پرداخت استفاده میکند.
مثلاً:
OrderService
↓
ZarinPalPayment
اما چند ماه بعد تصمیم میگیرید درگاه دیگری هم اضافه کنید.
اگر OrderService مستقیماً به درگاه اول وابسته باشد، تغییر دادن سیستم پرداخت میتواند دشوار شود.
اما اگر از Interface استفاده کنیم:
public interface IPaymentService
{
Task<bool> PayAsync(decimal amount);
}
میتوانیم پیادهسازیهای مختلفی داشته باشیم:
IPaymentService
/ \
/ \
PaymentGateway A PaymentGateway B
حالا OrderService فقط با IPaymentService کار میکند.
در نتیجه میتوانیم پیادهسازی مناسب را از طریق Dependency Injection در اختیار آن قرار دهیم.
این یکی از کاربردهای جذاب DI در پروژههای واقعی است.
آیا Dependency Injection همان Factory است؟
خیر.
این دو مفهوم با یکدیگر ارتباط دارند، اما یکی نیستند.
Factory Pattern معمولاً مسئول ایجاد اشیا است.
در حالی که Dependency Injection روی نحوه تأمین و مدیریت وابستگیهای موردنیاز یک کلاس تمرکز دارد.
در پروژههای واقعی حتی ممکن است Factory و Dependency Injection در کنار یکدیگر استفاده شوند.
آیا Dependency Injection فقط در C# استفاده میشود؟
خیر.
Dependency Injection یک مفهوم عمومی در طراحی نرمافزار است و محدود به C# یا ASP.NET Core نیست.
این مفهوم در فناوریها و زبانهای مختلف استفاده میشود؛ از جمله:
• C# و .NET
• Java و Spring
• PHP و Laravel
• TypeScript و NestJS
• Python
• Kotlin
ممکن است نحوه پیادهسازی در هر فریمورک متفاوت باشد، اما ایده اصلی همان است:
وابستگیها بهجای ساخته شدن داخل کلاس، از بیرون در اختیار آن قرار میگیرند.
آیا همیشه باید از Dependency Injection استفاده کنیم؟
نه.
Dependency Injection ابزار بسیار مفیدی است، اما قرار نیست در هر پروژه و برای هر کلاس به شکل پیچیده استفاده شود.
در یک برنامه بسیار کوچک، ایجاد چند کلاس ساده با new ممکن است کاملاً منطقی باشد.
اما زمانی که پروژه بزرگتر میشود و تعداد سرویسها، Repositoryها، APIها و وابستگیها افزایش پیدا میکند، مدیریت این وابستگیها اهمیت بیشتری پیدا میکند.
بنابراین هدف این نیست که همیشه و در همه جا از DI استفاده کنیم؛ هدف این است که بدانیم چه زمانی استفاده از آن باعث بهتر شدن معماری پروژه میشود.
اگر تازه برنامهنویسی را شروع کردهاید…
اگر مفهوم Dependency Injection در نگاه اول کمی پیچیده به نظر میرسد، طبیعی است.
برای درک کامل آن بهتر است ابتدا مفاهیمی مانند موارد زیر را یاد بگیرید:
• Class و Object
• Interface
• Constructor
• Encapsulation
• SOLID Principles
• Design Patterns
بعد از یادگیری این مفاهیم، DI بسیار قابل فهمتر خواهد شد.در واقع اگر بخواهیم Dependency Injection را در یک جمله خلاصه کنیم:
یک کلاس نباید همیشه خودش چیزهایی را که به آنها نیاز دارد بسازد؛ میتواند آنها را از بیرون دریافت کند.
همین ایده ساده، در پروژههای بزرگ میتواند تفاوت زیادی در کیفیت و ساختار کد ایجاد کند.
مهمترین نکات Dependency Injection
اگر بخواهیم تمام مقاله را خیلی خلاصه کنیم، کافی است این نکات را به خاطر بسپارید:
• Dependency یعنی وابستگی یک کلاس به کلاس یا سرویس دیگر.
• Injection یعنی این وابستگی از بیرون در اختیار کلاس قرار بگیرد.
• DI باعث کاهش وابستگی مستقیم بین کلاسها میشود.
• Constructor Injection یکی از رایجترین روشهای استفاده از DI است.
• ASP.NET Core دارای سیستم داخلی Dependency Injection است.
• DI میتواند تستپذیری و نگهداری پروژه را بهتر کند.
• Dependency Injection بیشتر یک مفهوم معماری است تا یک قابلیت محدود به یک زبان خاص.
• استفاده از DI باید متناسب با نیاز و اندازه پروژه باشد.
جمعبندی
Dependency Injection یا تزریق وابستگی روشی برای مدیریت وابستگیهای یک برنامه است که در آن یک کلاس بهجای اینکه وابستگیهای موردنیاز خود را مستقیماً ایجاد کند، آنها را از بیرون دریافت میکند.
این کار باعث میشود کلاسها وابستگی کمتری به یکدیگر داشته باشند و بتوانیم کدهایی منظمتر، قابل تستتر، انعطافپذیرتر و قابل نگهداریتر ایجاد کنیم.
Dependency Injection در پروژههای مدرن، مخصوصاً در ASP.NET Core، اهمیت زیادی دارد و یکی از مفاهیمی است که هر برنامهنویس حرفهای بهتر است با آن آشنا باشد.
اگر قصد دارید وارد دنیای توسعه نرمافزار و برنامهنویسی حرفهای شوید، یادگیری Dependency Injection در کنار مفاهیمی مانند SOLID، Repository Pattern، Unit of Work و Clean Architecture میتواند دید بهتری نسبت به طراحی و معماری پروژهها به شما بدهد.
فرایانه؛ جایی که مفاهیم پیچیده برنامهنویسی را به زبان ساده، کاربردی و قابل فهم آموزش میدهیم. 💚