وب دیتا
صفحه اصلی / آموزش طراحی وب / آموزش MVC / تزریق وابستگی (Dependency Injection) و سرویس ها در MVC

تزریق وابستگی (Dependency Injection) و سرویس ها در MVC

Dependency Injection

این مقاله نگاهی به نقش سرویس ها و سیستم جدید Dependency Injection ASP.NET Core می پردازد. مجموعه ای از مقالات با استفاده از Visual Studio RTM و Core Beta 6 ASP.NET توسعه داده شده است. این مقالات همراه با تحقیقات جدیدتر به روز خواهند شد.

قبل از اینکه در مورد سیستم تزریق وابستگی ASP.NET Core بحث کنم، نکته مهمتراین است که تزریق وابستگی برای حل کدام مشکل طراحی شده است. برای بررسی این مشکل، مجبور خواهیم بود که از قسمتEmpty template  فعلا صرف نظر کرده و آخرین بحثِ مقاله ی قبلی شروع کنیم . در این قسمت، برخی از استانداردهای MVC را به پروژه اضافه میکنیم؛ از جمله HomeController و Viewهای وابسته به آن، همراه با فایل های جاوا اسکریپت و CSS از  Web Application template. همچنین برای فعال سازی خدمات مربوط به فایل های استاتیک و فراهم کردن آن ها برای TagHelpers استفاده شده در viewها، دو پکیج اضافی به پروژه اضافه کنم.

مشکل:
یک الگوی مشترک در توسعه وب -به ویژه در میان مبتدیان- این است که همه ی کدهای مربوط به انجام آنچه که یک web page نیاز دارد(داده ها، ایمیل، رفع خطا، ورود به سیستم، احراز هویت و غیره) را در یک جای مشخص قرار داده و متمرکز کنیم. در دنیای مایکروسافت، این تمرین از کد اسپاگتی در یک کدفایل کلاسیک از ASP  آغاز شده و به ترتیب به code behind file برای web form و سپس کنترلری در یک MVC application تکامل یافته است. به کدهای زیر توجه کنید

[HttpPost]
public async Task<ActionResult> Contact(ContactViewModel model)
{
    using (var smtp = new SmtpClient("localhost"))
    {
        var mail = new MailMessage
        {
            Subject = model.Subject,
            From = new MailAddress(model.Email),
            Body = model.Message
        };
        mail.To.Add("Support@domain.com");
        await smtp.SendMailAsync(mail);
    }
    return View();
}

کدبالا مثالی از نوعِ کدهایی است که در بسیاری از نمونه های آزمایشی(از جمله برخی قسمت های سایت من)یافت می شود و یک متد controller action دارد که در پاسخ به یک درخواست POST ، با توجه به مقادیر ارائه شده در view model، ایمیلی را تولید کرده و می فرستد. پس مشکلش چیست؟

وابستگی ها:

مشکل اصلی کلاس کنترلر، پذیرفتن مسئولیت قسمت هایی است که آنچنان مهم و نگران کننده نیستند. کدی که مدیریت ایمیل را بر عهده دارد، یک وابستگی از کلاس کنترلر است. قابلیت معرفی شده مشکلات زیر را به وجود می آورد:

  • اگر می خواهید که سیستم ایمیل را تغییر داده و مثلا به Web Service تبدیل کنید، باید تغییراتی را در کلاس کنترلر ایجاد کنید که این کار تنها مسئولیت مربوط به کلاس کنترلر یعنی آماده کردن داده ها برای viewها را به خطر می اندازد و این کلاس نباید در قبال ارسال ایمیلی مسئول باشد. تغییر کدِ مربوط به یکی از مسئولیت های کلاس کنترلر می تواند اشکالاتی را در مسئولیت های دیگری که برای آن تعریف شده ایجاد کند.
  •    اگر قابلیت ارسال ایمیل در قسمت های مختلف application وجود داشته و همگی عملکرد مشابهی دارند پس باید دائما تغییرات یکسانی را برای هرکدام اعمال کنید. تغییرات بیشتر در قسمت های مختلف، احتمال بروز اشکال را در قسمت های مختلف افزایش می دهد – و همچنین ممکن است یک یا چند قسمتی که باید در آن ها تغییر اعمال بشود را نادیده بگیرد.
  •    اگر می خواهید متد controller’s Contact را (به عنوان مثال برای اطمینان از بازگشت View صحیح)تست کنید، قبل از انجام هر کاری باید ایمیل تولید کنید. برای تولید ایمیل، باید با فرایندهای بیرونی ارتباط برقرار کنید، که سرعت تست را کاهش می دهد.
  • این تست قبل از هرگونه خطای منطقی در انتخاب view ، به دلیل وجود مشکلاتی در روال(routine ) ارسال ایمیل، ناموفق خواهد بود(و در انتخاب قسمت هایی که باید برای تست پوشش بدهد به مشکل برمی خورد.)

برای یک طراحی خوب توصیه می شود که مولفه ها(components) و یا سرویس مشخصی ایجاد کنید تا به صورت مستقل، قسمت های لازم برای عملکرد را پوشش دهید و در جاهایی که لازم است آن را فراخوانی کنید. در این مثال، کد مربوط به ارسال پیام(mailing) باید به قطعات خاص خودش تقسیم شده تا یک متد عمومی (public) از درون کنترلر بتواند آن را فراخوانی کند. در اینجا کد برنامه ممکن است بازسازی شده و کلاسی با نام EmailSender ایجاد کند، که مسئولیت ارسال از طریق ایمیل را بر عهده خواهد داشت:

using System.Net.Mail;
using System.Threading.Tasks;

namespace WebApplication2.Services
{
    public class EmailSender
    {
        public async Task SendEmailAsync(string email, string subject, string message)
        {
            using (var smtp = new SmtpClient("localhost"))
            {
                var mail = new MailMessage
                {
                    Subject = subject,
                    From = new MailAddress(email),
                    Body = message
                };
                mail.To.Add("Support@domain.com");
                await smtp.SendMailAsync(mail);
            }
        }
    }
}

توجه داشته باشید که این برنامه نمایشی است. در component اصلی ایمیل بخشهای زیادی برای معتبرسازی و مدیریت خطاها گنجانده شده است و در اینجا نحوه قرارگیری مولفه EmailSender در controller action را نشان می دهیم:

[HttpPost]
public async Task<ActionResult> Contact(ContactViewModel model)
{
    EmailSender _emailSender = new EmailSender();
    await _emailSender.SendEmailAsync(model.Email, model.Subject, model.Message);
    return View();
}

در حال حاضر کنترلر، هیچ اطلاعی از چگونگی ارسال ایمیل ندارد. تا آنجا که به کنترلر مربوط می شود، کلاس EmailSender یک جعبه سیاهی است که جزئیات را کپسوله میکند (encapsulate)؛ جزئیات مربوط به چگونگی تولید ایمیلها و ارسال آنهاست. همه ی کلاسهای کنترلر باید بدانند کلاس EmailSender متد SendEmailAsync را برای پذیرش بعضی از رشته ها ارائه می دهد. اگر می خواهید هر گونه تغییری را در ارسال ایمیل ایجاد کنید، کافی است فقط EmailSender class/component را تغییر دهید و لزومی به تغییر کد کنترلر نیست. عدم تغییر کد کنترلر ممکن است در هنگام به روز رسانی روال ایمیل، errorهای جدیدی را برای کلاس کنترلر به وجود آورد. همچنین ضرورتی برای اعمال این تغییرات یکسان در قسمت های مختلفی که در آن ایمیل فرستاده می شود نیست که باعث صرفه جویی در زمان می شود.
اما هنوز یک مشکل وجود دارد. کنترلر هنوز هم وابستگی شدیدی به نوع خاصی از کامپوننتِ ایمیل دارد. هنگامی که controller action را تست میکنید، بازهم یک ایمیل ارسال خواهد شد. شما می توانید با جایگزینی کلاس EmailSender با کلاسMockEmailSender تغییراتی در کدتان ایجاد کنید تا درهنگام اجرای آزمون ایمیلی ارسال نشود. با این حال، هر زمانی که قصد اجرای آزمون را داشته باشید، باید تغییراتی را در بخش های مختلف انجام دهید و برای تمام سرویس های دیگر که به فرآیندهای بیرونی (ورود به سیستم، دسترسی به داده ها و غیره) وابسته هستند. سپس باید تمامی تغییرات را به حالت قبل برگردانده که راه حل عملی و مناسبی نیست. واقعیت این است که با تلاش و سختی زیاد هم نمی توانید آزمون خود اجرا کنید.

تزریق وابستگی:

Dependency Injection

تزریق وابستگی (DI) یک الگوی طراحیست برای رسیدگی به مشکلی که در بالا مطرح شد یعنی جایی که یک کلاس سرویسی را پیاده سازی کرد. DI با تزریق سرویس هایی به کلاسهای وابسته، این جفت شدگی را کاهش میدهد. برای انجام این کار از روش های مختلفی می توان استفاده کرد. با استفاده از setter می توان این سرویس ها را به سازنده کلاس و یا به یکی از خواص آن تزریق کرد یا به جای تزریق یک نوع خاصی از سرویس ها( EmailSender یا MockEmailSender یا ExchangeEmailSender ) یک انتزاع (abstraction) که نشان دهنده سرویس است، تزریق شود. انتزاع به صورت یک اینترفیس تعریف شده است. در این مثال بسیار ساده، اینترفیس یک متد را مشخص می کند:

public interface IEmailSender
{
    Task SendEmailAsync(string email, string subject, string message);
}

اینترفیس IEmailSender یک الگوی مشخص (contract معادل دیگری برای آن است) مشخص می کند که هر نوعی که بخواهد به عنوان یک IEmailSender دیده شود باید برای موافقت با اینترفیس از IEmailSender پیروی کند. در حال حاضر، هر نوع، باید متد SendEmailAsync که به وسیله اینترفیس سه رشته را (به صورتی که قبلا مشخص شد) می گیرد را پیاده سازی کند. در حال حاضر تمامی نیازمندی های کلاس EmailSender برطرف شده پس برای تبدیل به IEmailSender کافیست به صورت رسمی اینترفیس را پیاده سازی کنیم. پس بعد از نام کلاس یک colon اضافه می کنیم و سپس نام اینترفیسی که می خواهیم پیاده سازی کنیم را می آوریم:

public class EmailSender : IEmailSender
{
    public async Task SendEmailAsync(string email, string subject, string message)
    {       
    .... 

در تکرار بعدی از کنترلر، کلاس EmailSender توسط سازنده کلاس، تزریق شده و برای استفاده درون کنترلر، این کلاس داخل یک فیلد خصوصی ذخیره شده است:

public class HomeController : Controller
{
    IEmailSender _emailSender;
    public HomeController(EmailSender emailSender)
    {
        _emailSender = emailSender;
    }
        
    .... 
 
    [HttpPost]
    public async Task<ActionResult> Contact(ContactViewModel model)
    {
        await _emailSender.SendEmailAsync(model.Email, model.Subject, model.Message);
        return View();
    }
    ....

در حقیقت در اینجا می توانید بازسازی(refactoring) را متوقف کنید. این یک نمونه از تزریق وابستگی است که با اصطلاح کمی تنزل یافته تر یعنی ” poor man’s dependency injection” شناخته شده است. کد درون کنترلر از قاعده ” program to an interface ” پیروی می کند که کنترلر را از یک پیاده سازی(جدا از سازنده اش) مخصوص جدا کرده و جدایی های مربوطه را فعال می کند. اگر برای آزمون واحدِ کد خود طرح خاصی ندارید و در حال حاضر برای تغییر در پیاده سازی IEmailSender دلیل خوبی ندارید (به عنوان مثال بخشی از تنظیمات مورد نیاز برای انجام این کار نیست) پس می توانید از راه ساده و جذاب تر استفاده کنید. با این حال ممکن است به خواندن ادامه داده و روش بهتری برای کاهش جفت شدگی وابستگی ها پیدا کنید و در مورد سیستم تزریق وابستگی درCore ASP.NET بیشتر یاد بگیرید.

کد زیر چگونگی جایگزینی پیاده سازی کلاس EmailSender با اینترفیسِ کلاس کنترلر و حذف تمامی وابستگی های EmailSender را نشان می دهد:

public class HomeController : Controller
{
    IEmailSender _emailSender;
    public HomeController(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }
        
    .... 
 
    [HttpPost]
    public async Task<ActionResult> Contact(ContactViewModel model)
    {
        await _emailSender.SendEmailAsync(model.Email, model.Subject, model.Message);
        return View();
    }
    ....

یک فیلد private به کلاس سازنده اضافه شده است.این فیلد از نوع IEmailSender است و از آن برای ذخیره نمونه ای از IEmailSender استفاده می شود که توسط سازنده تزریق شده است که همچنین به کلاس اضافه شده است. سپس این نمونه مانند قبل به متد Contact ارجاع داده می شود. اگر هم اکنون می خواهید اپلیکیشن را اجرا کنید با خطای زیر روبرو می شوید:

Dependency Injection

یک استثناء ایجاد می شود زیرا در حال حاضر، هیچ راهی برای اپلیکیشن وجود ندارد که بداند به چه نوعی از IEmailSender باید resolved  شود. امکان ایجاد نمونه از اینترفیس و نمایش آن وجود ندارد و نمی توان یک متد از آن را فراخواند. این یک انتزاع است و متدهای آن فقط تعریف می شوند ولی پیاده سازی نمی شوند. در پیامی که از قسمت بالا نمایش داده شد می توان دید که استثنا به وسیله فریمورک تزریق وابستگی جدیدی تولید شده و این همان چیزی است که لازم است گفته بشود؛ هنگام برخورد با IEmailSender چه چیزی باید استفاده شود ؟با اضافه کردن یک registration به یک dependency injection container می توان این کار را انجام داد.

DI Containers در ساده ترین حالتشان، ساختاری شبیه دیکشنری دارند که abstraction و concrete type را ذخیره می کنند و هرجایی که از abstraction استفاده شود فراخوانده می شوند. بیشتر DI containers قابلیت های بیشتری را ارائه می کنند اما همگی هسته یکسانی داشته و به نوعی look-up table هستند.

تزریق وابستگی در ASP.NET MVC چیزی جدید نیست. افراد زیادی سالیان سال با آن کار کرده و از third party DI containers مختلف برای مدیریت resolving of types استفاده می کردند. در MVC 6 تغییر جدید رخ داده و یک basic DI container به عنوان یک بخش در فریمورک گنجانیده شده است. البته این ویژگی همه ی سناریوهای موجود را پوشش نمی دهد ولی برای بیشتر سناریوهای رایج کافی است.

در آموزش قبلی، با استفاده از متد توسعه یافته AddMvc ، MVC را به عنوان یک سرویس با DI container ثبت کردم؛ این متد در متد Startup class’s ConfigureServices قرار داشت. این قسمت همان جایی است که معمولا سرویس های دیگر را به کمک متدهای explicitly یا extension ثبت خواهید کرد. کدهای زیر نحوه ثبت EmailSender class explicitly را نشان می دهد:

services.AddTransient<IEmailSender, EmailSender>();

این ساده ترین متد برای اضافه کردن سرویس ها به اپلیکیشن است که پارامتر اول آن به عنوان سرویس و پارامتر دوم به عنوان پیاده سازی است. اگر در متد ConfigureServices یک breakpoint قرار دهید و سرویس ها را بررسی کنید می توانید تعداد زیادی سرویس که با مقادیر مختلفِ lifetime  ثبت شده اند را مشاهده کنید:

Dependency Injection

EmailSender با دامنه گذرا (Transient scope)به وسیله متد AddTransient<TService, TImplementation> به ثبت رسید. سرویس های ثبت شده با دامنه گذرا هرزمان که لازم شوند در داخل اپلیکیشن ایجاد می شوند. این بدان معناست که در هر اجرای متد contact توسط فریمورک، تزریق وابستگی یک نمونه جدید از کلاس EmailSender ایجاد خواهد شد.دو مقدارLifetime (طول عمر)که در تصویر دیده می شود عبارتند از: Singleton و Scoped . singleton با استفاده از متد AddSingleton ثبت می شود و نتیجه آن در قالب یک نمونه از سرویسی که در شروع برنامه ایجاد شده، خواهد بود و برای تمام درخواست های پس از آن قابل دسترس خواهد بود. آیتم های اضافه شده با Scoped lifetime از طریق متد AddScoped برای مدت زمان درخواست شده در دسترس خواهند بود.همچنین یک متد AddInstance وجود دارد که با استفاده از آن می توانید یک singleton ثبت کنید، اما به جای رهاکردن DI system باید یک نمونه ایجاد کنید.

همانطور که قبلا اشاره کردم ویژگی ها و امکانات سیستم Dependency Injection توکار(built in) آسان و قابل فهم هستند. تمام آیتم ها باید به صورت دستی ثبت بشوند. انتظار می رود که توسعه دهندگانِ ظروف پیشرفته تر تزریق وابستگی، کارهای لازم برای تطبیق سیستم هایشان با نیازمندی های ASP.NET Core را بر عهده گرفته و انجام دهند تا استفاده از dependency injection containers که جاگزینی برای default container است، آسان شود.

خلاصه

در ابتدای این مقاله، به بررسی این موضوع پرداختیم که Dependency Injection در یک برنامه ASP.NET MVC برای چه ایجاد شده است، و سپس سیستم تزریق وابستگی ASP.NET Core را بررسی کرده و نحوه ایجاد، ثبت و مصرف یک سرویس ساده را بررسی کردیم.



رمز فایل : www.mspsoft.ir , www.mspsoft.com
کانال ام اس پی سافت

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *



دوره های آنلاین ام اس پی سافت

آموزش طراحی فروشگاه اینترنتی

آموزش طراحی فروشگاه اینترنتی

طراحی سیستم مدیریت مشتریان

طراحی سیستم مدیریت مشتریان

دوره طراحی وب سایت پورتال خبری

دوره طراحی وب سایت پورتال خبری

دوره طراحی حسابداری فروشگاه

دوره طراحی حسابداری فروشگاه
تخفیفات نوروزی ام اس پی سافت

5 کد تخفیف 100 درصدی برای کاربران شبکه های اجتماعی | آدرس شبکه های اجتماعی ما : @mspsoft

هر روز یک کد تخفیف برای 5 نفر اول در شبکه انیستاگرام و تلگرام