مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت هفتم - کار با سرویس‌های متفاوتی با یک امضاء
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

فرض کنید قرارداد IService را تدارک دیده‌اید و بر اساس آن سرویس‌های A و B را به سیستم تزریق وابستگی‌های برنامه‌های NET Core. تزریق کرده‌اید (برای مثال در برنامه دو DbContext را تعریف کرده‌اید و یک اینترفیس IUnitOfWork را دارید). اکنون اگر از این سیستم، یک پیاده سازی IService را درخواست کنید، چه اتفاقی رخ می‌دهد؟ در حالت معمول آن، آخرین سرویسی را که ثبت کرده‌اید، یعنی وهله‌ای از سرویس B را بازگشت خواهد داد. در ادامه قصد داریم با این قابلیت امکان ثبت چندین سرویس مشتق شده‌ی از یک اینترفیس، بیشتر آشنا شویم.


ثبت پیاده سازی‌های متعدد یک اینترفیس در سیستم تزریق وابستگی‌های NET Core.

داشتن چندین پیاده سازی از یک اینترفیس، شاید یکی از اهداف اصلی وجود آن‌ها باشد. برای نمونه قرار داد ارسال پیامی را در برنامه، مانند IMessageService به صورت زیر طراحی و سپس بر اساس آن، دو پیاده سازی ارسال پیام‌ها را از طریق ایمیل و SMS، اضافه می‌کنیم:
namespace CoreIocServices
{
    public interface IMessageService
    {
        void Send(string message);
    }

    public class EmailService : IMessageService
    {
        public void Send(string message)
        {
            // ...
        }
    }

    public class SmsService : IMessageService
    {
        public void Send(string message)
        {
            //todo: ...
        }
    }
}
در ادامه برای معرفی آن‌ها به سیستم تزریق وابستگی‌های NET Core.، به نحو متداول زیر عمل خواهیم کرد و هر دوی این پیاده سازی‌ها را بر اساس اینترفیس آن‌ها معرفی می‌کنیم:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IMessageService, EmailService>();
            services.AddTransient<IMessageService, SmsService>();
        }
در این حالت اگر این سرویس‌ها را به صورت زیر به یک کنترلر تزریق کنیم، انتظار دریافت کدام سرویس‌ها را خواهید داشت؟
using CoreIocServices;
using Microsoft.AspNetCore.Mvc;

namespace CoreIocSample02.Controllers
{
    public class MessagesController : Controller
    {
        private readonly IMessageService _emailService;
        private readonly IMessageService _smsService;

        public MessagesController(IMessageService emailService, IMessageService smsService)
        {
            _emailService = emailService;
            _smsService = smsService;
        }
    }
}
در این حالت اگر بر روی سازنده‌ی این کنترلر یک break-point را قرار دهیم، پارامترهای تزریق شده‌ی در سازنده‌ی کلاس به صورت زیر خواهند بود:


همانطور که مشاهده می‌کنید، هر دو پارامتر، با وهله‌ای از آخرین سرویس معرفی شده‌ی از نوع IMessageService مقدار دهی شده‌اند؛ یعنی SmsService در اینجا. در ادامه روش‌های مختلفی را برای رفع این مشکل بررسی می‌کنیم.


روش اول: سیستم تزریق وابستگی‌های NET Core. از سازنده‌های IEnumerable پشتیبانی می‌کند

برای مدیریت دریافت سرویس‌هایی که از یک اینترفیس مشتق شده‌اند، خود NET Core. بدون نیاز به تنظیمات اضافه‌تری، روش تزریق IEnumerableها را در سازنده‌های کلاس‌ها پشتیبانی می‌کند:
using System.Collections.Generic;
using CoreIocServices;
using Microsoft.AspNetCore.Mvc;

namespace CoreIocSample02.Controllers
{
    public class MessagesController : Controller
    {
        private readonly IEnumerable<IMessageService> _messageServices;

        public MessagesController(IEnumerable<IMessageService> messageServices)
        {
            _messageServices = messageServices;
        }
در اینجا نیز اگر بر روی سازنده‌ی این کنترلر یک break-point را قرار دهیم، پارامتر تزریق شده‌ی در سازنده‌ی کلاس به صورت زیر خواهد بود:


به این ترتیب لیست وهله‌های تمام سرویس‌هایی از نوع IMessageService در اختیار ما قرار گرفته‌است و برای اهدافی مانند پیاده سازی الگوهایی در رده‌ی chain of responsibility و یا الگوی استراتژی، مفید است. در این حالت اگر نیاز به سرویس از نوع خاصی وجود داشت، می‌توان از روش زیر استفاده کرد:
var emailService = _messageServices.OfType<EmailService>().First();
و یا اگر از روش Service Locator استفاده می‌کنید، serviceProvider به همراه متد ویژه‌ی GetServices که لیست تمام سرویس‌هایی از نوعی خاص را بر می‌گرداند نیز هست:
var messageServices = serviceProvider.GetServices<IMessageService>();


روش دوم: دریافت شرطی وهله‌های مورد نیاز با «دخالت در مراحل وهله سازی اشیاء توسط IoC Container»

روش «دخالت در مراحل وهله سازی اشیاء توسط IoC Container» را در قسمت قبل بررسی کردیم. یکی دیگر از مثال‌هایی را که در این مورد می‌توان ارائه داد، شرطی کردن بازگشت وهله‌ها است که برای سناریوی مطلب جاری بسیار مفید است:
الف) برای شرطی کردن بازگشت وهله‌ها، بهتر است این شرط را بجای رشته‌ها و یا اعدادی خاص، بر اساس یک enum مشخص انجام دهیم:
namespace CoreIocServices
{
    public enum MessageServiceType
    {
        EmailService,
        SmsService
    }
در اینجا یک enum جدید را تعریف کرده‌ایم تا مقایسه‌ها را در زمان بازگشت سرویسی خاص، بر اساس اعضای مشخص آن انجام دهیم.

ب) سپس هر دو سرویس مشتق شده‌ی از یک اینترفیس را به صورت معمولی ثبت می‌کنیم:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<EmailService>();
            services.AddTransient<SmsService>();
اینکار سبب خواهد شد تا بتوانیم در حین بررسی شرط‌های رسیده، برای مثال دستور ()<serviceProvider.GetService<EmailService را صادر کنیم.

ج) اکنون می‌توان مرحله‌ی شرطی کردن بازگشت این وهله‌ها را به صورت زیر، با دخالت در مراحل وهله سازی اشیاء، پیاده سازی کرد:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<EmailService>();
            services.AddTransient<SmsService>();
            services.AddTransient<Func<MessageServiceType, IMessageService>>(serviceProvider => key =>
            {
                switch (key)
                {
                    case MessageServiceType.EmailService:
                        return serviceProvider.GetRequiredService<EmailService>();
                    case MessageServiceType.SmsService:
                        return serviceProvider.GetRequiredService<SmsService>();
                    default:
                        throw new NotImplementedException($"Service of type {key} is not implemented.");
                }
            });
در اینجا پارامتر ارسالی به متد AddTransient را از نوع <Func<MessageServiceType, IMessageService انتخاب کرده‌ایم. این مورد نیز دقیقا مانند «مثال 2: وهله سازی در صورت نیاز وابستگی‌های یک سرویس به کمک Lazy loading» قسمت قبل عمل می‌کند. یعنی تا زمانیکه این Func، در قسمتی از کدهای برنامه فراخوانی نشود، سرویسی را نیز بازگشت نخواهد داد.

د) مرحله‌ی آخر کار، روش استفاده‌ی از این امضایء ویژه‌ی Lazy load شده‌است:
namespace CoreIocSample02.Controllers
{
    public class MessagesController : Controller
    {
        private readonly Func<MessageServiceType, IMessageService> _messageServiceResolver;

        public MessagesController(Func<MessageServiceType, IMessageService> messageServiceResolver)
        {
            _messageServiceResolver = messageServiceResolver;
        }
دقیقا همان امضای Func ای را که در متد AddTransient معرفی تنظیمات تزریق وابستگی‌ها تعریف کردیم، به سازنده‌ی یک کنترلر تزریق می‌کنیم. تا اینجای کار هنوز هیچکدام از سرویس‌های از نوع IMessageService وهله سازی نشده‌اند (روش دیگر پیاده سازی وهله سازی با تاخیر و در صورت نیاز). اکنون برای وهله سازی آن‌ها باید به صورت زیر عمل کرد:
public IActionResult Index()
{
   var emailService = _messageServiceResolver(MessageServiceType.EmailService);
   //use emailService ...
   return View();
}

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: CoreDependencyInjectionSamples-07.zip
مطالب مشابه
  • #
    ‫۵ سال و ۸ ماه قبل، سه‌شنبه ۱۸ دی ۱۳۹۷، ساعت ۱۶:۲۷
    آیا می‌توان روش‌های بالا را با کتابخانه Scrutor  پیاده سازی کرد؟
    • #
      ‫۵ سال و ۸ ماه قبل، جمعه ۲۱ دی ۱۳۹۷، ساعت ۱۳:۰۰
      بله. چون این کتابخانه جایگزین سیستم تزریق وابستگی‌های توکار برنامه‌های NET Core. نیست؛ بلکه صرفا مکمل آن است.