در این پست می خوایم در مورد Repository Pattern و Unit of Work و نحوه پیاده سازی اون صحبت کنیم.(در این مقاله از زبان Type scripts برای کد ها استفاده شده ولی کدهای استفاده شده در نهایت سادگی هست و پس از مطالعه این مقاله , این پترن رو با هر زبانی میتونید پیاده کنید.)
پیش نیاز :
این پترن در کتاب Patterns of Enterprise Application Architecture نوشته ی Martin Fowler بیان شده و رفرنس من هم برای تعاریفی که در این مقاله بیان میشه همین کتاب هست.
درابتدا تعریف Repository Pattern رو از این کتاب با هم بررسی می کنیم.
Mediates between the domain and data mapping layers , acting like an in-memory collection of domain objects.
⁉️ این تعریف چی میگه؟
قبل از اینکه این تعریف رو تحلیل و موشکافی کنیم بهتره کلمات کلیدی این تعریف رو با هم بررسی کنیم.
توی معماری هایی مثل Clean Architecture یک لایه به اسم Domain داریم که منطق تجاری برنامه در اون قرار میگیره. توی این لایه مواردی که مربوط به جزییات پیاده سازی مثل دیتابیس و... هست قرار نمیگیره.
این جا با یه تعریف از کتاب Clean Architecture نوشته Uncle Bob این مفهوم رو بررسی می کنیم و در پست های بعدی به طور کامل Entity ها رو موشکافی می کنیم.
An Entity is an object within our computer system that embodies a small set of critical business rules operating on Critical Business Data. The Entity object either contains the Critical Business Data or has very easy access to that data. The interface of the Entity consists of the functions that implement the Critical Business Rules that operate on that data.
با یک مثال این تعریف رو ساده می کنیم.
مثلن توی هر اپلیکیشنی ما با کاربران سر و کار داریم (Users) پس یه کلاس برای User می سازیم.
هر کاربر یک سری ویژگی ها داره مثل اسم , سن و ...
این خصوصیات یا Property ها رو در واقع می تونیم Critical Business Data در نظر بگیریم و متد هایی که توی این کلاس اضافه میشن و روی این پراپرتی ها کار میکنن رو Critical Business Rules میگیم.
? یک نکته این که هر Entity حتمن باید یک id داشته باشه و دو Entity در صورتی با هم برابرند که اولن از یک نوع باشن و دومن id یکسانی داشته باشن. ینی دو Entity از جنس User همین که id برابری داشته باشن با هم دیگ یکسانن حتا اگه خصوصیات دیگه اون ها برابر نباشه.(این نکته رو توی مقاله مربوط به Entity ها و Value Object ها بیشتر بررسی می کنیم.)
در واقع Data Mapper Pattern هم یک پترن معماری هست که تعریف کاملش رو می تونید توی کتاب Martin Fowler پیدا کنید.
Objects and relational databases have different mechanisms for structuring data. Many parts of an object, such as collections and inheritance, aren't present in relational databases. When you build an object model with a lot of business logic it's valuable to use these mechanisms to better organize the data and the behavior that goes with it. Doing so leads to variant schemas; that is, the object schema and the relational schema don't match up.
You still need to transfer data between the two schemas, and this data transfer becomes a complexity in its own right. If the in-memory objects know about the relational database structure, changes in one tend to ripple to the other.
The Data Mapper is a layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data between the two and also to isolate them from each other. With Data Mapper the in-memory objects needn't know even that there's a database present; they need no SQL interface code, and certainly no knowledge of the database schema. (The database schema is always ignorant of the objects that use it.) Since it's a form of Mapper (473), Data Mapper itself is even unknown to the domain layer.
حالا برگردیم به تعریف Repository Pattern
Mediates between the domain and data mapping layers , acting like an in-memory collection of domain objects.
این تعریف میگه که Repository در واقع یه کالکشن از Domain Object ها یا همون Entity ها هست که به عنوان واسط بین لایه Domain و Data Mapper ها عمل میکنه.
خب این تعریف رو بیشتر بازش کنیم....
گفتیم که Repository یه Collection از Entity ها هست , پس باید متدهایی رو داشته باشه که یه Collection داره , متد هایی مانند :
? اولین نکته ای که این جا باید بهش توجه کنیم اینه که داریم در مورد یه Collection صحبت می کنیم, پس متدی به اسم save یا update نداریم.
⁉️ پس اگه save و update نداریم چطوری یه Entity جدید ایجاد کنیم یا اون رو اپدیت کنیم؟ این سوال رو وقتی که پترن Unit of Work رو گفتیم جواب میدیم.
یکم بریم سراغ پیاده سازی ???
گفتیم که هر Repository نماینده یک Entity هست و به عنوان Collection ای از اون Entity در Memory عمل میکنه.(به Memory هم توجه داشته باشید که بعد از توضیح Unit of Work مفهومشو میفهمیم). و همین طور گفتیم هر Repository به خاطر ویژگی Collection بودنش که داره متد هایی مثل add , remove , get , find رو داره. حالا چون که همه Repository ها باید این متد ها رو داشته باشن برای جلوگیری از تکرار کد ها یه کلاس بیس می سازیم که این متد های پایه رو داره و بقیه کلاس ها ازش ارث بری می کنند.
همچنین برای رعایت اصول معماری اول یه دونه اینترفیس می سازیم که شامل این متد های پایه میشه.
این جا اول یک کلاس بیس برای Entity ها ایجاد کردیم که شامل id هست و همه Entity ها باید از اون ارث بری کنن.
یه اینترفیس هم برای Repository های خودمون ساختیم که جنریک هست و با استفاده از جنریک تایپی که داره مشخص میشه که نماینده کدوم Entity هست.
? از نقطه نظر معماری این کلاس ها توی بخش Domain قرار میگیرن . یا اگه از معماری سه لایه استفاده میکنید میتونه توی بخشی که لاجیک رو کنترل میکنه قرار بگیره (Controller)
?منظور از اصول معماری رعایت اصول OCP (اصل دوم سالید ) و DIP (اصل پنجم سالید) و ... هست.
حالا باید یه کلاسی داشته باشیم که این اینترفیس رو پیاده سازی کنه .
اینجا کلاس Repository رو می بینیم که اینترفیس قبلی رو پیاده سازی کرده و یه Abstract Generic Class هست.
? برای هر دیتابیس یا هر Data Mapper که داریم می تونیم یه پیاده سازی جدا داشته باشیم.
خب تا اینجا کلاس های بیس رو پیاده کردیم و باید بریم سراغ هر کدوم از Entity ها .
به ازای هر Entity باید یه دونه Repository داشته باشیم که علاوه بر داشتن متد های پایه متد های اختصاصی مربوط به اون Entity رو هم داره (رابطه یک به یک - یک Collection به ازای هر Entity).
اینجا هم به ازای هر Entity یه اینترفیس میسازیم که از اینترفیس IRepository ارث بری میکنه.
فرض کنید یه اپلیکیشن فروشگاهی داریم و یکی از موجودیت های ما محصولات هستن.
میبینیم که یه IProductInterface داریم که نماینده محصولات هست و با ارث بری از IRepository متد های پیاه رو به ارث میبره و خودش هم دو تا متد اختصاصی برای دریافت پرفروش ترین محصولات و دریافت محصولات به وسیله ی دسته بندی اضافه میکنه.
?از نقطه نظر معماری این اینترفیس هم باید در بخش Domain قرار بگیره.
حالا بریم سراغ پیاده سازی این اینترفیس که توی بخش Data یا دیتابیس قرار میگیره.
اینجا کلاس ProductRepository رو داریم که از کلاس Repository ارث بری کرده تا از پیاده سازی های متد های پایه استفاده کنه و همچنین اینترفیس IProductRepository رو هم پیاده سازی کرده.
خب تا اینجا پیاده سازی Repository Pattern رو دیدیم ولی هنوز به چنتا سوال جواب ندادیم.
⁉️منظور از in-memory توی تعریف Collection of Domain Objects in-memory چیه؟
⁉️ چرا متدی برای ذخیره کردن یا اضافه کردن یه Entity نداریم و برای اضافه کردن یه Entity جدید باید چکار کنیم ( save action)؟
⁉️ چرا متدی برای ویرایش یه Entity نداریم و برای ویرایش یه Entity باید چکار کنیم ( update action)؟
حالا سوالا رو دونه دونه جواب میدیم ??? اما کلید واژه اصلی جواب این سوالا ترکیب کردن Repository Pattern با Unit of Work Pattern هست.
پس اول تعریف این پترن رو از کتاب Patterns of Enterprise Application Architecture با هم میبینیم.
Maintains a list of objects affected by a business transaction and coordinates the writing out of changes.
برای درک مفهوم این پترن و جواب به سوالات بالا یه مثال ساده مطرح میکنیم.
فرض کنید یه اپلیکیشن فروشگاهی داریم که یه Entity به اسم دسته بندی داره (Category) و یه Entity هم به اسم محصول (Product) که هر محصول میتونه یه دسته بندی داشته باشه.
حالا فرض میکنیم که 3 تا سرویس لازم داریم (یا به زبان Clean Architecture همون Use Case)
اول RemoveCategoryService رو بررسی میکنیم. چون که هر محصول به یک دسته بندی کوپل شده تصمیم داریم که با حذف یک دسته بندی همه محصولات مربوط به اون دسته بندی هم حذف شه.
پس کد مربوط به این سرویس رو به این شکل مینویسیم.
اینجا هم یه اینترفیس IService داریم که RemoveCategoryService اون رو پیاده کرده که از توضیح جزییات مربوط به این بحث صرف نظر میکنیم.
اگه به پیاده سازی متد execute توی سرویس مورد نظر توجه کنیم (فقط موارد ضروری اورده شده و از جزییات صرف نظر شده) میبینیم که ابتدا دسته بندی مورد نظر و محصولات مربوط به اون دریافت شدن و بعدن به ترتیب دسته بندی و محصولات حذف شدن.
با یه نگاه ساده به این پیاده سازی مشکلی نمیبینیم و به ظاهر درسته.
⁉️ پس مشکل کجاست؟ ???
ما اول داریم دسته بندی رو حذف میکنیم و بعد که مطمین شدیم ک دسته بندی به درستی حذف شد محصولاتش رو حذف میکنیم. حالا اگه حین حذف محصولات یه مشکلی به وجود بیاد و محصولات به درستی حذف نشن , دسته بندی حذف شده و یه سری محصول رو هوا مونده داریم. ???
اینجاست که Unit of Work Pattern در کنار Repository به کمکمون میاد.
قبلن گفتیم که Repository در واقع یه کالکشن از Entity ها توی حافظه (رم) هست . وقتی داریم از حافظه صحبت میکنیم یعنی اینکه این تغییراتی که داریم اعمال میکنیم اصلن توی دیتابیس ذخیره نمیشن و فقط توی حافظه ثبت میشن(این پترن باید به نحوی پیاده بشه که تغییرات فقط توی حافظه ثبت بشن).
یعنی وقتی که میگیم :
await this.categories.remove(category);
await this.products.removeRange(products);
این تغییرات فقط توی حافظه ثبت میشن و باید یه راهی پیدا کنیم که اون ها رو به صورت دایم ثبت کنیم.
اینجاست که Unit of Work به کمکمون میاد.
اول یکم راجب پیاده سازی این پترن توضیح بدیم. این جا هم اول ی اینترفیس میسازیم.
این اینترفیس شامل همه اینترفیس هایی هست که برای Repository هامون ساختیم(Abstract Factory Design Pattern) و یه متد complete داره.
⁉️ ولی کارش چیه؟
به جای ایکه توی سرویس های به صورت مستقیم به Repository ها دسترسی پیدا کنیم یه ابجکت از جنس IUnitOfWork رو به سرویس ها پاس میدیم و با استفاده از پراپرتی های این ابجکت (categories , products, ...)به Repository ها دسترسی پیدا میکنیم.
اینجوری وقتی که روی هر Repository یه تغییری اعمال میشه اون تغییر فقط و فقط توی حافظه اعمال میشه و اخر کار که کار سرویس تموم شد و همه تغییرات اعمال شدن , سرویس متد complete رو صدا میزنه و این متد همه تغییرات اعمال شده روی حافظه رو ذخیره میکنه.
⁉️ مزیت این روش چیه؟
???مزیت این روش اینه که وقتی توی مثال بالا اول دسته بندی و بعد محصولات رو حذف میکنیم این تغییرات فقط توی حافظه اعمال میشه و با صدا زدن متد complete یه transaction توی دیتابیس ساخته میشه و همه اون تغییرات رو با هم توی دیتابیس اعمال میکنه . حالا اگه یهو وسط کار یه مشکلی پیش بیاد و مثلن یه دونه از محصولات نتونه که حذف شه همه تغییرات به حالت اول برگردونده میشه و انگار که هیچ تغییری توی دیتابیس ندادیم (و این متد یه ارور برمیگردونه و به یوزر میگه حذف به درستی انجام نشد) و نه دسته بندی و نه محصولات هیچ کدوم حذف نمیشن.
پس یا همه تغییرات با هم به درستی اعمال میشن یا هیچ کدوم.
محصولات هم رو هوا نمیمونن. ???
حالا پیاده سازی اینترفیس IUnitOfWork رو هم با ببینیم.
توی سازنده این کلاس میبینیم که categories , products مقدار دهی شدن و پیاده سازی های واقعی برای این مقدار دهی استفاده شدن.
پس فک میکنم که مفهوم in_memory رو به خوبی درک کرده باشید.
حالا بریم به دو سوال دیگه جواب بدیم ???
⁉️ چرا متدی برای ذخیره کردن یا اضافه کردن یه Entity نداریم و برای اضافه کردن یه Entity جدید باید چکار کنیم ( save action)؟
اینکه چرا متدی به اسم save توی Repository نداریم رو قبلن توضیح دادیم ولی برای اضافه کردن یه Entity جدید از متد add استفاده میکنیم . این متد هم تغییرات رو توی حافظه اعمال میکنه و در نهایت با صدا زدن complete این تغییرات ذخیره میشه.
برای این منظور مثال AddCategoryService رو با هم میبینیم.
سوال بعدی ...
⁉️ چرا متدی برای ویرایش یه Entity نداریم و برای ویرایش یه Entity باید چکار کنیم ( update action)؟
گفتیم که Repository درو اقع یه کالکشن از ابجکت هاست. توی دنیای برنامه نویسی برای ویرایش یه عنصر از یه ارایه چکار میکنیم؟
names = ['Mohammad' , 'Arash' , 'Shayan']; names[0] = 'Daryoosh';
اینجا هم دقیقن همین کار رو میکنیم. به مثال EditCategoryService توجه کنید.
میبینیم که اول دسته بندی رو دریافت کردیم و بعد ویرایش کردیم و در نهایت تغییرات رو با صدا زدن متد complete ذخیره کردیم.
حالا یکم از مزایای Repository Pattern بگیم :
دیدیم که همه Repository ها به یک سری متد های پایه نیاز دارن و با استفاده از یه بیس کلاس این متد ها رو پیاده کردیم و از تکرار کد جلوگیری کردیم.
⁉️ به نظرتون دیگه چه مواردی باعث جلوگیری از تکرار کد میشه؟
این موضوع هم با رعایت اصول سالید از جمله اصل DIP و هچنین رعایت اصل IoC محقق میشه که به طور کامل این موارد رو رعایت کردیم.
این مورد هم همون طور که دیدین با رعایت اصل DIP محقق شد و ما میتونیم پیاده سازی های مختلفی از اینترفیس ها داشته باشیم و از هر کدوم که مایل بودیم استفاده کنیم و به سرویس هامون پاس بدیم (با رعایت اصل ioC).
⁉️ دیگه به نظرتون استفاده از این پترن چه مزیت هایی داشت ؟شما بگین (توی کامنت ??? ).