همانطور که قول داده بودم، به اصول GRASP میپردازیم.
اصول GRASP-General Responsibility Assignment
Software Principles
این اصول به بررسی نحوه تقسیم وظایف بین کلاسها و مشارکت اشیاء برای به انجام رساندن یک مسئولیت میپردازند. اینکه هر کلاس در ساختار نرم افزار چه وظیفهای دارد و چگونه با کلاسهای دیگر مشارکت میکند تا یک عملکرد به سیستم اضافه گردد. این اصول به چند بخش تقسیم میشوند:
- کنترلر ( Controller )
- ایجاد کننده ( Creator )
- انسجام قوی ( High Cohesion )
- واسطه گری ( Indirection )
- دانای اطلاعات ( Information Expert )
- اتصال ضعیف ( Low Coupling )
- چند ریختی ( Polymorphism )
- حفاظت از تاثیر تغییرات ( Protected Variations )
- مصنوع خالص ( Pure Fabrication )
Controller
این الگو بیان میکند که مسئولیت پاسخ به رویدادهای (Events ) یک سناریوی محدود مانند یک مورد کاربردی ( Use Case ) باید به عهده یک کلاس غیر UI باشد. کنترلر باید کارهایی را که نیاز است در پاسخ رویداد انجام شود، به دیگران بسپرد و نتایج را طبق درخواست رویداد بازگرداند. در اصل، کنترلر دریافت کننده رویداد، راهنمای مسیر پردازش برای پاسخ به رویداد و در نهایت برگرداننده پاسخ به سمت مبداء رویداد است. در زیر مثالی را میبینیم که رویداد اتفاق افتاده توسط واسط گرافیکی به سمت یک handler (که متدی است با ورودیِ فرستنده و آرگمانهای مورد نیاز) در کنترلر فرستاده میشود. این روش event handling، در نمونههای وب فرم و ویندوز فرم دیده میشود. به صورتی خود کلاسهای .Net وظیفه Event Raising از سمت UI با کلیک روی دکمه را انجام میدهد:
public class UserController { protected void OnClickCreate(object sender, EventArgs e) { // call validation services // call create user services } }
در مثال بعد عملیات مربوط به User در یک WebApiController پاسخ داده میشود. در اینجا به جای استفاده از Event Raising برای کنترل کردن رویداد، از فراخوانی یک متد در کنترلر توسط درخواست HttpPost انجام میگیرد. در اینجا نیاز است که در سمت کلاینت درخواستی را ارسال کنیم:
public class UserWebApiController { [HttpPost] public HttpResponseMessage Create(UserViewModel user) { // call validation services // call create user services } }
Creator :
این اصل میگوید شیء ای میتواند یک شیء دیگر را بسازد ( instantiate ) که: (اگر کلاس B بخواهد کلاس A را instantiate کند)
- کلاس B شیء از کلاس A را در خود داشته باشد؛
- یا اطلاعات کافی برای instantiate کردن از A را داشته باشد؛
- یا به صورت نزدیک با A در ارتباط باشد؛
- یا بخواهد شیء A را ذخیره کند.
از آنجایی که این اصل بدیهی به نظر میرسد، با مثال نقض، درک بهتری را نسبت به آن میتوان پیدا کرد:
// سازنده public class B { public static A CreateA(string name, string lastName, string job) { return new A() { Name =name, LastName = lastName, Job = job }; } } // ایجاد شونده public class A { public string Name { get; set; } public string LastName { get; set; } public string Job { get; set; } } public class Context { public void Main() { var name = "Rasoul"; var lastName = "Abbasi"; var job = "Developer"; var obj = B.CreateA(name, lastName, job); } }
و اما چرا این مثال، اصل Creator را نقض میکند. در مثال میبینید که کلاس B، یک شیء از نوع A را در متد Main کلاس Context ایجاد میکند. کلاس B فقط یک متد برای تولید A دارد و در عملیات تولید A هیچ منطق خاصی را پیاده سازی نمیکند.کلاس B شیء ای را از کلاس A ، در خود ندارد، با آن ارتباط نزدیک ندارد و آنرا ذخیره نمیکند. با اینکه کلاس B اطلاعات کافی را برای تولید A از ورودی میگیرد، ولی این کلاس Context است که اطلاعات کافی را ارسال مینماید. اگر در کلاس B منطقی اضافه بر instance گیریِ ساده وجود داشت (مانند بررسی صحت و اعتبار سنجی)، میتوانستیم بگوییم کلاس B از یک مجموعه عملیات instance گیری با خبر است که کلاس Context نباید از آن خبر داشته باشد. لذا اکنون هیچ دلیلی وجود ندارد که وظیفه تولید A را در Context انجام ندهیم و این مسئولیت را به کلاس B منتقل کنیم. این مورد ممکن است در ذهن شما با الگوی Factory تناقض داشته باشد. ولی نکته اصلی در الگو Factory انجام عملیات instance گیری با توجه به منطق برنامه است؛ یعنی وظیفهای که کلاس Context نباید از آن خبر داشته باشد را به کلاس Factory منتقل میکنیم. در غیر اینصورت ایجاد کلاس Factory بی معنا خواهد بود (مگر به عنوان افزایش انعطاف پذیری معماری که بتوان به راحتی نوع پیاده سازی یک واسط را تغییر داد).
High Cohesion :
این اصل اشاره به یکی از اصول اساسی طراحی نرم افزار دارد. انسجام واحدهای نرم افزاری باعث افزایش خوانایی، سهولت اشکال زدایی، قابلیت نگهداری و کاهش تاثیر زنجیرهای تغییرات میشود. طبق این اصل، مسئولیتهای هر واحد باید مرتبط باشد. لذا اجزایی کوچک با مسئولیتهای منسجم و متمرکز بهتر از اجزایی بزرگ با مسئولیتهای پراکنده است. اگر واحدهای سازنده نرم افزار انسجام ضعیفی داشته باشند، درک همکاریها، استفاده مجدد آنها، نگه داری نرم افزار و پاسخ به تغییرات سختتر خواهد شد.
در مثال زیر نقض این اصل را مشاهده میکنیم:
class Controller { public void CreateProduct(string name, int categoryId) { } public void EditProduct(int id, string name) { } public void DeleteProduct(int id) { } public void CreateCategory(string name) { } public void EditCategory(int id, string name) { } public void DeleteCategory(int id) { } }
همانطور که میبینید، کلاس
کنترلر ما، مسئولیت مدیریت Product و Category را بر عهده دارد. بزرگ شدن این کلاس، باعث سختتر شدن
خواندن کد و رفع اشکال میگردد. با جداسازی کنترلر مربوط به Product از Category میتوان انسجام را بالا برد.
Indirection :
این اصل بیان میکند که با تعریف یک واسط بین دو مولفه نرم افزاری میتوان میزان اتصال نرم افزار را کاهش داد. بدین ترتیب وظیفه هماهنگی ارتباط دو مؤلفه، به عهده این واسط خواهد بود و نیازی نیست دادههای ورودی و خروجی دو مؤلفه، هماهنگ باشند. در اینجا واسط، از وابستگی بین دو مؤلفه با پنهان کردن ضوابط هر مؤلفه از دیگری و ایجاد وابستگی ضعیف خود با دو مؤلفه، باعث کاهش اتصال کلی طراحی میگردد.
الگوهای Adapter و Delegate و همچنین نقش کنترلر در الگوی معماری MVC از این اصل پیروی میکنند.
class SenderA { public Mediator mediator { get; } public SenderA() { mediator = new Mediator(); } public void Send(string message, string reciever) { mediator.Send(message, reciever); } } class SenderB { public Mediator mediator { get; } public SenderB() { mediator = new Mediator(); } public void Send(string message) { } } public class RecieverA { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "create": break; case "delete": break; default: break; } } } public class RecieverB { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "edit": break; case "rollback": break; default: break; } } } class Mediator { internal void Send(string message, string reciever) { switch (reciever) { case "A": var recieverObjA = new RecieverA(); recieverObjA.DoAction(message); break; case "B": var recieverObjB = new RecieverB(); recieverObjB.DoAction(message); break; default: break; } } } class IndirectionContext { public void Main() { var senderA = new SenderA(); senderA.Send("rollback", "B"); var senderB = new SenderA(); senderB.Send("create", "A"); } }
در این مثال کلاس Mediator به عنوان واسط ارتباطی بین کلاسهای Sender و Receiver قرار گرفته و نقش تحویل پیغام را دارد.
در مقاله بعدی، به بررسی سایر اصول GRASP خواهم پرداخت.
بررسی وضعیت کتابخانهی Moq
Moq is a mocking library for .NET Unit Testing (cue the TDD folks reminding us mocks are unnecessary), and it is by far the most widely used mocking library in .NET (475 million downloads vs 87 million for the next largest, NSubstitute). Yesterday, its author released version 4.20.1; which added nagware and a backdoor to Moq, in a bid to drive up paid usages of Moq through ‘Sponsorships’.
بررسی معماری Stack Overflow
In the recent interview with Scott Hanselman, Roberta Arcoverde, Head of Engineering at Stack Overflow, revealed the story about the architecture of Stack Overflow. They handle more than 6000 requests per second, 2 billion page views per month, and they manage to render a page in about 12 milliseconds. We imagine they use a microservice solution running in the Cloud with Kubernetes.
Component architectures are an important part of ever modern front-end framework. In this article, I’m going to dissect Polymer, React, Rio.js, Vue.js, Aurelia and Angular 2 components. The goal is to make the commonalities between each solution obvious. Hopefully, this will convince you that learning one or the other isn’t all that complex, given that everyone has somewhat settled on a component architecture.
آشنایی با نحوه ایجاد یک IoC Container
IoC Container چیست؟
IoC Container، فریم ورکی است برای انجام تزریق وابستگیها. در این فریم ورک امکان تنظیم اولیه وابستگیهای سیستم وجود دارد. برای مثال زمانیکه برنامه از یک IoC Container، نوع اینترفیس خاصی را درخواست میکند، این فریم ورک با توجه به تنظیمات اولیهاش، کلاسی مشخص را بازگشت خواهد داد.
IoC Containerهای قدیمیتر، برای انجام تنظیمات اولیه خود از فایلهای کانفیگ استفاده میکردند. نمونههای جدیدتر آنها از روشهای Fluent interfaces برای مشخص سازی تنظیمات خود بهره میبرند.
زمانیکه از یک IOC Container در کدهای خود استفاده میکنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست میکند. عموما اینکار با درخواست یک اینترفیس صورت میگیرد؛ هرچند محدودیتی نیز نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
در این بین زنجیرهی وابستگیهای مورد نیاز نیز وهله سازی خواهند شد. برای مثال اگر وابستگی اول به وابستگی دوم برای وهله سازی نیاز دارد، کار وهله سازی وابستگیهای وابستگی دوم نیز به صورت خودکار انجام خواهند شد. (این موردی است که بسیاری از تازه واردان به این بحث تا یکبار آنرا امتحان نکنند باور نخواهند کرد!)
ج) سپس کد فراخوان وهله دریافتی را مورد پردازش قرار داده و سپس شروع به استفاده از متدها و خواص آن خواهد نمود.
در تصویر فوق محل قرارگیری یک IOC Container را مشاهده میکنید. یک IOC Container در مورد تمام وابستگیهای مورد نیاز، اطلاعات لازم را دارد. همچنین این فریم ورک در مورد کلاسی که قرار است از وابستگیهای سیستم استفاده نماید نیز مطلع است؛ به این ترتیب میتواند به صورت خودکار در زمان وهله سازی آن، نوعهای وابستگیهای مورد نیاز آنرا در اختیارش قرار دهد.
برای مثال در اینجا MyClass، وابستگی مشخص شده در سازنده خود را به نام IDependency از IOC Container درخواست میکند. سپس این IOC Container بر اساس تنظیمات اولیه خود، یکی از وابستگیهای A یا B را بازگشت خواهد داد.
آغاز به کار ساخت یک IOC Container نمونه
در ابتدا کدهای آغازین مثال بحث جاری را در نظر بگیرید:
using System; namespace DI01 { public interface ICreditCard { string Charge(); } public class Visa : ICreditCard { public string Charge() { return "Charging with the Visa!"; } } public class MasterCard : ICreditCard { public string Charge() { return "Swiping the MasterCard!"; } } public class Shopper { private readonly ICreditCard creditCard; public Shopper(ICreditCard creditCard) { this.creditCard = creditCard; } public void Charge() { var chargeMessage = creditCard.Charge(); Console.WriteLine(chargeMessage); } } }
var shopper = new Shopper(new Visa()); shopper.Charge();
using System; using System.Collections.Generic; using System.Linq; namespace DI01 { public class Resolver { //کار ذخیره سازی و نگاشت از یک نوع به نوعی دیگر در اینجا توسط این دیکشنری انجام خواهد شد private Dictionary<Type, Type> dependencyMap = new Dictionary<Type, Type>(); /// <summary> /// یک نوع خاص از آن درخواست شده و سپس بر اساس تنظیمات برنامه، کار وهله سازی /// نمونه معادل آن صورت خواهد گرفت /// </summary> public T Resolve<T>() { return (T)Resolve(typeof(T)); } private object Resolve(Type typeToResolve) { Type resolvedType; // ابتدا بررسی میشود که آیا در تنظیمات برنامه نگاشت متناظری برای نوع درخواستی وجود دارد؟ if (!dependencyMap.TryGetValue(typeToResolve, out resolvedType)) { //اگر خیر، کار متوقف خواهد شد throw new Exception(string.Format("Could not resolve type {0}", typeToResolve.FullName)); } var firstConstructor = resolvedType.GetConstructors().First(); var constructorParameters = firstConstructor.GetParameters(); // در ادامه اگر این نوع، دارای سازندهی بدون پارامتری است // بلافاصله وهله سازی خواهد شد if (!constructorParameters.Any()) return Activator.CreateInstance(resolvedType); var parameters = new List<object>(); foreach (var parameterToResolve in constructorParameters) { // در اینجا یک فراخوانی بازگشتی صورت گرفته است برای وهله سازی // خودکار پارامترهای مختلف سازنده یک کلاس parameters.Add(Resolve(parameterToResolve.ParameterType)); } return firstConstructor.Invoke(parameters.ToArray()); } public void Register<TFrom, TTo>() { dependencyMap.Add(typeof(TFrom), typeof(TTo)); } } }
var resolver = new Resolver(); //تنظیمات اولیه resolver.Register<Shopper, Shopper>(); resolver.Register<ICreditCard, Visa>(); //تزریق وابستگیها و وهله سازی var shopper = resolver.Resolve<Shopper>(); shopper.Charge();
ابتدا کار تعاریف نگاشتهای اولیه انجام میشود. در این صورت زمانیکه متد Resolve فراخوانی میگردد، نوع درخواستی آن به همراه سازنده دارای آرگومانی از نوع ICreditCard وهله سازی شده و بازگشت داده خواهد شد. سپس با در دست داشتن یک وهله آماده، متد Charge آنرا فراخوانی خواهیم کرد.
بررسی نحوه استفاده از Microsoft Unity به عنوان یک IoC Container
Unity چیست؟
Unity یک فریم ورک IoC Container تهیه شده توسط مایکروسافت میباشد که آنرا به عنوان جزئی از Enterprise Library خود قرار داده است. بنابراین برای دریافت آن یا میتوان کل مجموعه Enterprise Library را دریافت کرد و یا به صورت مجزا به عنوان یک بسته نیوگت نیز قابل تهیه است.
برای این منظور در خط فرمان پاورشل نیوگت در VS.NET دستور ذیل را اجرا کنید:
PM> Install-Package Unity
پیاده سازی مثال خریدار توسط Unity
همان مثال قسمت قبل را درنظر بگیرید. قصد داریم اینبار بجای IoC Container دست سازی که تهیه شد، پیاده سازی آنرا به کمک MS Unity انجام دهیم.
using Microsoft.Practices.Unity; namespace DI02 { class Program { static void Main(string[] args) { var container = new UnityContainer(); container.RegisterType<ICreditCard, MasterCard>(); var shopper = container.Resolve<Shopper>(); shopper.Charge(); } } }
مطابق کدهای فوق، ابتدا تنظیمات IoC Container انجام شده است. به آن اعلام کردهایم که در صورت نیاز به ICreditCard، نوع MasterCard را یافته و وهله سازی کن. با این تفاوت که Unity هوشمندتر بوده و سطر مربوط به ثبت کلاس Shoper ایی را که در قسمت قبل انجام دادیم، در اینجا حذف شده است.
سپس به این IoC Container اعلام کردهایم که نیاز به یک وهله از کلاس خریدار داریم. در اینجا Unity کار وهله سازیهای خودکار وابستگیها و تزریق آنها را در سازنده کلاس خریدار انجام داده و نهایتا یک وهله قابل استفاده را در اختیار ادامه برنامه قرار خواهد داد.
یک نکته:
به صورت پیش فرض کار تزریق وابستگیها در سازنده کلاسها به صورت خودکار انجام میشود. اگر نیاز به Setter injection و مقدار دهی خواص کلاس وجود داشت میتوان به نحو ذیل عمل کرد:
container.RegisterType<ICreditCard, MasterCard>(new InjectionProperty("propertyName", 5));
مدیریت طول عمر اشیاء در Unity
توسط یک IoC Container میتوان یک وهله معمولی از شیءایی را درخواست کرد و یا حتی طول عمر این وهله را به صورت Singleton معرفی نمود (یک وهله در طول عمر کل برنامه). در Unity اگر تنظیم خاصی اعمال نشود، هربار که متد Resolve فراخوانی میگردد، یک وهله جدید را در اختیار ما قرار خواهد داد. اما اگر پارامتر متد RegisterType را با وهلهای از ContainerControlledLifetimeManager مقدار دهی کنیم:
container.RegisterType<ICreditCard, MasterCard>(new ContainerControlledLifetimeManager());
حالت پیش فرض مورد استفاده، بدون ذکر پارامتر متد RegisterType، مقدار TransientLifetimeManager میباشد.