*یکی از پیش نیازهای این پست مطالعه این دو مطلب (^ ) و (^ ) میباشد.
فرض میکنیم در دیتابیس مورد نظر یک Store به همراه یک جدول به صورت زیر داریم:
[Entity] public interface IBook { [Identifier] string Id { get; } string Title { get; set; } string Isbn { get; set; } }
بعد از انخاب گزینه بالا یک فایل با پسوند tt به پروژه اضافه خواهد شد که وظیفه آن جستجو در اسمبلی مورد نظر و پیدا کردن تمام اینترفیس هایی که دارای EntityAttribute هستند و همچنین ایجاد کلاسهای متناظر جهت پیاده سازی اینترفیسهای بالا است. در نتیجه ساختار پروژه تا این جا به صورت زیر خواهد شد.
واضح است که فایلی به نام Book به عنوان پیاده سازی مدل IBook به عنوان زیر مجموعه فایل DatabaseContext.tt به پروژه اضافه شده است.
تا اینجا برای استفاده از Context مورد نظر باید به صورت زیر عمل نمود:
DatabaseContext context = new DatabaseContext(); context.Books.Add(new Book());
public interface IUnitOfWork { BrightstarEntitySet<T> Set<T>() where TEntity : class; void DeleteObject(object obj); void SaveChanges(); }
نکته: برای حذف یک آبجکت از Store، باید از متد DeleteObject تعبیه شده در Context استفاده نماییم. در نتیجه متد مورد نظر نیز در اینترفیس بالا در نظر گرفته شده است.
استفاده از IOC Container جهت رجیستر کردن IUnitOfWrok
در این قدم باید IUnitOfWork را در یک IOC container رجیستر کرده تا در جای مناسب عملیات وهله سازی از آن میسر باشد. من در اینجا از Castle Windsor Container استفاده کردم. کلاس زیر این کار را برای ما انجام خواهد داد:
public class DependencyResolver { public static void Resolve(IWindsorContainer container) { var context = new DatabaseContext("type=embedded;storesdirectory=c:\brightstar;storename=test "); container.Register(Component.For<IUnitOfWork>().Instance(context).LifestyleTransient()); } }
public class BookService { public BookService(IUnitOfWork unitOfWork) { UnitOfWork = unitOfWork; } public IUnitOfWork UnitOfWork { get; private set; } public IList<IBook> GetAll() { return UnitOfWork.Set<IBook>().ToList(); } public void Add() { UnitOfWork.Set<IBook>().Add(new Book()); } public void Remove(IBook entity) { UnitOfWork.DeleteObject(entity); } }
نکته: در حال حاضر امکان جداسازی مدلهای برنامه (تعاریف اینترفیس) در قالب یک پروژه دیگر(نظیر مدل CodeFirst در EF) در B*Db امکان پذیر نیست.
نکته : برای اضافه کردن آیتم جدید به Store نیاز به وهله سازی از اینترفیس IBook داریم. کلاس Book ساخته شده توسط DatabaseContext.tt در عملیات Insert و update کاربرد خواهد داشت.
کمپین ضد IF !
بکارگیری بیش از حد If و خصوصا Switch برخلاف اصول طراحی شیءگرا است؛ تا این حد که یک کمپین ضد IF هم وجود دارد!
البته سایت فوق بیشتر جنبه تبلیغی برای سمینارهای گروه مذکور را دارد تا اینکه جنبهی آموزشی/خود آموزی داشته باشد.
یک مثال کاربردی:
فرض کنید دارید یک سیستم گزارشگیری را طراحی میکنید. به جایی میرسید که نیاز است با Aggregate functions سروکار داشته باشید؛ مثلا جمع مقادیر یک ستون را نمایش دهید یا معدل امتیازهای نمایش داده شده را محاسبه کنید و امثال آن. طراحی متداول آن به صورت زیر خواهد بود:
using System.Collections.Generic;
using System.Linq;
namespace CircularDependencies
{
public enum AggregateFunc
{
Sum,
Avg
}
public class AggregateFuncCalculator
{
public decimal Calculate(IList<decimal> list, AggregateFunc func)
{
switch (func)
{
case AggregateFunc.Sum:
return getSum(list);
case AggregateFunc.Avg:
return getAvg(list);
default:
return 0m;
}
}
private decimal getAvg(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum() / list.Count;
}
private decimal getSum(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum();
}
}
}
در کلاس AggregateFuncCalculator یک متد Calculate داریم که توسط آن قرار است روی list دریافتی یک سری عملیات انجام شود. عملیات پشتیبانی شده هم توسط یک enum معرفی شده؛ برای مثال اینجا فقط جمع و میانگین پشتیبانی میشوند.
و مشکل طراحی این کلاس، همان switch است که برخلاف اصول طراحی شیءگرا میباشد. یکی از اصول طراحی شیءگرا بر این مبنا است که:
یک کلاس باید جهت تغییر، بسته اما جهت توسعه، باز باشد.
یعنی چی؟
داستان طراحی Aggregate functions که فقط به جمع و میانگین خلاصه نمیشود. امروز میگویند واریانس چطور؟ فردا خواهند گفت حداقل و حداکثر چطور؟ پس فردا ...
به عبارتی این کلاس جهت تغییر بسته نیست و هر روز باید بر اساس نیازهای جدید دستکاری شود.
چکار باید کرد؟
آیا میتوانید در کلاس AggregateFuncCalculator یک الگوی تکراری را تشخیص دهید؟ الگوی تکراری موجود، محاسبات بر روی یک لیست است. پس میشود بر اساس آن یک اینترفیس عمومی را تعریف کرد:
public interface IAggregateFunc
{
decimal Calculate(IList<decimal> list);
}
اکنون هر کدام از پیاده سازیهای موجود در کلاس AggregateFuncCalculator را به یک کلاس جدا منتقل خواهیم کرد تا یک اصل دیگر طراحی شیءگرا نیز محقق شود:
هر کلاس باید تنها یک کار را انجام دهد.
public class Sum : IAggregateFunc
{
public decimal Calculate(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum();
}
}
public class Avg : IAggregateFunc
{
public decimal Calculate(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum() / list.Count;
}
}
تا اینجا 2 هدف مهم حاصل شده است:
- کم کم کلاس AggregateFuncCalculator دارد خلوت میشود. قرار است هر کلاس یک کار را بیشتر انجام ندهد.
- برنامه از بسته بودن جهت توسعه هم خارج شده است (یکی دیگر از اصول طراحی شیءگرا). اگر تعاریف توابع محاسباتی را تماما در یک کلاس قرار دهیم صاحب اول و آخر آن کتابخانه خودمان خواهیم بود. این کلاس بسته است جهت تغییر. اما با معرفی IAggregateFunc، من امروز 2 تابع را تعریف کردهام، شما فردا توابع خاص خودتان را تعریف کنید. باز هم برنامه کار خواهد کرد. نیازی نیست تا من هر روز یک نگارش جدید از کتابخانه را ارائه دهم که در آن فقط یک تابع دیگر اضافه شده است.
اکنون یکی از چندین و چند روش بازنویسی کلاس AggregateFuncCalculator به صورت زیر میتواند باشد
public class AggregateFuncCalculator
{
public decimal Calculate(IList<decimal> list, IAggregateFunc func)
{
return func.Calculate(list);
}
}
بله! دیگر سوئیچی در کار نیست. این کلاس تنها یک کار را انجام میدهد. همچنین دیگر نیازی به تغییر هم ندارد (محاسبات از آن خارج شده) و باز است جهت توسعه (شما نگارشهای دلخواه IAggregateFunc دیگر خود را توسعه داده و استفاده کنید).
پایه و اساس
عموما این باور وجود دارد که با استفاده از الگوی Repository میتوانید (در مجموع) دسترسی به دادهها را از لایه دامنه (Domain) تفکیک کنید و "دادهها را بصورت سازگار و استوار عرضه کنید".اگر به هر کدام از پیاده سازیهای الگوی Repository در کنار (UnitOfWork (EF دقت کنید خواهید دید که تفکیک (decoupling) قابل ملاحظه ای وجود ندارد.
using System; using System.Collections.Generic; using System.Linq; using System.Data; using ContosoUniversity.Models; namespace ContosoUniversity.DAL { public class StudentRepository : IStudentRepository, IDisposable { private SchoolContext context; public StudentRepository(SchoolContext context) { this.context = context; } public IEnumerable<Student> GetStudents() { return context.Students.ToList(); } public Student GetStudentByID(int id) { return context.Students.Find(id); } //<snip> public void Save() { context.SaveChanges(); } } }
این کلاس بدون SchoolContext نمیتواند وجود داشته باشد، پس دقیقا چه چیزی را در اینجا decouple کردیم؟ هیچ چیز را!
در این قطعه کد - از MSDN - چیزی که داریم یک پیاده سازی مجدد از LINQ است که مشکل کلاسیک Repository APIهای بی انتها را بدست میدهد. منظور از Repository APIهای بی انتها، متدهای جالبی مانند GetStudentById, GetStudentByBirthday, GetStudentByOrderNumber و غیره است.
اما این مشکل اساسی نیست. مشکل اصلی روتین ()Save است. این متد یک دانش آموز (Student) را ذخیره میکند .. اینطور بنظر میرسد. دیگر چه چیزی را ذخیره میکند؟ آیا میتوانید حدس بزنید؟ من که نمیتوانم .. بیشتر در ادامه.
UnitOfWork تراکنشی است
یک UnitOfWork همانطور که از نامش بر میآید برای انجام کاری وجود دارد. این کار میتواند به سادگی واکشی اطلاعات و نمایش آنها، و یا به پیچیدگی پردازش یک سفارش جدید باشد. هنگامی که شما از EntityFramework استفاده میکنید و یک DbContext را وهله سازی میکنید، در واقع یک UnitOfWork میسازید.در EF میتوانید با فراخوانی ()SubmitChanges تمام تغییرات را فلاش کرده و بازنشانی کنید (flush and reset). این کار بیتهای مقایسه change tracker را تغییر میدهد. افزودن رکوردهای جدید، بروز رسانی و حذف آنها. هر چیزی که تعیین کرده باشید. و تمام این دستورات در یک تراکنش یا Transaction انجام میشوند.
یک Repository مطلقا یک UnitOfWork نیست
هر متد در یک Repository قرار است فرمانی اتمی (Atomic) باشد - چه واکشی اطلاعات و چه ذخیره آنها. مثلا میتوانید یک Repository داشته باشید با نام SalesRepository که اطلاعات کاتالوگ شما را واکشی میکند، و یا یک سفارش جدید را ثبت میکند. منظور از فرمانهای اتمیک این است، که هر متد تنها یک دستور را باید اجرا کند. تراکنشی وجود ندارد و امکاناتی مانند ردیابی تغییرات و غیره هم جایی ندارند.
یکی دیگر از مشکلات استفاده از Repositoryها این است که بزودی و به آسانی از کنترل خارج میشوند و نیاز به ارجاع دیگر مخازن پیدا میکنند. به دلیل اینکه مثلا نمیدانستید که SalesRepository نیاز به ارجاع ReportRepository داشته است (یا چیزی مانند این).
این مشکل به سرعت مشکل ساز میشود، و نیز به همین دلیل است که به UnitOfWork تمایل پیدا میکنیم.
بدترین کاری که میتوانید انجام دهید: <Repository<T
این الگو دیوانه وار است. این کار عملا انتزاعی از یک انتزاع دیگر است (abstraction of an abstraction). به قطعه کد زیر دقت کنید، که به دلیلی نامشخص بسیار هم محبوب است.public class CustomerRepository : Repository < Customer > { public CustomerRepository(DbContext context){ //a property on the base class this.DB = context; } //base class has Add/Save/Remove/Get/Fetch }
در نگاه اول شاید بگویید مشکل این کلاس چیست؟ همه چیز را کپسوله میکند و کلاس پایه Repository هم به کانتکست دسترسی دارد. پس مشکل کجاست؟
مشکلات عدیده اند .. بگذارید نگاهی بیاندازیم.
آیا میدانید این DbContext از کجا آمده است؟
خیر، نمیدانید. این آبجکت به کلاس تزریق (Inject) میشود، و نمیدانید که چه متدی آن را باز کرده و به چه دلیلی. ایده اصلی پشت الگوی Repository استفاده مجدد از کد است. بدین منظور که مثلا برای عملیات CRUD از کلاسی پایه استفاده کنید تا برای هر موجودیت و فرمی نیاز به کدنویسی مجدد نباشد. برگ برنده این الگو نیز دقیقا همین است. مثلا اگر بخواهید از کدی در چند فرم مختلف استفاده کنید از این الگو استفاده میشد.
الگوی UnitOfWork همه چیز در نامش مشخص است. اگر قرار باشد آنرا بدین شکل تزریق کنید، نمیتوانید بدانید که از کجا آمده است.
شناسه مشتری جدید را نیاز داشتم
کد بالا در CustomerRepository را در نظر بگیرید - که یک مشتری جدید را به دیتابیس اضافه میکند. اما CustomerID جدید چه میشود؟ مثلا به این شناسه نیاز دارید تا یک log بسازید. چه میکنید؟ گزینههای شما اینها هستند:
- متد ()SubmitChanges را صدا بزنید تا تغییرات ثبت شوند و بتوانید به CustomerID جدید دسترسی پیدا کنید
- CustomerRepository خود را باز کنید و متد پایه Add را بازنویسی (override) کنید. بدین منظور که پیش از بازگشت دادن، متد ()SubmitChanges را فراخوانی کند. این راه حلی است که MSDN به آن تشویق میکند، و بمبی ساعتی است که در انتظار انفجار است
- تصمیم بگیرید که تمام متدهای Add/Remove/Save در مخازن شما باید ()SubmitChanges را فراخوانی کنند
مشکل را میبینید؟ مشکل در خود پیاده سازی است. در نظر بگیرید که چرا New Customer ID را نیاز دارید؟ احتمالا برای استفاده از آن در ثبت یک سفارش جدید، و یا ثبت یک ActivityLog.
اگر بخواهیم از StudentRepository بالا برای ایجاد دانش آموزان جدید پس از خرید آنها از فروشگاه کتاب مان استفاده کنیم چه؟ اگر DbContext خود را به مخزن تزریق کنید و دانش آموز جدید را ذخیره کنید .. اوه .. تمام تراکنش شما فلاش شده و از بین رفته!
حالا گزینههای شما اینها هستند: 1) از StudentRepository استفاده نکنید (از OrderRepository یا چیز دیگری استفاده کنید). و یا 2) فراخوانی ()SubmitChanges را حذف کنید و به باگهای متعددی اجازه ورود به کد تان را بدهید.
اگر تصمیم بگیرید که از StudentRepository استفاده نکنید، حالا کدهای تکراری (duplicate) خواهید داشت.
شاید بگویید که برای دستیابی به شناسه رکورد جدید نیازی به ()SubmitChanges نیست، چرا که خود EF این عملیات را در قالب یک تراکنش انجام میدهد!
دقیقا درست است، و نکته من نیز همین است. در ادامه به این قسمت باز خواهیم گشت.
متدهای Repositories قرار است اتمیک باشند
به هر حال تئوری اش که چنین است. چیزی که در Repositoryها داریم حتی اصلا Repository هم نیست. بلکه یک abstraction برای عملیات CRUD است که هیچ کاری مربوط به منطق تجاری اپلیکیشن را هم انجام نمیدهد. مخازن قرار است روی دستورات مشخصی تمرکز کنند (مثلا ثبت یک رکورد یا واکشی لیستی از اطلاعات)، اما این مثالها چنین نیستند.
همانطور که گفته شده استفاده از چنین رویکردهایی به سرعت مشکل ساز میشوند و با رشد اپلیکیشن شما نیز مشکلات عدیده ای برایتان بوجود میآروند.
خوب، راه حل چیست؟
برای جلوگیری از این abstractionهای غیر منطقی دو راه وجود دارد. اولین راه استفاده از Command/Query Separation است که ممکن است در ابتدا کمی عجیب و بنظر برسند اما لازم نیست کاملا CQRS را دنبال کنید. تنها از سادگی انجام کاری که مورد نیاز است لذت ببرید، و نه بیشتر.
آبجکتهای Command/Query
Jimmy Bogard مطلب خوبی در اینباره نوشته است و با تغییراتی جزئی برای بکارگیری Properties کدی مانند لیست زیر خواهیم داشت. مثلا برای مطالعه بیشتر درباره آبجکتهای Command/Query به این لینک سری بزنید.
public class TransactOrderCommand { public Customer NewCustomer {get;set;} public Customer ExistingCustomer {get;set;} public List<Product> Cart {get;set;} //all the parameters we need, as properties... //... //our UnitOfWork StoreContext _context; public TransactOrderCommand(StoreContext context){ //allow it to be injected - though that's only for testing _context = context; } public Order Execute(){ //allow for mocking and passing in... otherwise new it up _context = _context ?? new StoreContext(); //add products to a new order, assign the customer, etc //then... _context.SubmitChanges(); return newOrder; } }
DataContext خود را در آغوش بگیرید
ایده ای که در ادامه خواهید دید را شخصا بسیار میپسندم (که توسط Ayende معرفی شد). چیزهایی که به آنها نیاز دارید را در قالب یک فیلتر wrap کنید و یا از یک کلاس کنترلر پایه استفاده کنید (با این فرض که از اپلیکیشنهای وب استفاده میکنید).using System; using System.Web.Mvc; namespace Web.Controllers { public class DataController : Controller { protected StoreContext _context; protected override void OnActionExecuting(ActionExecutingContext filterContext) { //make sure your DB context is globally accessible MyApp.StoreDB = new StoreDB(); } protected override void OnActionExecuted(ActionExecutedContext filterContext) { MyApp.StoreDB.SubmitChanges(); } } }
این کار به شما اجازه میدهد که از DataContext خود در خلال یک درخواست واحد (request) استفاده کنید. تنها کاری که باید بکنید این است که از این کلاس پایه ارث بری کنید. این بدین معنا است که هر درخواست به اپلیکیشن شما یک UnitOfWork خواهد بود. که بسیار هم منطقی و قابل قبول است. در برخی موارد هم شاید این فرض درست یا کارآمد نباشد، که در این هنگام میتوانید از آبجکتهای Command/Query استفاده کنید.
ایدههای بعدی: چه چیزی بدست آوردیم؟
چیزهای متعددی بدست آوردیم.- تراکنشهای روشن و صریح: دقیقا میدانیم که DbContext ما از کجا آمده و در هر مرحله روی چه UnitOfWork ای کار میکنیم. این امر هم الان، و هم در آینده بسیار مفید خواهد بود
- انتزاع کمتر == شفافیت بیشتر: ما Repositoryها را از دست دادیم، که دلیلی برای وجود داشتن نداشتند. به جز اینکه یک abstraction از abstraction دیگر باشند. رویکرد آبجکتهای Command/Query تمیزتر است و دلیل وجود هرکدام و مسئولیت آنها نیز روشنتر است
- شانس کمتر برای باگ ها: رویکردهای مبتنی بر Repository باعث میشوند که با تراکنشهای ناموفق یا پاره ای (partially-executed) مواجه شویم که نهایتا به یکپارچگی و صحت دادهها صدمه میزند. لازم به ذکر نیست که خطایابی و رفع چنین مشکلاتی شدیدا زمان بر و دردسر ساز است
برای مطالعه بیشتر
ایجاد Repositories بر روی UnitOfWork
به الگوی Repository در لایه DAL خود نه بگویید!
پیاده سازی generic repository یک ضد الگو است
نگاهی به generic repositories
بدون معکوس سازی وابستگیها، طراحی چند لایه شما ایراد دارد
تگ گذاری در کامنتها
TODO : معروفترین تگ شناخته شدهاست و میتوان گفت در اکثر اوقات به جای بقیه هم استفاده میشود. چون معنای دیگر کامنتها را نیز در بر میگیرد. این نوع کامنت به شما میگویند که این کد نیاز دارد در یک زمان معین و سر فرصت به آن رسیدگی شود. به عنوان مثال بهتر است ویژگی خاصی را به کدی اضافه کرد، یا مورد خاصی بهتر است در آن پیاده سازی شود و یا نیاز به ویرایش خاصی دارد که پیاده سازی آن منفعتی دارد و این کامنت برای این است که به برنامه نویس یادآوری کند تا در دفعات آتی به این کد رسیدگی کند. سپس در دفعات آتی برنامه نویس میتواند با استفاده از ابزاری که ادیتور در اختیار وی قرار میدهد، این نوع کامنتها را پیدا کند.
FIXME : این نوع تگ همانند بالاست، ولی اجبار بیشتری در اصلاح از خود نشان میدهد و ترتیب و اهمیت بالاتری دارد. عموما کدهایی که با این نوع کامنتها مزین میشوند، دارای طراحی بد یا موقتی هستند که باید در آینده آنها را اصلاح کرد.
UNDONE : این تگ برای اصلاح یا تغییر نیست. ولی به شما میگوید که قبلا این کد چگونه بوده است و چه تغییراتی کرده است. قبلا چه چیزهایی در کد پیاده سازی شده بوده است که الان در کد وجود ندارد و چرا حذف شده است.
HACK : گاهی اوقات در کدها، باگ هایی رخ میدهند که مجبور به استفاده از راههای غیرعادی برای رفع آن میشوید. این نوع روشهای رفع مشکل، روشها و راه حلهای مناسبی نیستند؛ ولی میتوانند به طور موقت و در زمان سریعتری پاسخگوی ما باشند. برنامه نویس بعد از رفع مشکل، با درج این نوع کامنت، در آینده به خود یادآوری میکند که این کد نیاز به راه حل مناسبتری دارد.
BUGBUG : این کامنت توسط برنامه نویس کد مربوطه درج میشود و مربوط به زمانی است که برنامه نویس کد را نوشته است، ولی اطمینانی از صحت آن ندارد. پس برنامه نویس نیاز دارد اطلاعات بیشتری را در مورد این مسئله بیابد.
// BUGBUG: I'm sure these GUIDs are defined somewhere but I'm not sure which library contains them, so defining them here. DEFINE_GUID(IID_IFoo, 0x12345678,0x1234,0x1234,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12);
XXX : به برنامه نویس هشدار میدهد که این کد راه حلهای نادرستی دارد و احتمالا بر اساس اطلاعات نادرستی این کد شکل گرفته است، ولی در حال حاضر کار میکند.
در ویژوال استادیو، پنل taskList برای نمایش این تگها به کار میرود و از تگهای HACK,UNDONE و TODO به طور پیش فرض پشتیبانی میکند. در صورتی که تمایل دارید تگهای اضافهتری داشته باشید یا ترتیب اولویت نمایش تگها در پنل taskList را تغییر دهید، مسیر زیر را طی کنید:
Tools>Options>Environment>Task List
در اندروید استادیو هم دو تگ اول لیست پشتیبانی میشوند. در اندروید استادیو شما میتوانید برای todo هایتان الگو و فیلتر تعریف کنید. برای اینکار ابتدای ادیتور را باز کرده و در بخش Editor گزینه Todo را انتخاب کنید. در لیست بالا میتوانید یک نمونه الگو برای todo خاص خود اضافه کنید. به عنوان مثال تگهای نامبرده در بالا را اضافه کنید و برای آن آیکن و نحوه رنگبندی و قلم و ... را برای نمایش آن انتخاب کنید.
در لیست پایینی که بخش فیلترهاست، میتوانید یک فیلتر را تعریف کنید تا بر اساس این فیلتر مشخص کنید که چه todo هایی نمایش یابند. برای فیلتر کردن در در پنل todo، در نوار ابزار، آیکن قیفی شکل را کلیک کند تا لیست فیلترها نمایش یابند.
نحوه صحیح قرار دادن یک todo به شکل زیر است:
// TODO:2008-12-06:johnc:Add support for negative offsets. // While it is unlikely that we get a negative offset, it can // occur if the garbage collector runs out of space.
آشنایی با الگوی MVP
پروژههای زیادی را میتوان یافت که اگر سورس کدهای آنها را بررسی کنیم، یک اسپاگتی کد تمام عیار را در آنها میتوان مشاهده نمود. منطق برنامه، قسمت دسترسی به دادهها، کار با رابط کاربر، غیره و غیره همگی درون کدهای یک یا چند فرم خلاصه شدهاند و آنچنان به هم گره خوردهاند که هر گونه تغییر یا اعمال درخواستهای جدید کاربران، سبب از کار افتادن قسمت دیگری از برنامه میشود.
همچنین از کدهای حاصل در یک پروژه، در پروژههای دیگر نیز نمیتوان استفاده کرد (به دلیل همین در هم تنیده بودن قسمتهای مختلف). حداقل نتیجه یک پروژه برای برنامه نویس، باید یک یا چند کلاس باشد که بتوان از آن به عنوان ابزار تسریع انجام پروژههای دیگر استفاده کرد. اما در یک اسپاگتی کد، باید مدتی طولانی را صرف کرد تا بتوان یک متد را از لابلای قسمتهای مرتبط و گره خورده با رابط کاربر استخراج و در پروژهای دیگر استفاده نمود. برای نمونه آیا میتوان این کدها را از یک برنامه ویندوزی استخراج کرد و آنها را در یک برنامه تحت وب استفاده نمود؟
یکی از الگوهایی که شیوهی صحیح این جدا سازی را ترویج میکند، الگوی MVP یا Model-View-Presenter میباشد. خلاصهی این الگو به صورت زیر است:
Model :
من میدانم که چگونه اشیاء برنامه را جهت حصول منطقی خاص، پردازش کنم.
من نمیدانم که چگونه باید اطلاعاتی را به شکلی بصری به کاربر ارائه داد یا چگونه باید به رخدادها یا اعمال صادر شده از طرف کاربر پاسخ داد.
View :
من میدانم که چگونه باید اطلاعاتی را به کاربر به شکلی بصری ارائه داد.
من میدانم که چگونه باید اعمالی مانند data binding و امثال آن را انجام داد.
من نمیدانم که چگونه باید منطق پردازشی موارد ذکر شده را فراهم آورم.
Presenter :
من میدانم که چگونه باید درخواستهای رسیده کاربر به View را دریافت کرده و آنها را به Model انتقال دهم.
من میدانم که چگونه باید اطلاعات را به Model ارسال کرده و سپس نتیجهی پردازش آنها را جهت نمایش در اختیار View قرار دهم.
من نمیدانم که چگونه باید اطلاعاتی را ترسیم کرد (مشکل View است نه من) و نمیدانم که چگونه باید پردازشی را بر روی اطلاعات انجام دهم. (مشکل Model است و اصلا ربطی به اینجانب ندارد!)
یک مثال ساده از پیاده سازی این روش
برنامهای وبی را بنویسید که پس از دریافت شعاع یک دایره از کاربر، مساحت آنرا محاسبه کرده و نمایش دهد.
یک تکست باکس در صفحه قرار خواهیم داد (txtRadius) و یک دکمه جهت دریافت درخواست کاربر برای نمایش نتیجه حاصل در یک برچسب به نام lblResult
الف) پیاده سازی به روش متداول (اسپاگتی کد)
protected void btnGetData_Click(object sender, EventArgs e)
{
lblResult.Text = (Math.PI * double.Parse(txtRadius.Text) * double.Parse(txtRadius.Text)).ToString();
}
اما این مشکلات را هم دارد:
- منطق برنامه (روش محاسبه مساحت دایره) با رابط کاربر گره خورده.
- کدهای برنامه در پروژهی دیگری قابل استفاده نیست. (شما متد یا کلاسی را اینجا با قابلیت استفاده مجدد میتوانید پیدا میکنید؟ آیا یکی از اهداف برنامه نویسی شیءگرا تولید کدهایی با قابلیت استفاده مجدد نبود؟)
- چگونه باید برای آن آزمون واحد نوشت؟
ب) بهبود کد و جدا سازی لایهها از یکدیگر
در روش MVP متداول است که به ازای هر یک از اجزاء ابتدا یک interface نوشته شود و سپس این اینترفیسها پیاده سازی گردد.
پیاده سازی منطق برنامه:
1- ایجاد Model :
یک فایل جدید را به نام CModel.cs به پروژه اضافه کرده و کد زیر را به آن خواهیم افزود:
using System;
namespace MVPTest
{
public interface ICircleModel
{
double GetArea(double radius);
}
public class CModel : ICircleModel
{
public double GetArea(double radius)
{
return Math.PI * radius * radius;
}
}
}
- خبری از textbox و برچسب و غیره نیست. اصلا نمیداند که رابط کاربری وجود دارد یا نه.
- خبری از رخدادهای برنامه و پاسخ دادن به آنها نیست.
- از این کد میتوان مستقیما و بدون هیچ تغییری در برنامههای دیگر هم استفاده کرد.
- اگر باگی در این قسمت وجود دارد، تنها این کلاس است که باید تغییر کند و بلافاصله کل برنامه از این بهبود حاصل شده میتواند بدون هیچگونه تغییری و یا به هم ریختگی استفاده کند.
- نوشتن آزمون واحد برای این کلاس که هیچگونه وابستگی به UI ندارد ساده است.
2- ایجاد View :
فایل دیگری را به نام CView.cs را به همراه اینترفیس زیر به پروژه اضافه میکنیم:
namespace MVPTest
{
public interface IView
{
string RadiusText { get; set; }
string ResultText { get; set; }
}
}
کار View دریافت ابتدایی مقادیر از کاربر توسط RadiusText و نمایش نهایی نتیجه توسط ResultText است البته با یک اما.
View نمیداند که چگونه باید این پردازش صورت گیرد. حتی نمیداند که چگونه باید این مقادیر را به Model جهت پردازش برساند یا چگونه آنها را دریافت کند (به همین جهت از اینترفیس برای تعریف آن استفاده شده).
3- ایجاد Presenter :
در ادامه فایل جدیدی را به نام CPresenter.cs با محتویات زیر به پروژه خواهیم افزود:
namespace MVPTest
{
public class CPresenter
{
IView _view;
public CPresenter(IView view)
{
_view = view;
}
public void CalculateCircleArea()
{
CModel model = new CModel();
_view.ResultText = model.GetArea(double.Parse(_view.RadiusText)).ToString();
}
}
}
کار این کلاس برقراری ارتباط با Model است.
میداند که چگونه اطلاعات را به Model ارسال کند (از طریق _view.RadiusText) و میداند که چگونه نتیجهی پردازش را در اختیار View قرار دهد. (با انتساب آن به _view.ResultText)
نمیداند که چگونه باید این پردازش صورت گیرد (کار مدل است نه او). نمیداند که نتیجهی نهایی را چگونه نمایش دهد (کار View است نه او).
روش معرفی View به این کلاس به constructor dependency injection معروف است.
اکنون کد وب فرم ما که در قسمت (الف) معرفی شده به صورت زیر تغییر میکند:
using System;
namespace MVPTest
{
public partial class _Default : System.Web.UI.Page, IView
{
protected void Page_Load(object sender, EventArgs e)
{
}
public string RadiusText
{
get { return txtRadius.Text; }
set { txtRadius.Text = value; }
}
public string ResultText
{
get { return lblResult.Text; }
set { lblResult.Text = value; }
}
protected void btnGetData_Click(object sender, EventArgs e)
{
CPresenter presenter = new CPresenter(this);
presenter.CalculateCircleArea();
}
}
}
در اینجا یک وهله از Presenter برای برقراری ارتباط با Model ایجاد میشود. همچنین کلاس وب فرم ما اینترفیس View را نیز پیاده سازی خواهد کرد.
کلاس Kid را با تعریف زیر در نظر بگیرید. هدف از آن نگهداری اطلاعات فرزندان یک شخص خاص میباشد:
namespace IOCBeginnerGuide
{
class Kid
{
private int _age;
private string _name;
public Kid(int age, string name)
{
_age = age;
_name = name;
}
public override string ToString()
{
return "KID's Age: " + _age + ", Kid's Name: " + _name;
}
}
}
اکنون کلاس والد را با توجه به اینکه در حین ایجاد این شیء، فرزندان او نیز باید ایجاد شوند؛ در نظر بگیرید:
using System;
namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;
private Kid _obj;
public Parent(int personAge, string personName, int kidsAge, string kidsName)
{
_obj = new Kid(kidsAge, kidsName);
_age = personAge;
_name = personName;
}
public override string ToString()
{
Console.WriteLine(_obj);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}
و نهایتا مثالی از استفاده از آن توسط یک کلاینت:
using System;
namespace IOCBeginnerGuide
{
class Program
{
static void Main(string[] args)
{
Parent p = new Parent(35, "Dev", 6, "Len");
Console.WriteLine(p);
Console.ReadKey();
Console.WriteLine("Press a key...");
}
}
}
که خروجی برنامه در این حالت مساوی سطرهای زیر میباشد:
KID's Age: 6, Kid's Name: Len
ParentAge: 35, ParentName: Dev
مثال فوق نمونهای از الگوی طراحی ترکیب یا composition میباشد که به آن Object Dependency یا Object Coupling نیز گفته میشود. در این حالت ایجاد شیء والد وابسته است به ایجاد شیء فرزند.
مشکلات این روش:
1- با توجه به وابستگی شدید والد به فرزند، اگر نمونه سازی از شیء فرزند در سازندهی کلاس والد با موفقیت روبرو نشود، ایجاد نمونهی والد با شکست مواجه خواهد شد.
2- با از بین رفتن شیء والد، فرزندان او نیز از بین خواهند رفت.
3- هر تغییری در کلاس فرزند، نیاز به تغییر در کلاس والد نیز دارد (اصطلاحا به آن Dangling Reference هم گفته میشود. این کلاس آویزان آن کلاس است!).
چگونه این مشکلات را برطرف کنیم؟
بهتر است کار وهله سازی از کلاس Kid به یک شیء، متد یا حتی فریم ورک دیگری واگذار شود. به این واگذاری مسئولیت، delegation و یا inversion of control - IOC نیز گفته میشود.
بنابراین IOC میگوید که:
1- کلاس اصلی (یا همان Parent) نباید به صورت مستقیم وابسته به کلاسهای دیگر باشد.
2- رابطهی بین کلاسها باید بر مبنای تعریف کلاسهای abstract باشد (و یا استفاده از interface ها).
تزریق وابستگی یا Dependency injection
برای پیاده سازی IOC از روش تزریق وابستگی یا dependency injection استفاده میشود که میتواند بر اساس constructor injection ، setter injection و یا interface-based injection باشد و به صورت خلاصه پیاده سازی یک شیء را از مرحلهی ساخت وهلهای از آن مجزا و ایزوله میسازد.
مزایای تزریق وابستگیها:
1- گره خوردگی اشیاء را حذف میکند.
2- اشیاء و برنامه را انعطاف پذیرتر کرده و اعمال تغییرات به آنها سادهتر میشود.
روشهای متفاوت تزریق وابستگی به شرح زیر هستند:
تزریق سازنده یا constructor injection :
در این روش ارجاعی از شیء مورد استفاده، توسط سازندهی کلاس استفاده کننده از آن دریافت میشود. برای نمونه در مثال فوق از آنجائیکه کلاس والد به کلاس فرزندان وابسته است، یک ارجاع از شیء Kid به سازندهی کلاس Parent باید ارسال شود.
اکنون بر این اساس تعاریف، کلاسهای ما به شکل زیر تغییر خواهند کرد:
//IBuisnessLogic.cs
namespace IOCBeginnerGuide
{
public interface IBuisnessLogic
{
}
}
//Kid.cs
namespace IOCBeginnerGuide
{
class Kid : IBuisnessLogic
{
private int _age;
private string _name;
public Kid(int age, string name)
{
_age = age;
_name = name;
}
public override string ToString()
{
return "KID's Age: " + _age + ", Kid's Name: " + _name;
}
}
}
//Parent.cs
using System;
namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;
private IBuisnessLogic _refKids;
public Parent(int personAge, string personName, IBuisnessLogic obj)
{
_age = personAge;
_name = personName;
_refKids = obj;
}
public override string ToString()
{
Console.WriteLine(_refKids);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}
//CIOC.cs
using System;
namespace IOCBeginnerGuide
{
class CIOC
{
Parent _p;
public void FactoryMethod()
{
IBuisnessLogic objKid = new Kid(12, "Ren");
_p = new Parent(42, "David", objKid);
}
public override string ToString()
{
Console.WriteLine(_p);
return "Displaying using Constructor Injection";
}
}
}
//Program.cs
using System;
namespace IOCBeginnerGuide
{
class Program
{
static void Main(string[] args)
{
CIOC obj = new CIOC();
obj.FactoryMethod();
Console.WriteLine(obj);
Console.ReadKey();
Console.WriteLine("Press a key...");
}
}
}
توضیحات:
ابتدا اینترفیس IBuisnessLogic ایجاد خواهد شد. تنها متدهای این اینترفیس در اختیار کلاس Parent قرار خواهند گرفت.
از آنجائیکه کلاس Kid توسط کلاس Parent استفاده خواهد شد، نیاز است تا این کلاس نیز اینترفیس IBuisnessLogic را پیاده سازی کند.
اکنون سازندهی کلاس Parent بجای ارجاع مستقیم به شیء Kid ، از طریق اینترفیس IBuisnessLogic با آن ارتباط برقرار خواهد کرد.
در کلاس CIOC کار پیاده سازی واگذاری مسئولیت وهله سازی از اشیاء مورد نظر صورت گرفته است. این وهله سازی در متدی به نام Factory انجام خواهد شد.
و در نهایت کلاینت ما تنها با کلاس IOC سرکار دارد.
معایب این روش:
- در این حالت کلاس business logic، نمیتواند دارای سازندهی پیش فرض باشد.
- هنگامیکه وهلهای از کلاس ایجاد شد دیگر نمیتوان وابستگیها را تغییر داد (چون از سازندهی کلاس جهت ارسال مقادیر مورد نظر استفاده شده است).
تزریق تنظیم کننده یا Setter injection
این روش از خاصیتها جهت تزریق وابستگیها بجای تزریق آنها به سازندهی کلاس استفاده میکند. در این حالت کلاس Parent میتواند دارای سازندهی پیش فرض نیز باشد.
مزایای این روش:
- از روش تزریق سازنده بسیار انعطاف پذیرتر است.
- در این حالت بدون ایجاد وهلهای میتوان وابستگی اشیاء را تغییر داد (چون سر و کار آن با سازندهی کلاس نیست).
- بدون نیاز به تغییری در سازندهی یک کلاس میتوان وابستگی اشیاء را تغییر داد.
- تنظیم کنندهها دارای نامی با معناتر و با مفهومتر از سازندهی یک کلاس میباشند.
نحوهی پیاده سازی آن:
در اینجا مراحل ساخت Interface و همچنین کلاس Kid با روش قبل تفاوتی ندارند. همچنین کلاینت نهایی استفاده کننده از IOC نیز مانند روش قبل است. تنها کلاسهای IOC و Parent باید اندکی تغییر کنند:
//Parent.cs
using System;
namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;
public Parent(int personAge, string personName)
{
_age = personAge;
_name = personName;
}
public IBuisnessLogic RefKID {set; get;}
public override string ToString()
{
Console.WriteLine(RefKID);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}
//CIOC.cs
using System;
namespace IOCBeginnerGuide
{
class CIOC
{
Parent _p;
public void FactoryMethod()
{
IBuisnessLogic objKid = new Kid(12, "Ren");
_p = new Parent(42, "David");
_p.RefKID = objKid;
}
public override string ToString()
{
Console.WriteLine(_p);
return "Displaying using Setter Injection";
}
}
}
همانطور که ملاحظه میکنید در این روش یک خاصیت جدید به نام RefKID به کلاس Parent اضافه شده است که از هر لحاظ نسبت به روش تزریق سازنده با مفهومتر و خود توضیح دهندهتر است. سپس کلاس IOC جهت استفاده از این خاصیت اندکی تغییر کرده است.
ماخذ
class Painter { private readonly float daysPerHouse; public Painter(float daysPerHouse) { this.daysPerHouse = daysPerHouse; } public float EstimateDaysToPaint(int houses) { return houses * daysPerHouse; } }
class LandOwner { private readonly Painter painter; private readonly int housesCount; public LandOwner(Painter painter, int housesCount) { this.painter = painter; this.housesCount = housesCount; } public void ManageHouses() { float daysToPaint = this.painter.EstimateDaysToPaint(this.housesCount); Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint); } }
class LandOwner { private readonly IEnumerable<Painter> painters; private readonly int housesCount; public LandOwner(IEnumerable<Painter> painters, int housesCount) { this.painters = new List<Painter>(painters); this.housesCount = housesCount; } ... }
اکنون مالک زمین مسئولیت انجام این محاسبه را برعهده گرفته است؛ ولی این پیاده سازی کمی پیچیدهتر میشود:
class LandOwner { private readonly IEnumerable<Painter> painters; private readonly int housesCount; public LandOwner(IEnumerable<Painter> painters, int housesCount) { this.painters = new List<Painter>(painters); this.housesCount = housesCount; } private float GetVelocity(Painter painter) { return painter.EstimateDaysToPaint(1); } private float GetTotalVelocity() { float sum = 0; foreach (Painter painter in this.painters) sum += 1 this.GetVelocity(painter); return sum; } public void ManageHouses() { float daysToPaint = this.GetTotalVelocity() * this.housesCount; Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint); } }
این پیاده سازی کمی پیچیدهاست؛ اما کار میکند و همچنین دارای مشکلاتی است. فرض کنید یکی از نقاشان صاحب شرکت، نقاشی است که نقاشان دیگر را استخدام میکند. حتی بدتر از آن این است که اگر شرکت نقاشی، شرکت دیگری را نیز همراه با نقاشان خود، استخدام کند. مالک زمین با سلسله مراتبی چندسطحی از نقاشان روبرو میشود. برآورد زمان لازم برای نقاشی خانهها در چنین شرایطی برای مالک زمین دشوار خواهد شد.
پیاده سازی Composite
اگر تنها بتوانیم یک اینترفیس عمومی را از یک نقاش، بیرون بکشیم، سازماندهی نقاشها راحتتر میشود:
interface IPainter { float EstimateDaysToPaint(int houses); }
مالک زمین دیگر کاری با مجموعه نقاشها ندارد و در حال حاضر تنها یک نقاش انتزاعی را کنترل میکند:
class LandOwner { private readonly IPainter painter; private readonly int housesCount; public LandOwner(IPainter painter, int housesCount) { this.painter = painter; this.housesCount = housesCount; } public void ManageHouses() { float daysToPaint = this.painter.EstimateDaysToPaint(this.housesCount); Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint); } }
اینبار مالک زمین فقط ارجاعی را به یک نقاش انتزاعی دارد. از سوی دیگر، کلاس نقاش دست نخورده باقی میماند و تنها رابط IPainter را پیاده سازی میکند:
class Painter: IPainter { ... }
حالا میتوانیم نتیجه آن را ببینیم. ما آماده تعریف یک عنصر Composite هستیم که خود و عناصرش، اینترفیس IPainter را پیاده سازی کردهاند.
class PaintingCompany: IPainter { private readonly IEnumerable<IPainter> painters; public PaintingCompany(IEnumerable<IPainter> painters) { this.painters = new List<IPainter>(painters); } private float GetVelocity(Painter painter) { return painter.EstimateDaysToPaint(1); } private float GetTotalVelocity() { float sum = 0; foreach (Painter painter in this.painters) sum += 1 this.GetVelocity(painter); return sum; } public float EstimateDaysToPaint(int houses) { return this.GetTotalVelocity() * houses; } }
این پیاده سازی شرکت نقاشی است. کد کلاس LandOwner قبلی که وظیفه آن کنترل نقاشها بود، به این کلاس منتقل شدهاست. تفاوت این است که شرکت نقاشی اکنون تعدادی نقاش انتزاعی را مدیریت میکند. از انتزاعات میتوان دو حالت را در نظر گرفت: به صورت تک و یا به صورت گروه. این مورد قدرت نوع انتزاعی است در برنامه نویسی شیء گرا که در اینجا خودش را به صورت یک نقاش و یا گروهی از افراد که با هم کار میکنند، نشان میدهد.
نتیجه گیری
در این مقاله ما به یک نمونه از الگوی طراحی Composite پرداختیم. با استفاده از الگوی Composite، شیوهای که کلاسها با مجموعهها برخورد میکنند، بسیار سادهتر شدهاست. کار با مجموعهها، کد را پیچیدهتر کرده و باعث میشود کلاس، کاری بیشتر از مسئولیتهای خود را انجام دهد که ربطی به آن ندارد.
تفاوتهای طراح ارشد و طراح تازهکار
در جامعه کاری که ما در اون قرار داریم، خیلی وقتها تفاوت بین ادو مفهوم طراح ارشد (Senior Designer) و طراح تازهکار (Junior Designer) خیلی مشخص نیست و تعیینشون بعضا بواسطه تجربه و گاهی هم از طریق سمتهایی که در شرکتها داده میشه، انجام میپذیره.
با در نظر گرفتن این موضوع که اخیرا مفهوم طراح، شکلی کلیتر به خودش گرفته و به طوری تغییر (و به دید من بهبود) پیدا کرده که معرف شخصیست با نوع متفاوتی از تفکر در فهم یک مسئله و پیدا کردن راه حل اون. طراحان بسیاری هستند که علیرغم داشتن تجربه زیاد، اختیارات بالا و تاثیرگذاری در محصولات سازمان و همینطور تبحر استفاده از ابزارهای متنوع، همچنان تغییری در روند و نحوه تفکر اونها برای طراحی محصولات ایجاد نشده.