تزریق وابستگی چیست؟
تزریق وابستگی (DI) یک الگوی طراحی نرم افزاری است که به ما امکان ایجاد برنامه های کاربردی و ماژولار را فراهم می کند. با استفاده از تکنیک تزریق وابستگی می توانیم پیاده سازی یک شی را از وابستگی های آن جدا کنیم. به جای اینکه یک شی وابستگی های خود را ایجاد کند، آن ها از بیرون به شی ارائه می شوند. به این ترتیب، میتوانیم وابستگی ها را بدون درهم ریختن کد شی تغییر دهیم یا تعویض کنیم. تست اشیاء به طور مستقل بسیار ساده تر است زیرا می توانیم از اشیاء ساختگی به جای واقعی استفاده کنیم.
ASP.NET Core دارای یک چارچوب تزریق وابستگی داخلی برای ارائه سرویس به اجزای مختلف برنامه است. تزریق وابستگی در همه جا برای اتصال سایر بخش های برنامه با سرویس های مورد نیاز آنها استفاده می شود. این فریمورک از یک کانتینر وارونگی کنترل (IoC) برای رسیدگی به تمام وابستگیهای بین مؤلفهها استفاده میکند.
کانتینر IOC
کانتینر IoC(Inversion of Control) چارچوبی است که ایجاد و طول عمر اشیاء را در یک برنامه مدیریت می کند و از ایجاد و ردیابی اشیاء در یک برنامه مراقبت می کند. علاوه بر این، راهی برای register و resolve وابستگی ها در صورت نیاز فراهم می کند. در ASP.NET Core، کانتینر IoC در فریمورک تعبیه شده است و می توان آن را در فایل Program.cs پیکربندی کرد.
// Program.cs
builder.Services.AddScoped<IUserService, UserService>();
در مثال بالا، ما در حال ثبت سرویسی به نام UserService هستیم که اینترفیس IUserService را پیاده سازی می کند. متد AddScoped به کانتینر می گوید که برای هر درخواست مشتری یک نمونه جدید از کلاس UserService ایجاد کند. روشهای دیگری مانند AddSingleton و AddTransient برای ثبت خدمات با طول عمر متفاوت در دسترس هستند. در ادامه به سایر گزینهها میپردازیم.
هنگامی که یک سرویس با کانتینر IoC ثبت می شود، می توان آن را با استفاده از تزریق سازنده به سایر اجزای برنامه تزریق کرد.
مزایای تزریق وابستگی
استفاده از Dependency Injection در یک برنامه ASP.NET Core مزایای متعددی دارد:
- آزمایش پذیری بهبود یافته: با استفاده از تزریق وابستگی، اشیاء به صورت loosely coupleمی شوند و تست هر شی را به صورت مجزا آسان تر می کند. این امر پیچیدگی تست ها را کاهش می دهد و آنها را قابل نگهداری تر می کند.
- جداسازی Concern ها: تزریق وابستگی امکان جداسازی ایجاد و استفاده از شی را فراهم می کند که به کاهش پیچیدگی برنامه و بهبود ماژولار بودن آن کمک می کند.
- کاهش پیچیدگی کد: تزریق وابستگی به ما اجازه می دهدکدهای تمیزتر و ماژولارتری بنویسیم. این می تواند نگهداری و به روز رسانی کد را در آینده آسان تر کند.
- قابلیت استفاده مجدد بهتر: با استفاده از Dependency Injection، اشیاء را می توان مجدداً در چندین کلاس استفاده کرد که باعث کاهش تکرار و بهبود کارایی کلی برنامه می شود.
تزریق وابستگی از طریق سازنده یا Constructor Injection
Constructor Injection تکنیکی است که در آن وابستگی ها از طریق سازنده آن به یک کلاس ارائه می شود. این کار دیدن وابستگی های یک کلاس را آسان می کند و به کانتینر IoC اجازه می دهد تا وابستگی های مورد نیاز را به صورت خودکار ایجاد و تزریق کند.
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
}
در مثال بالا، کلاس UserController به اینترفیس IUserService وابستگی دارد. وابستگی از طریق سازنده تزریق می شود و کانتینر IoC به طور خودکار وابستگی مورد نیاز را ایجاد و تزریق می کند.
طول عمر وابستگی
در زمان ثبت سرویس، وابستگی ها نیاز به تعریف طول عمر دارند. طول عمر سرویس، شرایطی را که تحت آن یک نمونه جدید از سرویس ایجاد می شود را مشخص می کند. در زیر طول عمر تعریف شده توسط چارچوب .NET Core DI آمده است.
Singleton
متد AddSingleton یک سرویس را از طریق کانتینر تزریق وابستگی به صورت Singleton ثبت می کند. Singleton یک شی است که فقط یک بار ایجاد می شود و همان نمونه در کل برنامه به اشتراک گذاشته می شود. این به این معنی است که هر مؤلفه ای که به سرویس سینگلتون ثبت شده وابسته باشد، همان نمونه سرویس را دریافت خواهد کرد.
builder.Services.AddSingleton<IEmailService, EmailService>();
در کد بالا، ما در حال ثبت سرویسی به نام EmailService هستیم که رابط IEmailService را پیاده سازی می کند. متد AddSingleton به کانتینر می گوید که فقط یک نمونه از کلاس EmailService ایجاد کند و آن را در کل برنامه به اشتراک بگذارد.
Singleton زمانی مفید است که می خواهیم داده ها یا منابع را در اجزای مختلف یک برنامه به اشتراک بگذاریم. در مثال بالا، ما یک EmailService داریم که میخواهیم آن را در بخشهای مختلف برنامه خود به اشتراک بگذاریم. با ثبت EmailService بهعنوان Singleton ، میتوانیم اطمینان حاصل کنیم که همه مؤلفههایی که نیاز به ارسال ایمیل دارند، از همان نمونه سرویس استفاده میکنند.
توجه به این نکته مهم است که سرویس singleton هنگام شروع برنامه ایجاد می شود و تا زمان خاموش شدن از بین نمی رود. این به این معنی است که هر منبعی که توسط سرویس singleton استفاده میشود در طول عمر برنامه در حافظه نگهداری میشود. بنابراین، هنگام استفاده از singleton ها باید مراقب باشیم تا از memory leak یا مشکلات عملکردی جلوگیری کنیم.
ما باید از روش AddSingleton به اندازه کافی و فقط برای سرویس هایی استفاده کنیم که باید در کل برنامه به اشتراک گذاشته شوند. برای سرویسهای دیگر، مانند سرویسهایی که مخصوص یک درخواست خاص هستند، باید به جای آن از روشهای AddScoped یا AddTransient استفاده کنیم.
Scoped
متد AddScoped برای ثبت یک سرویس از طریق کانتینر تزریق وابستگی به عنوان یک سرویس محدوده استفاده می شود. یک سرویس scoped یک بار در هر درخواست کلاینت ایجاد می شود و در آن درخواست به اشتراک گذاشته می شود. این به این معنی است که هر مؤلفه ای که به سرویس scoped ثبت شده وابستگی دارد، همان نمونه سرویس را در یک درخواست کلاینت دریافت می کند.
builder.Services.AddScoped<IUserService, UserService>();
در کد بالا، سرویسی به نام UserService ثبت می کنیم که اینترفیس IUserService را پیاده سازی می کند. متد AddScoped به کانتینر می گوید که برای هر درخواست کلاینت یک نمونه از کلاس UserService ایجاد کند. این تضمین می کند که هر درخواست کلاینت نمونه ای از سرویس را دارد.
سرویس های Scoped هنگام به اشتراک گذاری داده ها یا منابع در یک درخواست کلاینت مفید هستند. به عنوان مثال، ما یک کانتکست پایگاه داده داریم که می خواهیم برای یک درخواست کلاینت استفاده کنیم. با ثبت کانتکست پایگاه داده به عنوان یک سرویس scoped ، میتوانیم اطمینان حاصل کنیم که همه مؤلفههایی که از کانتکست در یک درخواست کلاینت استفاده میکنند، از نمونه مشابهی از کانتکست استفاده میکنند.
builder.Services.AddDbContext<DataContext>(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DataContext")));
در مثال بالا، Entity Framework DbContext به طور پیش فرض به عنوان یک سرویس scoped ثبت شده است.
توجه به این نکته مهم است که سرویس های scoped در پایان هر درخواست کلاینت حذف می شوند. این بدان معنی است که هر منبعی که توسط سرویس scoped استفاده می شود در پایان هر درخواست کلاینت آزاد می شود که این موضوع از memory leak جلوگیری و باعث بهبود عملکرد می شود.
ما باید از متد AddScoped برای سرویس هایی استفاده کنیم که باید در یک درخواست کلاینت به اشتراک گذاشته شوند.
Transient
متد AddTransient یک سرویس را از طریق کانتینر تزریق وابستگی به عنوان یک سرویس Transientثبت می کند. یک سرویس Transienهر زمان که درخواست شود ایجاد می شود و بین اجزا یا درخواست های مختلف به اشتراک گذاشته نمی شود. این بدان معنی است که هر مؤلفه ای که به سرویس Transient ثبت شده وابسته است، هر بار که درخواست شود، نمونه جدیدی از سرویس را دریافت می کند.
builder.Services.AddTransient<IPasswordGeneratorService, PasswordGeneratorService>();
در کد بالا، سرویسی به نام PasswordGeneratorService ثبت می کنیم که اینترفیس IPasswordGeneratorService را پیاده سازی می کند. متد AddTransient به کانتینر می گوید هر زمان که درخواست شد نمونه جدیدی از کلاس PasswordGeneratorService را ایجاد کند.
سرویسهای Transient زمانی که میخواهیم در هر لحظه یک نمونه جدید از یک سرویس را ایجاد کنیم مفید هستند. در مثال بالا، ما سرویسی داریم که رمزهای عبور تصادفی تولید می کند. با ثبت PasswordGeneratorService به عنوان یک سرویس Transient ، می توانیم اطمینان حاصل کنیم که هر بار که سرویس درخواست می شود، یک نمونه جدید از آن ایجاد می شود.
ما باید از متد AddTransient برای سرویس هایی استفاده کنیم که هر زمان که درخواست می شود باید ایجاد شوند. برای سرویس هایی که باید در یک درخواست کلاینت به اشتراک گذاشته شوند، باید از متد AddScoped و برای سرویس هایی که باید در کل برنامه به اشتراک گذاشته شوند، باید از روش AddSingleton استفاده کنیم.
تصمیم گیری برای انتخاب طول عمر وابستگی
تصمیم به استفاده از AddSingleton، AddTransient یا AddScoped برای ثبت سرویس با کانتینر تزریق وابستگی به طول عمر سرویس و نحوه استفاده از آن در برنامه ما بستگی دارد. در اینجا چند دستورالعمل وجود دارد که به ما کمک می کند تصمیم بگیریم از کدام روش استفاده کنیم:
- زمانی که می خواهیم یک نمونه از یک سرویس را ایجاد کنیم و آن را در کل برنامه به اشتراک بگذاریم، از AddSingleton استفاده کنیم. این برای سرویس هایی مفید است که در طول عمر برنامه تغییر نمی کنند و می توانند توسط اجزای مختلف دوباره استفاده شوند.
- زمانی که میخواهیم نمونهای از سرویسی ایجاد کنیم که در یک درخواست کلاینت به اشتراک گذاشته شده است، از AddScoped استفاده کنید. این برای سرویس هایی که نیاز به حفظ وضعیت یا داده در حین رسیدگی به یک درخواست HTTP دارند مفید است. به طور معمول، AddScoped بیشترین طول عمر سرویس در برنامه های ASP .NET Core است. DbContext، ریپازیتوری ها و سرویس های مرتبط با پایگاه داده باید با استفاده از طول عمر scoped ثبت شوند.
- زمانی که میخواهیم نمونه جدیدی از یک سرویس را درهر زمان درخواست کنیم، از AddTransient استفاده می کنیم. این برای سرویس هایی که عملیات سبک و بدون حالت را انجام می دهند مفید است.
نباید ها در تزریق وابستگی:
چندین کار وجود دارد که برای جلوگیری از خطاهای مربوط به تزریق وابستگی نباید انجام دهیم:
❌ تزریق سرویس scoped به سرویس singleton
تزریق یک سرویس scoped به یک کلاس singleton میتواند منجر به خطاهای runtime شود، مانند یک InvalidOperationException با پیامی که میگوید: « Cannot consume scoped service from singleton».
builder.Services.AddSingleton<ISingletonService, SingletonService>();
builder.Services.AddScoped<IScopedService, ScopedService>();
public class SingletonService : ISingletonService
{
public SingletonService(IScopedService scopedService)
{
}
}
کد مثال بالا منجر به خطای runtime می شود. این خطا به این دلیل رخ می دهد که یک سرویس scoped یک بار در هر درخواست ایجاد می شود در حالی که یک سرویس singleton یک بار در طول عمر برنامه ایجاد می شود. بنابراین، اگر ما یک سرویس scoped را به یک سرویس singleton تزریق کنیم، سرویس Scoped از سرویس singleton بیشتر خواهد ماند و ممکن است قبل از اتمام سرویس singleton باطل شود یا از بین برود.
❌ ایجاد وابستگی های circular
وابستگی های circular زمانی رخ می دهد که دو یا چند کلاس به طور مستقیم یا غیرمستقیم به یکدیگر وابسته باشند. به مثال زیر توجه کنید:
public class ClassA
{
public ClassA(ClassB b)
{
}
}
public class ClassB
{
public ClassB(ClassC c)
{
}
}
public class ClassC
{
public ClassC(ClassA a)
{
}
}
در کد بالا، ClassA به ClassB بستگی دارد که به ClassC بستگی دارد و ClassC به ClassA بستگی دارد. این یک وابستگی circular یا دایره ای بین کلاس ها ایجاد می کند. وابستگی circular میتواند باعث ایجاد خطای سرریز پشته(stack overflow) یا یک حلقه بینهایت همانطور که کلاسها یکدیگر را به صورت بازگشتی فراخوانی میکنند، شود. برای جلوگیری از این خطا، مهم است که کلاس های خود را به گونه ای طراحی کنیم که از وابستگی های دایره ای جلوگیری شود.
نتیجه گیری:
تزریق وابستگی یک تکنیک قدرتمند است که به مدیریت وابستگی بین اجزای مختلف یک برنامه کمک می کند. در ASP.NET Core، تزریق وابستگی در فریمورک تعبیه شده است و به طور گسترده برای ارائه خدمات به اجزای مختلف برنامه استفاده می شود. کانتینر IoC وابستگی های بین اجزا را مدیریت می کند و از تزریق سازنده برای تزریق وابستگی ها به کلاس ها استفاده می شود. با استفاده از تزریق وابستگی، میتوانیم برنامههای قابل تست و نگهداری بیشتری ایجاد کنیم که اصلاح و گسترش آن آسان تر است.
برای درک تفاوت بین طول عمرهای وابستگی و تفاوت های آن به صورت عملی، می توانیدآموزش بررسی طول عمر سرویس ها در تزریق وابستگی را به صورت رایگان از کدسل دانلود کنید.
برای ثبت نظر باید در سایت ثبت نام یا ورود نمایید