پایه و اساس
عموما این باور وجود دارد که با استفاده از الگوی 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
بدون معکوس سازی وابستگیها، طراحی چند لایه شما ایراد دارد