Bootstrap has become the world’s favorite framework for building responsive web-projects. With the Bootstrap 4 Beta release just around the corner, it is time to take a more detailed look at what the project has to offer, what has changed and what one can expect when migrating over from Bootstrap 3.
معرفی پروژه DNTFrameworkCore
پروژه DNTFrameworkCore که قصد پشتیبانی از آن را دارم، یک زیرساخت سبک وزن و توسعه پذیر با پشتیبانی از طراحی چند مستاجری با کمترین وابستگی به کتابخانههای ثالث میباشد که با تمرکز بر کاهش زمان و افزایش کیفیت توسعه بخش منطق تجاری پروژههای تحت وب، توسعه داده شده است. به مرور زمان مطالب و مستندات آن نیز کامل خواهد شد. برای برخی از امکانات از جمله اعتبارسنجی خودکار، مدیریت تراکنش ها، شماره گذاری خودکار و ... آزمون واحد نیز در نظر گرفته شده است که در آینده نزدیک با تکمیل آزمون واحد بخشهای دیگر، انتشار آنها نیز انجام خواهد شد.
برای نصب و استفاده از بستههای نیوگت آن، دستورات زیر را اجرا کنید:
PM>Install-Package DNTFrameworkCore PM>Install-Package DNTFrameworkCore.EntityFramework PM>Install-Package DNTFrameworkCore.Web PM>Install-Package DNTFrameworkCore.Web.EntityFramework
به منظور بررسی دقیقتر امکانات آن میتوانید پروژه TestAPI موجود در مخزن گیت هاب را بررسی کنید.
نمونه API پیاده سازی شده:
[Route("api/[controller]")] public class TasksController : CrudController<ITaskService, int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel> { public TasksController(ITaskService service) : base(service) { } protected override string CreatePermissionName => PermissionNames.Tasks_Create; protected override string EditPermissionName => PermissionNames.Tasks_Edit; protected override string ViewPermissionName => PermissionNames.Tasks_View; protected override string DeletePermissionName => PermissionNames.Tasks_Delete; }
زمانی نیاز به این بازسازی کد بهوجود میآید که استفاده کنندهی از کلاسها، درگیر جزییات بیش از اندازهی کلاسها میشود. به طور مثال به نمودار بالا توجه نمایید.
در این نمودار تکه کدی مدل شده است که در آن ClientClass استفاده کننده از امکانات دو کلاس دیگر است. برای بدست آوردن مدیر یک شخص در این طراحی نیاز است ابتدا ClientClass اطلاعات مربوط به department یک شخص را با استفاده از متد GetDepartment بدست آورد. سپس با استفاده از متد GetManager در کلاس Department اقدام به دریافت اطلاعات مدیر نماید.
در طراحی بالا، برای دریافت اطلاعات مدیر یک فرد، با تکه کدی مانند زیر روبرو خواهیم شد:
var manager = person.GetDepartment().GetManager();
یکی از اصلیترین اصول طراحی کلاسها، کپسوله سازی اعضای کلاس، از استفاده کنندگان بیرونی آن است. کپسوله سازی به این معنی است که کلاس کمترین نیاز را به اطلاع از دیگر بخشهای سیستم داشته باشد. بنابراین در زمان تغییر آن بخشها دیگر نیازی نیست کلاس استفاده کننده از ریز تغییرات اطلاع پیدا کند.
روش کلی بازسازی کد پنهان سازی delegate، ایجاد یک کلاس (یا استفاده از کلاسهای موجود) به عنوان سرویس دهنده یا server است. در این کلاس به ازای کارکردهایی که نیاز به استفاده از چندین شیء یا متد را داشته باشند، یک متد ایجاد میکنیم. این متد روال لازم برای فراخوانیها را خود مدیریت و پیاده سازی میکند.
در مثال ذکر شدهی در ابتدای نوشتار میتوان کلاس سرویس دهندهی کارکرد دریافت مدیر را کلاس Person دانست. با این ترتیب بازسازی کد، رابطهی بین کلاسها را به صورت زیر تغییر میدهد.
با کمی توجه در نمودار میتوان متوجه شد متد موجود در کلاس Person به متد GetManager تغییر کرده است. زیرا در این کارکرد برای دریافت مدیر به کلاس Person رجوع میکنیم و نیازی نیست مستقیما به کلاس Department رجوع کنیم. مدیریت کردن نحوه دریافت مدیر یک Person نیز بر عهده این متد است.
همچنین برای دریافت مدیر یک شخص با چنین تکه کدی روبرو خواهیم شد:
var manager = person.GetManager();
همان طور که قبلا نیز ذکر شد، یکی از مزایای عمده این روش طراحی، مخفی کردن اطلاعات اضافی، از دید استفاده کنندگان کلاس است که در این مثال، نحوه دقیق دریافت مدیر است.
در کلاس ViewModelLocator ما تمام میانجی(Interface)ها و اشیا(Objects)ی مورد نیازمان را ثبت(register) میکنیم.
در ادامه اجزای مختلف آن را شرح میدهیم.
class ViewModelLocator { static ViewModelLocator() { ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); if (ViewModelBase.IsInDesignModeStatic) { SimpleIoc.Default.Register<IDataService, Design.DesignDataService>(); } else { SimpleIoc.Default.Register<IDataService, DataService>(); } SimpleIoc.Default.Register<MainViewModel>(); SimpleIoc.Default.Register<SecondViewModel>(); } public MainViewModel Main { get { return ServiceLocator.Current.GetInstance<MainViewModel>(); } } }
1) هر شیء که به صورت پیش فرض ایجاد میشود با الگوی Singlton ایجاد میشود.
SimpleIoc.Default.GetInstance<MainViewModel>(Guid.NewGuid().ToString());
SimpleIoc.Default.Register<IDataService, Design.DesignDataService>();
SimpleIoc.Default.Register<IDataService>(myObject);
SimpleIoc.Default.Register<MainViewModel>();
SimpleIoc.Default.GetInstance<IDataService>();
SimpleIoc.Default.GetInstance();
if (ViewModelBase.IsInDesignModeStatic) { SimpleIoc.Default.Register<IDataService, Design.DesignDataService>(); } else { SimpleIoc.Default.Register<IDataService, DataService>(); }
منبع
- شماره تلفن، به صورت رشته کاراکتری
- آدرس، به عنوان رشته کاراکتری
- نام مسئول رسیدگی به تیکت، به صورت رشته کاراکتری
اما بعد از سپری شدن مدتی از توسعه محصول ممکن است اقلام اطلاعاتی خاصی بر روی هر یک از آیتمهای بالا نیاز شود. به طور مثال برای آدرس نیاز باشد اطلاعات استان و شهر جداگانه قابل ذخیره سازی و گزارش گیری باشند و یا در کنار نام مسئول رسیدگی به تیکت، شماره تلفن او نیز وجود داشته باشد.
در چنین شرایطی، یک اقدام ممکن، افزودن اقلام اطلاعاتی مورد نیاز در همان مکان آیتم قبلی است؛ به طور مثال اگر نام مسئول بر روی موجودیت تیکت باشد، شماره تلفن مسئول نیز در همان موجودیت تیکت اضافه شود.
راه حل مناسبتر برای حل این نوع مشکلات ایجاد کلاس خاص آیتم اطلاعاتی و استفاده از شیء آن بهجای مقدار مربوطه است. به طور مثال به طراحی زیر دقت نمایید. در طراحی زیر کلاس دیگری به نام Agent ایجاد و در کلاس تیکت از آن استفاده کردهایم.
این بازسازی کد دو مزیت کلی دارد:
- راه را برای توسعه آینده آیتمهای دادهای باز میکند
- از تکرار آیتمهای دادهای جلوگیری میکند (به طور مثال زمانیکه از پایگاه دادههای رابطهای جهت ذخیره سازی، استفاده شود)
class Car { } class CarProducer { public void DeliverTo(int carsCount, string town) { Car[] cars = new Car[carsCount]; ... } }
class Transporter { public string Name { get; private set; } public Transporter(string name) { this.Name = name; } public void Deliver(Car[] cars, string town) { Console.WriteLine("Delivering {0} car(s) to {1} by {2}", cars.Length, town, this.Name); } }
static class TransporterLocator { static IList<Transporter> transporters = new List<Transporter>(); public static void Register(Transporter transporter) { transporters.Add(transporter); } public static Transporter Locate(string name) { return transporters .Where(transporter => transporter.Name == name) .Single(); } }
class CarProducer { public void DeliverTo(int carsCount, string town) { Car[] cars = new Car[carsCount]; Transporter transporter = null; if (carsCount <= 12) transporter = TransporterLocator.Locate("truck"); else transporter = TransporterLocator.Locate("train"); transporter.Deliver(cars, town); } }
TransporterLocator.Register(new Transporter("truck")); TransporterLocator.Register(new Transporter("train")); CarProducer producer = new CarProducer(); producer.DeliverTo(7, "Tehran"); producer.DeliverTo(74, "Tehran");
TransporterLocator.Register(new Transporter("truck")); CarProducer producer = new CarProducer(); producer.DeliverTo(7, "Tehran"); producer.DeliverTo(74, "Tehran");
class CarProducer { private Transporter truck; private Transporter train; public CarProducer(Transporter truck, Transporter train) { if (truck == null) throw new ArgumentNullException("truck"); if (train == null) throw new ArgumentNullException("train"); this.truck = truck; this.train = train; } public void DeliverTo(int carsCount, string town) { Car[] cars = new Car[carsCount]; Transporter transporter = this.truck; if (carsCount > 12) transporter = this.train; transporter.Deliver(cars, town); } }
آیا وضعیتی وجود دارد که در آن Service Locator یک راه حل قابل قبول باشد؟
در برخی موارد بجای اینکه وابستگیها را به صورت صریح قید کنیم، بهتر است از این الگو استفاده کنیم.
اصل چهارم: Starve for loosely coupled designs
"به دنبال طراحی با اتصال سست بین اجزا باش"
اتصال بین اجزای برنامه نویسی باعث سختتر شدن مدیریت تغییرات میشود؛ چرا که با تغییر یک بخش، بخشهای متصل نیز دچار مشکل خواهند شد. اتصالها از لحاظ نوع قدرت متفاوتند و اساسا سیستمی بدون اتصال وجود ندارد. لذا باید به دنبال یک طراحی با کمترین میزان قدرت اتصال یا همان سست اتصال باشیم.
تا به اینجا، اصلهای دوم و سوم ما را در کاهش وابستگی و اتصال قوی کمک کردهاند. استفاده از واسطها، باعث کاهش وابستگی به نوع پیاده سازی میشود. استفاده از ترکیب نیز به نوعی باعث از بین رفتن وابستگی قوی بین کلاسهای فرزند و کلاس والد میشود و با روشی دیگر (استفاده از شیء در برگرفته شده برای پیاده سازی وظیفهی تغییر کننده) وظایف را در کلاسها پیاده سازی میکند. در زیر نمونهی اتصال قوی و نتیجهی آن را میبینیم:
public class StrongCoupledConcreteA { public string GenerateString(string s) { return s + " from" + this.GetType().ToString(); } } public class StrongCoupledConcreteB { public void GenerateString(ref string s) { s += " from" + this.GetType().ToString(); } } public class Printer { bool condition; public Printer(bool cond) { condition = cond; } public void SetCondition(bool value) { condition = value; } public void Print() { string result; string input = " this message is"; if (condition) { var stringGenerator = new StrongCoupledConcreteA(); result = stringGenerator.GenerateString(input); } else { var stringGenerator = new StrongCoupledConcreteB(); result = input; stringGenerator.GenerateString(ref result); } Console.WriteLine(result); } } public class Context { Printer printer; public void DoWork() { printer = new Printer(true); printer.Print(); printer.SetCondition(false); printer.Print(); } }
حال کد بازنویسی شده را با آن مقایسه کنید:
public interface IStringGenerator { string GenerateString(string s); } public class LooslyCoupledConcreteA : IStringGenerator { public string GenerateString(string s) { return s + " from " + this.GetType().ToString(); } } public class LooslyCoupledConcreteB : IStringGenerator { public string GenerateString(string s) { return s + " from " + this.GetType().ToString(); } } public class Printer { bool condition; public Printer(bool cond) { condition = cond; } public void SetCondition(bool value) { condition = value; } public void Print() { string result; string input = " this message is"; IStringGenerator generator; if (condition) { generator = new LooslyCoupledConcreteA(); } else { generator = new LooslyCoupledConcreteB(); } result = generator.GenerateString(input); Console.WriteLine(result); } }
با کمی دقت مشاهده میکنیم که در کلاسهای strongly coupled با اینکه هدف هر دو کلاس تولید یک رشته است، ولی عدم وجود پروتکل باعث شده است نحوهی گرفتن ورودی و برگرداندن خروجی متفاوت شود و در نتیجه نیازمند به اضافه کردن پیچیدگی در کلاس فراخوانی کنندهی آنها میشویم. این در حالی است که در روش loosely coupled با ایجاد یک پروتکل (واسط IStringGenerator ) این پیچیدگی از بین رفته است. در اینجا نوع اتصال (وابستگی) از جنس اتصال (وابستگی) قوی به تعریف (prototype) و شاید به نوعی نحوهی پیاده سازی متد میباشد.
SOLID Principles *
پنج اصل بعدی به اصول SOLID معروف هستند.
S: Single Responsibility
O: Open/Closed
L: Liskov’s Substitution
I: Interface Segregation
D: Dependency Injection
اصل پنجم: Single responsibility
"به دنبال ماژولهای تک مسئولیتی باش"
در این قسمت مقصود از مسئولیت، «دلیلی است که کلاس باید تغییر کند» بدین معنا که اگر کلاسی با چند دلیل متفاوت مجبور به تغییر شود، آن کلاس چند مسئولیتی است. کلاسهای چند مسئولیتی عموما کد حجیمی دارند؛ نام آنها تعریف دقیقی را از مسئولیتشان ارائه نمیدهد و با عنوانی بسیار کلی نامگذاری میشوند و اشکال زدایی آنها بسیار طاقت فرساست. از طرفی، چند مسئولیتی بودن یک کلاس، باعث از بین رفتن مزایای توارث میشود. مثلا فرض کنید دو مسئولیت A,B در واسطی بیان میشوند که به یکدیگر مرتبط نبوده و مستقلند. برای مسئولیت A دو پیاده سازی و برای مسئولیت B، سه پیاده سازی در نظر گرفته شده است و جمعا برای پشتیبانی از تمامی حالات باید شش کلاس پیاده ساز، در نظر گرفته شود که توارث را سخت و بی معنی میکند زیرا قابلیت استفاده مجدد را از توارث سلب کرده است. با این وجود عملا رعایت همچین نکتهای در دنیای واقعی کار سختی است.
مثال زیر این مشکل را بیان میدارد:
// single responsibility principle - bad example interface IEmail { void SetSender(string sender); void SetReceiver(string receiver); void SetContent(string content); } class Email : IEmail { public void SetSender(string sender) { throw new NotImplementedException(); } public void SetReceiver(string receiver) { throw new NotImplementedException(); } public void SetContent(string content) { throw new NotImplementedException(); } }
در این مثال کلاس Email دارای دو مسئولیت (دلیل برای تغییر) است: الف- نحوه مقداردهی فرستنده و گیرنده براساس پروتکلهای مختلف مانند IMAP, POP3 ، بدین معنا که با تغییر پروتکل نیاز به تغییر پیاده سازی خواهیم شد. ب- تعریف محتوای پیام، بدین معنا که برای پشتیبانی از محتوای html, xml نیاز به تغییر کلاس Email داریم.
با تغییر طراحی خواهیم داشت:
// single responsibility principle - good example public interface IMessage { void SetSender(string sender); void SetReceiver(string receiver); void SetContent(IContent content); } public interface IContent { string GetAsString(); // used for serialization } public class Email : IMessage { public void SetSender(string sender) { throw new NotImplementedException(); } public void SetReceiver(string receiver) { throw new NotImplementedException(); } public void SetContent(IContent content) { throw new NotImplementedException(); } }
در اینجا واسط IContent مسئولیت پشتیبانی از xml, html را
خواهد داشت و نیازی به تغییر کلاس Email برای
پشتیبانی از این فرمتهای محتوای پیام را نخواهیم داشت.
اصل ششم: Open for
extension, close for modification : Open/Closed Principle
"پذیرای توسعه و
بازدارنده از تغییر هر آنچه که هست، باش"
ا ین اصل میگوید طراحی باید به گونهای باشد که با
اضافه شدن یک ویژگی، کدهای قبلی تغییری نکنند و فقط کدهای جدید برای پیاده سازی
ویژگی جدید نوشته شوند.
public class AreaCalculator { public double Area(object[] shapes) { double area = 0; foreach (var shape in shapes) { if (shape is Square) { Square square = (Square)shape; area += Math.Sqrt(square.Height); } if (shape is Triangle) { Triangle triangle = (Triangle)shape; double TotalHalf = (triangle.FirstSide + triangle.SecondSide + triangle.ThirdSide) / 2; area += Math.Sqrt(TotalHalf * (TotalHalf - triangle.FirstSide) * (TotalHalf - triangle.SecondSide) * (TotalHalf - triangle.ThirdSide)); } if (shape is Circle) { Circle circle = (Circle)shape; area += circle.Radius * circle.Radius * Math.PI; } } return area; } } public class Square { public double Height { get; set; } } public class Circle { public double Radius { get; set; } } public class Triangle { public double FirstSide { get; set; } public double SecondSide { get; set; } public double ThirdSide { get; set; } }
در اینجا کلاس AreaCalculator برای محاسبه مساحت تمام اشیاء ورودی، مساحت تک تک اشیاء را محاسبه میکند و نتیجه را برمیگرداند. در این مثال با اضافه شدن شکل هندسی جدید، باید کد این کلاس تغییر کند که با اصل Open/Closed مغایر است. برای بهبود این کد طراحی زیر پیشنهاد شده است:
public class AreaCalculator { public double Area(Shape[] shapes) { double area = 0; foreach (var shape in shapes) { area += shape.Area(); } return area; } } public abstract class Shape { public abstract double Area(); } public class Square : Shape { public double Height { get { return _height; } } private double _height; public Square(double Height) { _height = Height; } public override double Area() { return Math.Sqrt(_height); } } public class Circle : Shape { public double Radius { get { return _radius; } } private double _radius; public Circle(double Radius) { _radius = Radius; } public override double Area() { return _radius * _radius * Math.PI; } } public class Triangle : Shape { public double FirstSide { get { return _firstSide; } } public double SecondSide { get { return _secondSide; } } public double ThirdSide { get { return _thirdSide; } } private double _firstSide; private double _secondSide; private double _thirdSide; public Triangle(double FirstSide, double SecondSide, double ThirdSide) { _firstSide = FirstSide; _secondSide = SecondSide; _thirdSide = ThirdSide; } public override double Area() { double TotalHalf = (_firstSide + _secondSide + _thirdSide) / 2; return Math.Sqrt(TotalHalf * (TotalHalf - _firstSide) * (TotalHalf - _secondSide) * (TotalHalf - _thirdSide)); } }
در این طراحی، پیچیدگی محاسبه مساحت هر شکل به کلاس آن شکل منتقل شده است و با اضافه شدن شکل جدید نیازی به تغییر کلاس AreaCalculator نداریم.
در مقالهی بعدی به سه اصل دیگر اصول SOLID خواهم پرداخت.
رها سازی منابع IDisposable در StructureMap
x.For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use<MyContext>();
سؤال: برای سایر حالات چطور؟ در یک برنامهی ویندوزی کنسول یا سرویس ویندوز که Http Scoped در آن معنا ندارد چکار باید کرد؟
پاسخ: در اینجا حداقل دو راه حل وجود دارد:
الف) استفاده از nested containers
using (var container = ObjectFactory.Container.GetNestedContainer()) { var uow = container.GetInstance<IUnitOfWork>(); }
در مثال فوق، پس از پایان کار قطعهی using نوشته شده، به صورت خودکار کلیه اشیاء IDisposable یافت شده و Dispose میشوند.
ب) نگاهی به پشت صحنهی متد DisposeAndClearAll
اگر اشیاء IDisposable شما با طول عمر HybridHttpOrThreadLocalScoped معرفی شده باشند (و Transient نباشند)، با دستور ذیل چه در برنامههای ویندوزی و چه در برنامههای وب، کلیهی آنها یافت شده و به صورت خودکار Dispose میشوند:
new HybridLifecycle().FindCache(null).DisposeAndClear();
بنابراین به صورت خلاصه
اگر طول عمر شیء IDisposable مدنظر به صورت هیبرید تعریف شدهاست، از متد DisposeAndClear موجود در HybridLifecycle میتوان استفاده کرد. اگر طول عمر شیء IDisposable مورد استفاده، معمولی است و هیچ نوع caching خاصی برای آن درنظر گرفته نشدهاست، میتوان از روش nested containers برای رها سازی خودکار منابع آن کمک گرفت.
من تا به حال برنامه نویسهای زیادی را دیدهام که میپرسند «چه تفاوتی بین الگوهای معماری MVC و Three-Tier وجود دارد؟» قصد من روشن کردن این سردرگمی، بوسیله مقایسه هردو، با کنار هم قرار دادن آنها میباشد. حداقل در این بخش، من اعتقاد دارم، منبع بیشتر این سردرگمیها در این است که هر دوی آنها، دارای سه لایه متمایز و گره، در دیاگرام مربوطهاشان هستند.
اگر شما به دقت به دیاگرام آنها نگاه کنید، پیوستگی را خواهید دید. بین گرهها و راه اندازی آنها، کمی تفاوت است.
معماری سه لایه
سیستمهای سه لایه، واقعاً لایهها را میسازند: لایه UI به
لایه Business logic دسترسی دارد و لایه Business logic به
لایه Data دسترسی دارد. اما لایه UI دسترسی مستقیمی
به لایه Data ندارد و باید از طریق لایه Business logic و روابط آنها
عمل کند. بنابراین میتوانید فکر کنید که هر لایه، بعنوان یک جزء،
آزاد است؛ همراه با قوانین محکم طراحی دسترسی بین لایه ها.
MVC
در مقابل، اینPattern ، لایههای سیستم را نگهداری نمیکند. کنترلر به
مدل و View (برای انتخاب یا ارسال مقادیر) دسترسی
دارد. View نیز دسترسی دارد به مدل . دقیقاً چطور کار میکند؟
کنترلر در نهایت نقطه تصمیم گیری منطقی است. چه نوع منطقی؟ نوعاً، کنترلر، ساخت و تغییر مدل را در اکشنهای مربوطه، کنترل
خواهد کرد. کنترلر سپس تصمیم گیری میکند که برای
منطق داخلیش، کدام View مناسب
است. در آن نقطه، کنترلر مدل را به View ارسال میکند. من در اینجا چون هدف بحث مورد دیگهای میباشد،
مختصر توضیح دادم.
چه موقع و چه طراحی را انتخاب کنم؟
اول از همه، هر دو طراحی قطعاً و متقابلاً منحصر بفرد
نیستند. در واقع طبق تجربهی من، هر دو آنها کاملاً هماهنگ هستند. اغلب ما از معماری چند
لایه استفاده میکنیم مانند معماری سه لایه، برای یک ساختار معماری کلی. سپس من در
داخل لایه UI، از MVC استفاده میکنم، که در زیر دیاگرام آن را آورده
ام.