اشتراکها
اشتراکها
بررسی افزونه jPages
مقوله همزمانی و غیر همزمانی در بحث خواندن و نوشتن از TCP همواره از موارد پیچیده نام برده میشود. کاش مثالی در این زمینه هم عنوان کنید
«قسمت هشتم- تعریف سطوح دسترسی پیچیده » :: با نوشتن یک AuthorizationHandler سفارشی
نظرات مطالب
آشنایی با Defensive programming
ولی بعضی وقتها اعتبارسنجی مقادیر ورودی آنقدر زمانگیر هستند و پیچیده که بهتر است بگذاریم خطا به وجود بیاید و وقتی به وجود آمد آن را «مدیریت» کنیم.
جهت تکمیل مباحث این دوره میتوان به نحوه مدیریت سشنها و document store بانک اطلاعاتی RavenDB با استفاده از یک IoC Container مانند StructureMap در ASP.NET MVC پرداخت. اصول کلی آن به تمام فناوریهای دات نتی دیگر مانند وب فرمها، WPF و غیره نیز قابل بسط است. تنها پیشنیاز آن مطالعه «کامل» دوره «بررسی مفاهیم معکوس سازی وابستگیها و ابزارهای مرتبط با آن » میباشد.
هدف از بحث
ارائه راه حلی جهت تزریق یک وهله از واحد کار تشکیل شده (همان شیء سشن در RavenDB) به کلیه کلاسهای لایه سرویس برنامه و همچنین زنده نگه داشتن شیء document store آن در طول عمر برنامه است. ایجاد شیء document store که کار اتصال به بانک اطلاعاتی را مدیریت میکند، بسیار پرهزینه است. به همین جهت این شیء تنها یکبار باید در طول عمر برنامه ایجاد شود.
ابزارها و پیشنیازهای لازم
ابتدا یک برنامه جدید ASP.NET MVC را آغاز کنید. سپس ارجاعات لازم را به کلاینت RavenDB، سرور درون پروسهای آن (RavenDB.Embedded) و همچنین StructureMap با استفاده از نیوگت، اضافه نمائید:
دریافت کدهای کامل این مثال
این مثال، به همراه فایلهای باینری ارجاعات یاد شده، نیست (جهت کاهش حجم 100 مگابایتی آن). برای بازیابی آنها میتوانید به مطلبی در اینباره در سایت مراجعه کنید.
این پروژه از چهار قسمت مطابق شکل زیر تشکیل شده است:
الف) لایه سرویسهای برنامه
نکته مهمی که در اینجا وجود دارد، استفاده از اینترفیسهای خود RavenDB است. به عبارتی IDocumentSession، تشکیل دهنده الگوی واحد کار در RavenDB است و نیازی به تعاریف اضافهتری در اینجا وجود ندارد.
هر کلاس لایه سرویس با یک اینترفیس مشخص شده و اعمال آنها از طریق این اینترفیسها در اختیار کنترلرهای برنامه قرار میگیرند.
ب) لایه Infrastructure برنامه
در این لایه کدهای اتصالات IoC Container مورد استفاده قرار میگیرند. کدهایی که به برنامه جاری وابستهاند، اما حالت عمومی و مشترکی ندارند تا در سایر پروژههای مشابه استفاده شوند.
تعاریف اتصالات StructureMap را در اینجا ملاحظه میکنید.
IDocumentStore و IDocumentSession، دو اینترفیس تعریف شده در کلاینت RavenDB هستند. اولی کار اتصال به بانک اطلاعاتی را مدیریت خواهد کرد و دومی کار مدیریت الگوی واحد کار را انجام میدهد. IDocumentStore به صورت Singleton تعریف شده است؛ چون باید در طول عمر برنامه زنده نگه داشته شود. اما IDocumentStore در ابتدای هر درخواست رسیده، وهله سازی شده و سپس در پایان هر درخواست در متد ApplicationEndRequest به صورت خودکار Dispose خواهد شد.
اگر به فایل Global.asax.cs پروژه وب برنامه مراجعه کنید، نحوه استفاده از این کلاس را مشاهده خواهید کرد:
در ابتدای کار برنامه، متد IoCConfig.ApplicationStart جهت برقراری اتصالات، فراخوانی میشود. در پایان هر درخواست نیز شیء سشن جاری تخریب خواهد شد. همچنین کلاس StructureMapControllerFactory نیز جهت وهله سازی خودکار کنترلرهای برنامه به همراه تزریق وابستگیهای مورد نیاز، تعریف گشته است.
ج) استفاده از کلاسهای لایه سرویس در کنترلرهای برنامه
پس از این مقدمات و طراحی اولیه، استفاده از کلاسهای لایه سرویس در کنترلها، ساده خواهند بود. تنها کافی است اینترفیسهای مورد نیاز را از طریق روش تزریق در سازنده کلاسها تعریف کنیم. سایر مسایل وهله سازی آن خودکار خواهند بود.
هدف از بحث
ارائه راه حلی جهت تزریق یک وهله از واحد کار تشکیل شده (همان شیء سشن در RavenDB) به کلیه کلاسهای لایه سرویس برنامه و همچنین زنده نگه داشتن شیء document store آن در طول عمر برنامه است. ایجاد شیء document store که کار اتصال به بانک اطلاعاتی را مدیریت میکند، بسیار پرهزینه است. به همین جهت این شیء تنها یکبار باید در طول عمر برنامه ایجاد شود.
ابزارها و پیشنیازهای لازم
ابتدا یک برنامه جدید ASP.NET MVC را آغاز کنید. سپس ارجاعات لازم را به کلاینت RavenDB، سرور درون پروسهای آن (RavenDB.Embedded) و همچنین StructureMap با استفاده از نیوگت، اضافه نمائید:
PM> Install-Package RavenDB.Client PM> Install-Package RavenDB.Embedded -Pre PM> Install-Package structuremap
دریافت کدهای کامل این مثال
این مثال، به همراه فایلهای باینری ارجاعات یاد شده، نیست (جهت کاهش حجم 100 مگابایتی آن). برای بازیابی آنها میتوانید به مطلبی در اینباره در سایت مراجعه کنید.
این پروژه از چهار قسمت مطابق شکل زیر تشکیل شده است:
الف) لایه سرویسهای برنامه
using RavenDB25Mvc4Sample.Models; using System.Collections.Generic; namespace RavenDB25Mvc4Sample.Services.Contracts { public interface IUsersService { User AddUser(User user); IList<User> GetUsers(int page, int count = 20); } } using System.Collections.Generic; using System.Linq; using Raven.Client; using RavenDB25Mvc4Sample.Models; using RavenDB25Mvc4Sample.Services.Contracts; namespace RavenDB25Mvc4Sample.Services { public class UsersService : IUsersService { private readonly IDocumentStore _documentStore; private readonly IDocumentSession _documentSession; public UsersService(IDocumentStore documentStore, IDocumentSession documentSession) { _documentStore = documentStore; _documentSession = documentSession; } public User AddUser(User user) { _documentSession.Store(user); return user; } public IList<User> GetUsers(int page, int count = 20) { return _documentSession.Query<User>() .Skip(page * count) .Take(count) .ToList(); } //todo: سایر متدهای مورد نیاز در اینجا } }
هر کلاس لایه سرویس با یک اینترفیس مشخص شده و اعمال آنها از طریق این اینترفیسها در اختیار کنترلرهای برنامه قرار میگیرند.
ب) لایه Infrastructure برنامه
در این لایه کدهای اتصالات IoC Container مورد استفاده قرار میگیرند. کدهایی که به برنامه جاری وابستهاند، اما حالت عمومی و مشترکی ندارند تا در سایر پروژههای مشابه استفاده شوند.
using Raven.Client; using Raven.Client.Embedded; using RavenDB25Mvc4Sample.Services; using RavenDB25Mvc4Sample.Services.Contracts; using StructureMap; namespace RavenDB25Mvc4Sample.Infrastructure { public static class IoCConfig { public static void ApplicationStart() { ObjectFactory.Initialize(x => { // داکیومنت استور سینگلتون تعریف شده چون باید در طول عمر برنامه زنده نگه داشته شود x.ForSingletonOf<IDocumentStore>().Use(() => { return new EmbeddableDocumentStore { DataDirectory = "App_Data" }.Initialize(); }); // سشن در برنامه وب هیبرید تعریف شده تا در طول عمر یک درخواست زنده نگه داشته شود // در برنامههای ویندوزی حالت هیبرید را حذف کنید x.For<IDocumentSession>().HybridHttpOrThreadLocalScoped().Use(context => { return context.GetInstance<IDocumentStore>().OpenSession(); }); // اتصالات لایه سرویس در اینجا x.For<IUsersService>().Use<UsersService>(); // ... }); } public static void ApplicationEndRequest() { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } } }
IDocumentStore و IDocumentSession، دو اینترفیس تعریف شده در کلاینت RavenDB هستند. اولی کار اتصال به بانک اطلاعاتی را مدیریت خواهد کرد و دومی کار مدیریت الگوی واحد کار را انجام میدهد. IDocumentStore به صورت Singleton تعریف شده است؛ چون باید در طول عمر برنامه زنده نگه داشته شود. اما IDocumentStore در ابتدای هر درخواست رسیده، وهله سازی شده و سپس در پایان هر درخواست در متد ApplicationEndRequest به صورت خودکار Dispose خواهد شد.
اگر به فایل Global.asax.cs پروژه وب برنامه مراجعه کنید، نحوه استفاده از این کلاس را مشاهده خواهید کرد:
using System; using System.Globalization; using System.Web.Mvc; using System.Web.Routing; using RavenDB25Mvc4Sample.Infrastructure; using StructureMap; namespace RavenDB25Mvc4Sample { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { IoCConfig.ApplicationStart(); AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); //Set current Controller factory as StructureMapControllerFactory ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); } protected void Application_EndRequest(object sender, EventArgs e) { IoCConfig.ApplicationEndRequest(); } } public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { if (controllerType == null) throw new InvalidOperationException(string.Format("Page not found: {0}", requestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture))); return ObjectFactory.GetInstance(controllerType) as Controller; } } }
ج) استفاده از کلاسهای لایه سرویس در کنترلرهای برنامه
using System.Web.Mvc; using Raven.Client; using RavenDB25Mvc4Sample.Models; using RavenDB25Mvc4Sample.Services.Contracts; namespace RavenDB25Mvc4Sample.Controllers { public class HomeController : Controller { private readonly IDocumentSession _documentSession; private readonly IUsersService _usersService; public HomeController(IDocumentSession documentSession, IUsersService usersService) { _documentSession = documentSession; _usersService = usersService; } [HttpGet] public ActionResult Index() { return View(); //نمایش صفحه ثبت } [HttpPost] public ActionResult Index(User user) { if (this.ModelState.IsValid) { _usersService.AddUser(user); _documentSession.SaveChanges(); return RedirectToAction("Index"); } return View(user); } } }
نظرات مطالب
بهبود کارآیی حلقههای foreach در دات نت 7
یک نکتهی تکمیلی: آشنایی با مفهوم «C# Lowering»
در این مطلب، جهت بررسی درک علت یکسان بودن کارآیی حلقههایی که از دیدگاه ما یکی نیستند، از قابلیت نمایش #Low-level C استفاده شد که نام اصلی آن «C# Lowering» است. Lowering به معنای ترجمهی امکانات سطح بالای یک زبان به امکانات سطح پایین آن است. یعنی حاصل عملیات صورت گرفته نیز باز به همان زبان اولیه است که نمونهی آن، تبدیل یک حلقهی foreach سطح بالا به نمونهی سطح پایینی است که توسط NET Runtime. بهتر درک شده و سادهتر اجرا میشود.
مزایای Lowering
- بهبود کارآیی برنامه: برای مثال یکی از کارهایی که در این بین عموما انجام میشود «Loop unrolling» است. یعنی یک حلقه به چندین حلقهی کوچکتر تقسیم میشود تا سربار instructions کنترلی حلقه کاهش پیدا کنند.
- طراحی سادهتر زبان: اینکار به تیم طراحی زبان امکان نوشتن کدهای اضافهتری را میدهد که کار برنامه نویسها را کمتر میکند. برای مثال یک record واقعا چیزی نیست بجز یک کلاس پیاده سازی کنندهی IEquatable به صورت خودکار و در پشت صحنه.
Lowering چه زمانی رخ میدهد؟
Lowering جزئی از عملیات صورت گرفتهی در حین کامپایل است. زمانیکه دستور dotnet build را صادر میکنیم، ابتدا semantics & syntax analysis صورت میگیرد تا اگر برای مثال خطای دستوری وجود دارد، مشخص شود. سپس کدها به CIL یا Common intermediate language تبدیل میشوند. در حین این قسمت است که عملیات lowering نیز انجام میشود.
اگر علاقمند به مشاهدهی این کد #C ثانویهی تولید شدهی توسط کامپایلر هستید، میتوان از ابزار https://sharplab.io نیز استفاده کرد. برای مثال در سمت چپ آن کدهای زیر را قرار دهید:
using System; using System.Collections.Generic; var list = new List<int> { 1, 2 }; foreach(var item in list) Console.Write(item);
الگوی طراحی builder، برای ساختن اشیاء بسیار مفید است؛ اما پروسه ساختن اشیاء آن بسیار پیچده هست و به صورت معمول، این پروسه شامل چندین قسمت میشود.
در این مثال ما مشکلات ساختن شیء Person را مورد بررسی قرار میدهیم و این شیء از اشیایی کوچکتر مانند Name ، Surname و یا Primary Contact و غیره نیز تشکیل شده است.
class Person : IPerson { private string Name { get; } private string Surname { get; } private IContact PrimaryContact { get; set; } private IList<IContact> AllContacts { get; } public Person(string name, string surname, IContact primaryContact) { if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name)); if (string.IsNullOrEmpty(surname)) throw new ArgumentException(nameof(surname)); this.Name = name; this.Surname = surname; this.AllContacts = new List<IContact>(); this.SetPrimaryContact(primaryContact); } public void SetPrimaryContact(IContact contact) { this.AddContact(contact); this.PrimaryContact = contact; } public void AddContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.AllContacts.Add(contact); } }
همان طور که مشاهده میکنید، مقدار دهی شیء IContact پیچیدهتر از Name و Surname هست و روش اضافه کردن Contactها نیز بسیار پیچیده است؛ زیرا آنها به دو گروه PrimaryContact و Contacts تقسیم شدهاند.
شی Person شامل تعدای Contact مانند تلفن، ایمیل و یا هر چیزی دیگری میتواند باشد.
در این مثال ما دو نوع Contact داریم که به صورت زیر پیاده سازی شدهاند:
interface IContact { } class PhoneNumber : IContact { private string AreaCode { get; } private string Number { get; } public PhoneNumber(string areaCode, string number) { if (string.IsNullOrEmpty(areaCode)) throw new ArgumentException(nameof(areaCode)); if (string.IsNullOrEmpty(number)) throw new ArgumentException(nameof(number)); this.AreaCode = areaCode; this.Number = number; } } class EmailAddress : IContact { private string Address { get; } public EmailAddress(string address) { if (string.IsNullOrEmpty(address)) throw new ArgumentException(nameof(address)); this.Address = address; } }
به صورت کلی سه راه برای ساختن اشیاء وجود دارد:
1) استفاده از سازنده کلاس Person و سپس استفاده از متدهای AddContact و SetPrimaryContact برای ساختن شیء، به صورت کامل.
2) استفاده از Abstract Factory برای ساختن Person و سپس استفاده از متدهای AddContact و SetPrimaryContact برای ساختن شیء به صورت کامل.
3) استفاده از Builder برای ساختن شیء به صورت کامل و یکجا همراه با contactهای آن.
طراحی PersonBuilder :
interface IPerson { void SetPrimaryContact(IContact primaryContact); void AddContact(IContact contact); } interface IPersonBuilder { void SetName(string name); void SetSurname(string surname); void SetPrimaryContact(IContact primaryContact); void AddContact(IContact contact); IPerson Build(); }
class PersonBuilder: IPersonBuilder { private string Name { get; set; } private string Surname { get; set; } private IContact PrimaryContact { get; set; } private IList<IContact> OtherContacts { get; } = new List<IContact>(); public void SetName(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name)); this.Name = name; } public void SetSurname(string surname) { if (string.IsNullOrEmpty(surname)) throw new ArgumentException(nameof(surname)); this.Surname = surname; } public void SetPrimaryContact(IContact primaryContact) { if (primaryContact == null) throw new ArgumentNullException(nameof(primaryContact)); this.PrimaryContact = primaryContact; } public void AddContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.OtherContacts.Add(contact); } public IPerson Build() { IPerson person = new Person(this.Name, this.Surname, this.PrimaryContact); foreach (IContact contact in this.OtherContacts) person.AddContact(contact); return person; } }
خوب، اولین مشکلی که در این پیاده سازی مشهود است، مربوط به متد Build هست. اگر مقدارهای سازنده کلاس Person را به صورت null ارسال کنیم، باعث خطا میشود و این خطا به این خاطر نیست که ما مقدار Null را به کلاس PersonBuilder ارسال کردهایم؛ زیرا ما تمام متدهای Set را با استفاده NullGurd مورد حفاظت قرار دادهایم. مشکل اصلی از وضعیت داخلی شیء PersonBuilder هست. اگر متدهای Set را فراخوانی نکنیم، تمام فیلدهای خصوصی، مقدار null میگیرند و یکی از راههای رفع این مشکل این است که پارامترها را از طریق سازنده PersonBuilder مقدار دهی کنیم. ولی کمی بعدتر متوجه خواهیم شد که این پیاده سازی مانند کلاس person هست و در نتیجه این روش بی استفاده است.
راه حل: استفاده از Interface Segregation principle در PersonBuilder :
اصل ISP میگوید: "کلاینتها نباید وابسته به متدهایی باشند که آنها را پیاده سازی نمیکنند." برای رسیدن به این امر در مثال بالا، باید آن واسط را به واسطهای کوچکتری تقسیم کرد. این تقسیم بندی باید بر اساس استفاده کنندگان از واسطها صورت گیرد.
برای اینکه شیء Person را بسازیم، متوجه خواهید شد بعضی از دادهها الزامی و بعضی دیگر اختیاری هستند؛ مانند PrimaryContact که از دادههای ضروری شیء Person است. ولی AllContacts میتواند به صورت اختیاری تعریف شود و در پیاده سازی PersonBuilder بالا، کلاینت متوجه نخواهد شد کدام متد اختیاری یا اجباری هست و در نتیجه ممکن است فراموش کند متد SetPrimaryContact را فراخوانی کند و همین مساله باعث میشود تا نرم افزار با خطا مواجه شود.
راه حل: به کد زیر توجه فرمایید:
class PersonBuilder { private PersonBuilder() { } public static IExpectSurnamePersonBuilder WithName(string name) { ... } }
همانطور که مشاهده میفرمایید، سازنده کلاس به صورت خصوصی تعریف شدهاست. درنتیجه بیرون از کلاس نمیتوان از آن وهله ساخت و آنرا مورد استفاده قرار داد و تنها راه وهله سازی از کلاس PersonBuilder از طریق متد WithName خواهد بود. ثانیا این متد PersonBuilder را برنمیگرداند؛ بلکه شیءایی را برمیگرداند که منتظر فراهم کردن مقدار Surname است و با استفاده از این روش میتوانیم پروسه فراخوانی متدها را مشخص کنیم.
درنتیجه پروسه ساختن شیء، به چندین قسمت تقسیم شده که به صورت زیر میباشد:
1) فراهم کردن مقدار Surname
2) فراهم کردن مقدار Name
3) فراهم کردن مقدار PrimaryContact
4) فراهم کردن مقدار سایر Contactهای شخص
5) ساختن شیء Person
پس به ازای هر کدام از عملیاتها، یک اینترفیس خواهیم داشت:
interface IExpectSurnamePersonBuilder { IExpectPrimaryContactPersonBuilder WithSurname(string surname); } interface IExpectPrimaryContactPersonBuilder { IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact); } interface IExpectOtherContactsPersonBuilder { IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact); IPersonBuilder WithNoMoreContacts(); } interface IPersonBuilder { IPerson Build(); }
class PersonBuilder : IExpectSurnamePersonBuilder, IExpectPrimaryContactPersonBuilder, IExpectOtherContactsPersonBuilder, IPersonBuilder { private string Name { get; } private string Surname { get; set; } private IContact PrimaryContact { get; set; } private Person Person { get; set; } private PersonBuilder(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name)); this.Name = name; } public static IExpectSurnamePersonBuilder WithName(string name) { return new PersonBuilder(name); } public IExpectPrimaryContactPersonBuilder WithSurname(string surname) { if (string.IsNullOrEmpty(surname)) throw new ArgumentException(nameof(surname)); this.Surname = surname; return this; } public IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.Person = new Person(this.Name, this.Surname, contact); return this; } public IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.Person.AddContact(contact); return this; } public IPersonBuilder WithNoMoreContacts() { return this; } public IPerson Build() { return this.Person; } }
و اگر کلاینت بخواهد وهلهای را از کلاس PersonBuilder بسازد، به صورت زیر خواهد بود:
IPerson person = PersonBuilder .WithName("Ali") .WithSurname("Karimi") .WithPrimaryContact(new EmailAddress("admin@gmail.com")) .WithOtherContact(new EmailAddress("Test1@work.com")) .WithOtherContact(new EmailAddress("Test2@home.com")) .WithNoMoreContacts() .Build();
اصول طراحی ISP باعث میشوند، کد خواناتر شود و همین خوانایی سبب میگردد نگهداری و توسعه نرم افزار راحتتر شود.
چکیده:
ساختن اشیا در زبانهای object oriented کار بسیار سادهای است و همین سادگی، خطاهای جبران ناپذیری را به نرم افزار تحمیل میکنند و باعث ایجاد اشیایی ناپایدار در سیستم میشود. در اولین گام، الگوی طراحی Builder را به صورت ساده مورد بررسی قرار دادیم و در نهایت این طراحی را تا جای پیش بردیم که بتوانیم اشیایی پایدار را بسازیم. ولی این طراحی هنوز با مشکلاتی رو به رو هست؛ مانند نقض کردن قانون command query separation که این مشکل را در مقالهی بعدی برطرف خواهیم کرد.