دریافت StructureMap
برای دریافت آن نیاز است دستور پاورشل ذیل را در کنسول نیوگت ویژوال استودیو فراخوانی کنید:
PM> Install-Package structuremap
آشنایی با ساختار برنامه
ابتدا یک برنامه کنسول را آغاز کرده و سپس یک Class library جدید را به نام Services نیز به آن اضافه کنید. در ادامه کلاسها و اینترفیسهای زیر را به Class library ایجاد شده، اضافه کنید. سپس از طریق نیوگت به روشی که گفته شد، StructureMap را به پروژه اصلی (ونه پروژه Class library) اضافه نمائید و Target framework آنرا نیز در حالت Full قرار دهید بجای حالت Client profile.
namespace DI03.Services { public interface IUsersService { string GetUserEmail(int userId); } } namespace DI03.Services { public interface IEmailsService { void SendEmailToUser(int userId, string subject, string body); } } using System; namespace DI03.Services { public class UsersService : IUsersService { public UsersService() { //هدف صرفا نمایش وهله سازی خودکار این وابستگی است Console.WriteLine("UsersService ctor."); } public string GetUserEmail(int userId) { //برای مثال دریافت از بانک اطلاعاتی و بازگشت یک نمونه جهت آزمایش برنامه return "name@site.com"; } } } using System; namespace DI03.Services { public class EmailsService: IEmailsService { private readonly IUsersService _usersService; public EmailsService(IUsersService usersService) { Console.WriteLine("EmailsService ctor."); _usersService = usersService; } public void SendEmailToUser(int userId, string subject, string body) { var email = _usersService.GetUserEmail(userId); Console.WriteLine("SendEmailTo({0})", email); } } }
سرویس کاربران بر اساس آی دی یک کاربر، برای مثال از بانک اطلاعاتی ایمیل او را بازگشت میدهد. سرویس ارسال ایمیل، نیاز به ایمیل کاربری برای ارسال ایمیلی به او دارد. بنابراین وابستگی مورد نیاز خود را از طریق تزریق وابستگیها در سازنده کلاس و وهله سازی شده در خارج از آن (معکوس سازی کنترل)، دریافت میکند.
در سازندههای هر دو کلاس سرویس نیز از Console.WriteLine استفاده شدهاست تا زمان وهله سازی خودکار آنها را بتوان بهتر مشاهده کرد.
نکته مهمی که در اینجا وجود دارد، بیخبری لایه سرویس از وجود IoC Container مورد استفاده است.
استفاده از لایه سرویس و تزریق وابستگیها به کمک StructureMap
using DI03.Services; using StructureMap; namespace DI03 { class Program { static void Main(string[] args) { // تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود ObjectFactory.Initialize(x => { x.For<IEmailsService>().Use<EmailsService>(); x.For<IUsersService>().Use<UsersService>(); }); //نمونهای از نحوه استفاده از تزریق وابستگیهای خودکار var emailsService = ObjectFactory.GetInstance<IEmailsService>(); emailsService.SendEmailToUser(userId: 1, subject: "Test", body: "Hello!"); } } }
به این ترتیب IoC Container ما زمانیکه قرار است object graph مربوط به IEmailsService درخواستی را تشکیل دهد، خواهد دانست ابتدا به سازندهی کلاس EmailsService میرسد. در اینجا برای وهله سازی این کلاس به صورت خودکار، باید وابستگیهای آنرا نیز وهله سازی کند. بنابراین بر اساس تنظیمات آغازین برنامه میداند که باید از کلاس UsersService برای تزریق خودکار وابستگیها در سازنده کلاس ارسال ایمیل استفاده نماید.
در این حالت اگر برنامه را اجرا کنیم، به خروجی زیر خواهیم رسید:
UsersService ctor. EmailsService ctor. SendEmailTo(name@site.com)
ابتداییترین مزیت استفاده از تزریق وابستگیها امکان تعویض آنها است؛ خصوصا در حین Unit testing. اگر کلاسی برای مثال قرار است با شبکه کار کند، میتوان پیاده سازی آنرا با یک نمونه اصطلاحا Fake جایگزین کرد و در این نمونه تنها نتیجهی کار را بازگشت داد. کلاسهای لایه سرویس ما تنها با اینترفیسها کار میکنند. این تنظیمات قابل تغییر اولیه IoC container مورد استفاده هستند که مشخص میکنند چه کلاسهایی باید در سازندههای کلاسها تزریق شوند.
تعیین طول عمر اشیاء در StructureMap
برای اینکه بتوان طول عمر اشیاء را بهتر توضیح داد، کلاس سرویس کاربران را به نحو زیر تغییر دهید:
using System; namespace DI03.Services { public class UsersService : IUsersService { private int _i; public UsersService() { //هدف صرفا نمایش وهله سازی خودکار این وابستگی است Console.WriteLine("UsersService ctor."); } public string GetUserEmail(int userId) { _i++; Console.WriteLine("i:{0}", _i); //برای مثال دریافت از بانک اطلاعاتی و بازگشت یک نمونه جهت آزمایش برنامه return "name@site.com"; } } }
//نمونهای از نحوه استفاده از تزریق وابستگیهای خودکار var emailsService1 = ObjectFactory.GetInstance<IEmailsService>(); emailsService1.SendEmailToUser(userId: 1, subject: "Test1", body: "Hello!"); var emailsService2 = ObjectFactory.GetInstance<IEmailsService>(); emailsService2.SendEmailToUser(userId: 1, subject: "Test2", body: "Hello!");
UsersService ctor. EmailsService ctor. i:1 SendEmailTo(name@site.com) UsersService ctor. EmailsService ctor. i:1 SendEmailTo(name@site.com)
اگر به هر دلیلی نیاز بود تا این رویه تغییر کند، میتوان بر روی طول عمر اشیاء تشکیل شده نیز تاثیر گذار بود. برای مثال تنظیمات آغازین برنامه را به نحو ذیل تغییر دهید:
// تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود ObjectFactory.Initialize(x => { x.For<IEmailsService>().Use<EmailsService>(); x.For<IUsersService>().Singleton().Use<UsersService>(); });
UsersService ctor. EmailsService ctor. i:1 SendEmailTo(name@site.com) EmailsService ctor. i:2 SendEmailTo(name@site.com)
حالتهای دیگر تعیین طول عمر مطابق متدهای زیر هستند:
Singleton() HttpContextScoped() HybridHttpOrThreadLocalScoped()
در حالت ThreadLocal، به ازای هر Thread، وهلهای متفاوت در اختیار مصرف کننده قرار میگیرد.
حالت Hybrid ترکیبی است از حالتهای HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد کرد در غیراینصورت به ThreadLocal سوئیچ میکند.
شاید بپرسید که کاربرد مثلا HttpContextScoped در کجا است؟
در یک برنامه وب نیاز است تا یک وهله از DbContext (مثلا Entity framework) را در اختیار کلاسهای مختلف لایه سرویس قرار داد. به این ترتیب چون هربار new Context صورت نمیگیرد، هربار هم اتصال جداگانهای به بانک اطلاعاتی باز نخواهد شد. نتیجه آن رسیدن به یک برنامه سریع، با سربار کم و همچنین کار کردن در یک تراکنش واحد است. چون هربار فراخوانی new Context به معنای ایجاد یک تراکنش جدید است.
همچنین در این برنامه وب قصد نداریم از حالت طول عمر Singleton استفاده کنیم، چون در این حالت یک وهله از Context در اختیار تمام کاربران سایت قرار خواهد گرفت (و DbContext به صورت Thread safe طراحی نشده است). نیاز است به ازای هر کاربر و به ازای طول عمر هر درخواست، تنها یکبار این وهله سازی صورت گیرد. بنابراین در این حالت استفاده از HttpContextScoped توصیه میشود. به این ترتیب در طول عمر کوتاه Object graphهای تشکیل شده، فقط یک وهله از DbContext ایجاد و استفاده خواهد شد که بسیار مقرون به صرفه است.
مزیت دیگر مشخص سازی طول عمر به نحو HttpContextScoped، امکان Dispose خودکار آن به صورت زیر است:
protected void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); }
تنظیمات خودکار اولیه در StructureMap
اگر نام اینترفیسهای شما فقط یک I در ابتدا بیشتر از نام کلاسهای متناظر با آنها دارد، مثلا مانند ITest و کلاس Test هستند؛ فقط کافی است از قراردادهای پیش فرض StructureMap برای اسکن یک یا چند اسمبلی استفاده کنیم:
// تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود ObjectFactory.Initialize(x => { //x.For<IEmailsService>().Use<EmailsService>(); //x.For<IUsersService>().Singleton().Use<UsersService>(); x.Scan(scan => { scan.AssemblyContainingType<IEmailsService>(); scan.WithDefaultConventions(); }); });
دریافت مثال قسمت جاری
DI03.zip
به روز شدهی این مثالها را بر اساس آخرین تغییرات وابستگیهای آنها از مخزن کد ذیل میتوانید دریافت کنید:
Dependency-Injection-Samples