استفاده از متدهای TryAdd()
به کد زیر نگاه کنید :
services.AddScoped<IMessageServiceB, MessageServiceBA>();
services.AddScoped<IMessageServiceB, MessageServiceBB>();
همانطور که میبینید، در
اینجا یک اینترفیس را دوبار ثبت کردیم. در این حالت موقع واکشی سرویس،
DI Container
آخرین نمونهی ثبت شدهی برای اینترفیس را واکشی کرده و نمونه سازی میکند و به کلاسها تزریق میکند. این یکی از مواردی است که ترتیب ثبت کردن سرویسهای مهم است. برای جلوگیری از این خطا میتوانیم از متدهای TryAddSingleton() ، TryAddScoped() و TryAddTransient() استفاده کنیم. این متدها درون فضای نام Microsoft.Extionsion.DependencyInjection.Extension قرار دارند.
عملکرد کلی این
متدها درست مثل متدهای Add() است؛ با این تفاوت که این متد ابتدا IServiceCollection را جستجو میکند و اگر برای type مورد نظر سرویسی ثبت نشده بود،
آن را ثبت میکند:
services.TryAddScoped<IMessageServiceB, MessageServiceBA>();
services.TryAddScoped<IMessageServiceB, MessageServiceBB>();
جایگذاری یک سرویس
با نمونهای دیگر
گاهی اوقات میخواهیم یک پیاده سازی دیگر را بجای پیاده سازی فعلی، در DI Container ثبت کنیم. در
این حالت از متد Replace() بر روی IServiceCollection برای این کار استفاده میکنیم. این متد فقط یک ServiceDescriptor را به عنوان پارامتر ورودی میگیرد:
services.Replace(serviceDescriptor3);
ناگفته نماند که متد
Replace()
فقط اولین سرویس را با نمونهی مورد نظر ما جایگذاری میکند. اگر میخواهید تمام
نمونه سرویسهای ثبت شده را برای یک نوع حذف کنید، میتوانید از متد RemoveAll()
استفاده کنید: services.RemoveAll<IMessageServiceB>();
معمولا در پروژههای معمول خودمان نیازی به استفاده از Replace() و RemoveAll() نداریم؛ مگر اینکه بخواهیم پیاده سازی اختصاصی خودمان را برای سرویسهای درونی فریم ورک یا کتابخانههای شخص
ثالث، بجای پیاده سازی پیش فرض، ثبت و استفاده کنیم.
AddEnumerable()
فرض کنید دارید
برنامهی نوبت دهی یک کلینیک را مینویسید و به صورت پیش فرض از شما خواستهاند که
هنگام صدور نوبت، این قوانین را بررسی کنید:
- هر شخص در هفته نتواند بیش
از 2 نوبت برای یک تخصص بگیرد.
- اگر شخص در ماه بیش از 3
نوبت رزرو شده داشته باشد ولی مراجعه نکرده باشد، تا پایان ماه، امکان رزرو نوبت
را نداشته باشد .
- تعداد نوبتهای ثبت شدهی برای پزشک در آن روز نباید بیش از تعدادی باشد که پزشک پذیرش میکند.
- و ...
یک روش معمول برای
پیاده سازی این قابلیت، ساخت سرویسی برای
ثبت نوبت است که درون آن متدی برای بررسی کردن قوانین ثبت نام وجود دارد. خب، ما
این کار را انجام میدهیم. تستهای واحد و تستهای جامع را هم مینویسیم و بعد
برنامه را انتشار میدهیم و همه چیز خوب است؛ تا اینکه مالک محصول یک نیازمندی جدید
را میخواهد که در آن ما باید قانون زیر را در هنگام ثبت نوبت بررسی کنیم:
- نوبتهای ثبت شده برای یک
شخص نباید دارای تداخل باشند.
در این حالت ما
باید دوباره سرویس Register را باز کنیم و به متد بررسی کردن قوانین برویم و دوباره کدهایی را برای بررسی کردن قانون جدید بنویسیم و احتمالا کد ما به این صورت خواهد شد:
public class RegisterAppointmentService : RegisterAppointmentService
{
public Task<Result> RegisterAsync(
PatientInfoDTO patientIfno , DateTimeOffset requestedDateTime ,
PhysicianId phusicianId )
{
CheckRegisterantionRule(patientInfo);
// code here
}
private Task CheckRegisterationRule(PatientInfoDTO patientInfo)
{
CheckRule1(patientInfo);
CheckRule2(patientInfo);
CheckRule3(patientInfo);
}
}
در این حالت باید
به ازای هر قانون جدید، به متد CheckRegisterationRule برویم و به ازای هر قانون، یک متد private
جدید را بسازیم. مشکل این روش این است که در این حالت ما مجبوریم با هر کم و زیاد
شدن قانون، این کلاس را باز کنیم و آن را تغییر دهیم و با هر تغییر دوباره، تستهای واحد آن را دوباره نویسی کنیم. در یک کلام در کد بالا اصول Separation of Concern
و Open/Closed Principle را رعایت نمیشود.
یک راهکار این است که یک
سرویس جداگانه را برای بررسی کردن قوانین بنویسیم و آن را به سرویس ثبت نوبت تزریق کنیم:
public class ICheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService
{
public Task CheckRegisterantionRule(PatientInfoDTO patientInfo)
{
CheckRule1(patientInfo);
CheckRule2(patientInfo);
CheckRule3(patientInfo);
}
}
public class RegisterAppointmentService : IRegisterAppointmentService
{
private ICheckRegisterationRuleForAppointmentService _ruleChecker;
public RegisterAppointmentService (RegisterAppointmentService ruleChecker)
{
_ruleChecker = ruleChecker;
}
public Task<Result> RegisterAsync(
PatientInfoDTO patientIfno ,
DateTimeOffset requestedDateTime ,
PhysicianId phusicianId )
{
_ruleChecker.CheckRegisterantionRule(patientInfo);
// code here
}
}
با این کار وظیفهی چک کردن قوانین و وظیفهی ثبت و ذخیره سازی قوانین را از یکدیگر جدا کردیم؛ ولی همچنان
در سرویس بررسی کردن قوانین، اصل Open/Closed رعایت نشدهاست. خب راه حل چیست !؟
یکی از راه حلهای موجود، استفاده از الگوی قوانین یا Rule Pattern است. برای اجرای این الگو، میتوانیم با تعریف یک اینترفیس کلی
برای بررسی کردن قانون، به ازای هر قانون یک پیاده سازی اختصاصی را داشته باشیم:
interface IAppointmentRegisterationRule
{
Task CheckRule(PatientInfo patientIfno);
}
public class AppointmentRegisterationRule1 : IAppointmentRegisterationRule
{
public Task CheckRule(PatientInfo patientIfno)
{
Console.WriteLine("Rule 1 is checked");
return Task.CompletedTask;
}
}
public class AppointmentRegisterationRule2 : IAppointmentRegisterationRule
{
public Task CheckRule(PatientInfo patientIfno)
{
Console.WriteLine("Rule 2 is checked");
return Task.CompletedTask;
}
}
public class AppointmentRegisterationRule3 : IAppointmentRegisterationRule
{
public Task CheckRule(PatientInfo patientIfno)
{
Console.WriteLine("Rule 3 is checked");
return Task.CompletedTask;
}
}
public class AppointmentRegisterationRule4 : IAppointmentRegisterationRule
{
public Task CheckRule(PatientInfo patientIfno)
{
Console.WriteLine("Rule 4 is checked");
return Task.CompletedTask;
}
}
حالا که ما قوانین
خودمان را تعریف کردیم، به روش زیر میتوانیم آنها را درون سازنده ثبت کنیم:
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule1>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule2>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule3>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule4>();
حالا میتوانیم درون
سازندهی سرویس مورد نظرمان، لیستی از سرویسهای ثبت شدهی برای یک نوع خاص را به با
استفاده از اینترفیس جنریک
IEnumerable<T> دریافت کنیم که در اینجا T،
برابر نوع سرویس مورد نظرمان است: public class CheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService
{
private IEnumerable<IAppointmentRegisterationRule> _rules ;
public CheckRegisterationRuleForAppointmentService(IEnumerable<IAppointmentRegisterationRule> rules)
{
_rules = rules;
}
public Task CheckRegisterantionRule(PatientInfoDTO patientInfo)
{
foreach(var rule in rules)
{
rule.CheckRule(patientInfo);
}
}
}
با این تغییرات، هر زمانیکه
خواستیم میتوانیم با استفاده از
DI Container،
قوانین جدیدی را اضافه یا کم کنیم و با این کار، اصل Open/Closed را نیز رعایت
کردهایم. کد بالا به
نظر کامل میآید ولی مشکلی دارد! اگر در DI Container برای IAppointmentRegisterationRule یک قانون را دو یا چند بار ثبت کنیم، در هر بار بررسی کردن قوانین، آن را به همان تعداد بررسی میکند و اگر این فرآیند منابع زیادی را به
کار میگیرد، میتواند عملکرد برنامهی ما را به هم بریزد. برای جلوگیری از این مشکل، از متد TryAddEnumerabl()
استفاده میکنیم که لیستی از ServiceDescriptor ها را میگیرد و هر serviceDescriptor را فقط یکبار ثبت میکند:
services.TryAddEnumerable(new[] {
ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule1)),
ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule2)),
ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule3)),
ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule4)),
});