در این یادداشت به باید ها و نباید ها،الگو ها و ضدالگوها اشاره ای نمی شود و صرفا به بررسی مفاهیم پایه ای برنامه نویسی شی گرا در قالب Repository و Unit Of Work می پردازیم. پایه و اساس مطلب فوق برگرفته شده از دوره کپسوله سازی entity framework آقای Vladimir Khorikov می باشد و همچنین دارای ارجاعات متعددی به مقالاتی در این زمینه در وب می باشد تا حد ممکن سعی شده است که به منابع در پایان یادداشت اشاره شود.
ایده کپسوله سازی (Encapsulation) در مورد محتوی یک شی(Object) است. نه تنها برای نگه داشتن آنها، بلکه برای محافظت از آنها می باشد. بعضی مواقع تعریفی که از کپسوله سازی میبنید ممکن است Information Hiding یا مخفی کردن اطلاعات باشد یا حتی در کنار هم قرار دادن داده ها و یا عملیاتی که روی داده ها انجام میشود و یا به اصطلاح Bundle کردن داده ها باشد.
هر چند که این دو تکنیک میتوانند کپسوله سازی رو ایجاد کنند اما Encapsulation این نیست.
کپسوله سازی یا Encapsulation به معنای محافظت کردن از جامعیت داده ها یا Data Integrity است. یعنی یک کلاس وقتی به خوبی کپسوله شود داده های درونی یا Internal Data آن نمیتواند درون یک مقدار یا حالت Inconsistent یا ناسازگار یا نامعتبری قرار بگیرد. یعنی محافظت از جامعیت و صحت داده ها که به آن Encapsulation یا کپسوله سازی گفته می شود.
همانطور که ذکر شد Information Hiding و Bundle کردن داده ها و یا عملیاتی که روی داده ها انجام میشود در کنار هم به کپسوله سازی کمک میکند. Information Hiding داده های درونی رو از دید کلاینت های آن کلاس مخفی میکند و قرار دادن داده ها و Operation در کنار هم یک نقطه ورود یگانه ایجاد میکند برای تمامی عملیاتی که میتواند روی کلاس انجام گیرد.
به این ترتیب شما میتوانید قبل از اینکه اقدام به تغییر دادن State یا داده های یک کلاس بکنید چک انجام بدهید و Integrity آن داده هایی که قرار هست در واقع درون کلاس شما قرار بگیرد را بررسی کنید.
در بحث کپسوله سازی Invariant ها هم مطرح میشود. Invariant یعنی یک سری شروط که باید در تمامی اوقات برقرار باشد. به عنوان یک برنامه نویس شما باید کاری کنید که هیچ کدام از Invariant ها هیچ وقت نقص نشوند و این وظیفه شماست که تمهیدات لازم برای یک همچنین کاری رو در نظر بگیرید.
بزرگ کردن ضروریات و حذف کردن نامرتبط ها تعریف کامل و جامعی از Abstraction می باشد که توسط رابرت مارتین ارائه شده است.
وقتی شما یک کدی مینوسید روش های نامحدودی برای نوشتن یک برنامه دارید بعضی از زبان های برنامه نویسی هم Turing-complete هستند که این اجازه رو به شما میدهد که یک برنامه را به n روش مختلف پیاده سازی کنید.
در سبک برنامه نویسی Freestyle شما این امکان رو خواهید داشت که کل برنامه رو با یک کلاس و یک متد پیاده سازی کنید که مسلما اشتباه است به این علت که ذهن انسان محدود می باشد و همزمان قادر به مدیریت اطلاعات محدودی می باشد به همین علت هم هست که کلی روش و راهنمای نوشتن برنامه خوب بوجود آمده است تا سطح Productivity ما رو بالا ببرد. چون با کاهش سطح بهره وری امکان اشتباه ما بالا میرود و به عبارت دیگر ما با این ساختار باید جلوی پیچیدگی کد رو گرفته و کد برنامه را قابل مدیریت کنیم.
انتزاع یا Abstraction هم اشاره دارد به اینکه به جای تمرکز بر روی مجموعه ای از کارها به صورت همزمان فقط و فقط بر روی یک موضوع متمرکز شود و آنچه که فعلا موضوعیت ندارد رو در نظر نگیرد و یا به عبارت دیگر برنامه ات را به قطعات کوچیکتر شکسته و روی هر کدام به صورت مجزا متمرکز شود.
انتزاع یا Abstraction میتواند سلسله مراتب هم داشته باشد. یعنی Abstraction ای سوار بر Abstraction های دیگر که این به شما کمک میکند ایده های پیچیده تان را به سطوح مختلف شکسته و در هرکدام از سطوح فکر کنید و کد بزنید.
انتزاع های سطح پایین رو lower-order abstraction و انتزاع های سطح بالا رو higher-order abstraction می گویند.
این سلسله مراتب کمک میکند که شما تمرکز کنید که در هر لحظه در یک سطح کد برنید و به سطح دیگر فکر نکنید و درگیر جزئیات سطوح دیگر نشوید.
این دو مفهوم گره خورده بهم هستند ولی تفاوت های زیادی نیز باهم دارند. Encapsulation درمورد سازگاری دیتا صحبت میکند در حالی که Abstraction به بزرگنمایی ضروریات و حذف کردن نامرتبط ها اشاره دارد.
کمکی که Abstraction به ما میکند این می باشد که دیگر نگران اینکه عملیاتی که انجام میدهیم معتبرهست یا نه نیستیم و این وظیفه را به عهده Encapsulation میگذاریم و تمرکز را بر روی آنچه که کد انجام میدهد و نه چگونگی آن قرار میدهیم.
وظیفه اصلی این الگو این است که تمامی کار های مربوط به دیتابیس را در یک محیط یکسان متمرکز کند و در سرتاسر برنامه پخش نباشد و اجازه دسترسی در سرتاسر برنامه و از هرجایی به دیتابیس را نتوانید پیدا کنید.
ریپازیتوری ها با استفاده دامین کلاس ها، نه دیتابیس آبجکت ها ارتباط برقرار میکنند.
یعنی StudentRepository اگر متد GetById داشته باشد یک instance از student به عنوان یه دامین کلاس برای شما return میکند.
به عبارت دیگر ریپازیتوری ها نه تنها کد دسترسی به داده را دارند مثلا کوئری های SQL را داخل خودشان قرار میدهند بلکه Mapping بین دامین مدل و دیتابیس را نیز انجام میدهند.
هدف این پترن این است که دسترسی به داده ها را برای شما ساده تر کند و داده هایی که داخل دیتابیس هستند و دامین آبجکت هایی که داخل دیتابیس هستند را براحتی واکشی کند که اصلا شما مجبور نباشید بدانید که آیا این دامین آبجکت ها از داخل دیتابیس یا مثلا از درون رم و یا ... واکشی می شوند.
همه این موارد را تجمیع و Abstract میکند. پس تمام جزئیات پیاده سازی با این پترن Abstract میشوند.
حالا بیاید برای روشن تر شدن قضیه به این کد یک نگاه بیندازیم.
اگر بخواهیم این قطعه کد رو با Repository جایگزین کنیم چنین تغییری خواهم داشت
Student student = _repository.GetById(id);
میبینید که این دو قطعه کد تفاوت چندانی با هم ندارن. پراپرتی Student از نوع DbSet است و کل متد هایی که برای student در یک Repository میتوانید قرار بدهید در این نوع وجود دارد مثل find,add,update و ...
اینجاست که اختلاف نظر ایجاد Repository ها بوجود میاد از یک طرف طرفدارن Repository معتقد هستن که میبایست همه دیتابیس اکسس کد درون Repository قرار بگیرد و از طرفی کسانی که با DbSet و EF کار میکنند اذعان دارند که DbSet به خودی خود Repository است پس نیازی به ایجاد Repository دیگری نیست.
هر دو این نوع تفکر تا حدودی درست می باشد. پترن Repository و ایده پشت آن برای کار کردن با دیتابیس ها مفید است ولی خیلی قبل تر از معرفی ORM هایی مثل EF شکل گرفته است و امروزه با وجود ORM های پرقدرتی مثل EF شاید خیلی استفاده از این پترن مفید نباشد.
حالا سوالی که مطرح هست این است که باید از کدام روش استفاده کنیم. از DbSet ها استفاده کنیم یا نه بریم یک لایه Abstraction دیگه اضافه کنیم و از Repository های سفارشی خودمون استفاده کنیم. این بسته به نوع پروژه شما دارد.
به عبارت دیگه پترن Repository مباحث مربوط به persistence یا ذحیره کردن اطلاعات داخل دیتابیس رو abstract میکنه پس Repository یک Abstraction است.
برای مثال پیچیدگی مربوط به بازیابی یک student با استفاده از شناسه اون و یا اضافه کردن یک student را abstract میکند. کاری که DbSet هم انجام میدهد. بنابراین اگر پروژه شما نیاز به پیچیدگی بیش از حدی ندارد عملا نیازی به اضافه کردن Abstraction جدید و اضافه کردن یک لایه Repository جدید ندارید.
ولی اگر پروژه شما دارای پیچیدگی ای بیشتر از آنچه هست که DbSet ها در اختیار شما قرار میدهند اضافه کردن یه لایه Abstraction جدید میتواند مفید باشد.
حالا چطور پیچیدگی های برنامه را مدیریت کنیم.
با اضافه کردن سلسله مراتب Abstraction روی همدیگر. به این شکل که Abstraction های سطح بالاتر وابسته به Abstraction های سطح پایین تر شوند.
فقط باید این را در نظر بگیرید که فقط زمانی منطقی هست که شما Repository ها را اضافه کنید که واقعا رسالت مخفی کردن یکسری پیچیدگی ها را برعهده داشته باشند. کلاس هایی که شامل پیچیدگی های اضافه ای نیستن و هیچ چیزی را abstract نمیکنند به Abstraction های کم عمق معروف هستن.(Shallow abstractions)
سازگاری یا consistency یا به عبارتی یک شکل بودن باعث میشود که من توصیه کنم همیشه از Repository های غیر جنریک استفاده کنید.
وقتی شما همیشه از Repository های غیر جنریک استفاده میکنید آگاهید که همیشه از برگ های سلسه مراتب Repository و اون درخت Repository تون رو استفاده میکنید.
خیلی خوبه که به صورت Explicit تمامی Repository هاتون رو بررسی کنید و ببینید که چه Repository های توی کد بیس تون وجود دارند. ایده کلی اینه که Aggregate Root ها توی بحث DDD نیاز به Repository دارند.
کلاس های غیر برگ در این سلسه مراتب باید به صورت یک کلاس Abstract تعریف شود که امکان استفاده به صورت مستقیم فراهم نباشد. این موضوع برای Repository های جنریک هم صدق می کند.
در نهایت باید به این نکته توجه داشته باشید که شما باید به دید صرفا یک Abstraction بهش نگاه کنید. مثلا فرض کنید تا الان استراتژی حذف شما به صورت Physical (حذف واقعی از دیتابیس) بوده و اکنون میخواین اون رو به حذف Logical (حذف منطقی توسط IsDelete) تغییر بدین. در این صورت باید تمام جا هایی که مستقیما از متد Removeخود DbSet استفاده میکردین رو تغییر بدین و این تغییر در یک پروژه بزرگ دردسر زیادی رو به همراه داره، در صورتی که استفاده از یک Abstraction کار رو بسیار ساده میکرد.
درواقع به جای اینکه مستقیما با DbContext و DbSet ها سرو کار داشته باشیم بهتره یک لایه انتزاعی (Abstraction) روی اون ایجاد کنیم و همه جا از اون Abstraction استفاده کنیم، یعنی به جای اینکه در همه جای پروژه متد Remove خود EF رو صدا بزنیم، اون رو داخل کلاس Repository نامی بنویسیم و همه جا متد Delete ریپازیتوری رو فراخوانی کنیم. این باعث میشه اگه یه روزی لازم شد متد Delete ریپازیتوری رو سفارشی کنیم و تغییر بدیم، اون تغییر تو کل پروژه اعمال بشه، چرا که تو کل پروژه از متد Delete ریپازیتوری استفاده کردیم.
به همین ترتیب اگر نیاز به سفارشی سازی متد های دیگر (مثلا Add یا Update) دارید (مثلا اعمال اعتبارسنجی خاص به هنگام افزودن و ویرایش یا لاگ گرفتن تغییرات به هنگام ویرایش و... ) فقط کافیه متد های اون رو داخل Repository تغییر بدین و نه اینکه مجبور باشین تو کل پروژه کد هاتون رو تغییر بدین.
همانطور که پیشتر اشاره شد زمانی ما از یک Abstraction استفاده میکنیم که یک Abstraction سطح بالا یکسری از پیچیدگی های سطح پایین تر رو abstract کرده باشد وقتی خود DbContext یک Unit Of Work هست دلیلی نداریم که یک Unit Of Work تعریف کنیم. با این پیاده سازی عملا دچار Abstraction های کم عمق می شویم که با عنوان Shallow abstractions با اون آشنا شدیم. در سناریو هایی که پیچدگی بر روی DbContext بیش از حد می شود Unit Of Work از حالت shallow خارج شده و پیاده سازی اون توجیح پذیر می شود.
هنگامی که شما از EF استفاده میکنید و یک DbContext را وهله سازی میکنید، در واقع یک Unit Of Work میسازید.
در عمده سناریو ها نیاز هست تا داده های زیادی از پایگاه داده فراخوانی شده و سپس در برخی از آن ها تغییراتی بدهیم، داده ای را اضافه کنیم و داده های دیگر ( مثلا نامعتبر ) را حذف کنیم.
طبیعتا نمی توانیم به ازای هر تغییر و هر بار افزودن و حذف داده ها، بلافاصله و تک به تک، داده ها را به سمت پایگاه داده ارسال کنیم. چرا که این امر هم موجب کاهش کارایی و مصرف منابع خواهد شد و هم ممکن است در حین ثبت داده ها ناگهان خطایی رخ دهد و باقی تغییرات در پایگاه داده منعکس نشوند و خاصیت تراکنشی (Transactional) بیزینس از بین برود.
الگوی Unit Of Work راهکاری را ارائه می دهد که در آن تمامی تغییرات داده شده در مدل، مانیتور می شوند. یعنی دقیقا در جایی ثبت می کنیم که چه مشخصاتی از چه اشیایی تغییر کرده، چه اشیایی اضافه شده و چه اشیایی حذف شده اند. سپس در انتها و پس از انجام کارها، با صدا زدن متد Save خواهیم فهمید که باید چه تغییراتی را به سمت پایگاه داده ارسال کنیم و البته این کار هم به صورت Transactional انجام خواهد گرفت.
اگر کلاس Unit Of Work شما ارجاعی به تمام Repository های شما دارد طبیعاتا نقض اصل Abstraction می باشد. به این علت که Unit Of Work یک Abstraction سطح پایین می باشد ولی Repository ها Abstraction سطح بالا می باشد. با استناد به این دانش که Unit Of Work بدون Repository میتواند کار کند ولی Repository بدون Unit Of Work نمیتواند کار کند.
همانطور که میدانید ارتباط بین Abstraction های سطح بالا و سطح پایین همیشه یک طرفه است از سطح بالا به سطح پایین. بنابراین بجای استفاده از DbSet ها باید از نوع جنریک اون استفاده بشه حالا بنظرتون این همون اتفاق قبلی نیست. DbContext همچنان به DbSet دسترسی داره؟
تفاوت اینجا این هست که DbSet جنریک یه Abstraction سطح پایین تر نسبت به DbContext هست. این کار عینا استفاده از یک اینترفیس بجای کلاس concrete می باشد.
اگر صرفا برای دست یابی به الگوی "Context Per Request" یا "Session Per Request" از Unit Of Work استفاده میکنید باید بدونید که اینکار نه توسط Unit Of Work و یا حتی خود EF بلکه توسط سیستم تزریق وابستگی و در اصل توسط IOC Container ها اتفاق میافته.
درواقع IOC Container هست که میدونه باید برای هر درخواست (یا اصولا Scope)، فقط و فقط یک DbContext ساخته بشه (به جای اینکه چندین DbContext ایجاد بشه!)
اگه بخوایم دقیق تر بحث درخواست و Scope رو باز کنیم باید بگیم که Scope یک "محدوده" هست که وقتی ایجاد میشه، اشیایی که درون اون ایجاد میشوند داخل همون Scope (محدوده) قابل استفاده هستند و در واقع "زنده" هستند و پس از پایان اون محدوده (Scope)، تمام اشیایی که در اون Scope ایجاد شده اند نیز "میمیرند" (در واقع Dispose میشوند و از بین میروند)
البته تمامی این حرف ها "فقط" برای اشیایی صادق است که Lifetime (بازه عمر) آنها به صورت Scope تنظیم شده باشند.
حال جالبه بدونین که IOC Container میاد و در ابتدای یک درخواست وب (Request)، یک Scope ایجاد میکنه و در پایان اون درخواست وب، اون Scope رو از بین میبره (در نتیجه تمام اشیایی آن Scope هم از بین میروند)
در این حالت وقتی ما DbContext رو به صورت Scope (درواقع با Lifetime یا همون بازه عمر برابر با Scope) تعریف میکنیم، اتفاقی که میافته اینه که IOC Container در ابتدای هر درخواست، یک Scope ایجاد میکنه و درنتیجه DbContext ما هم (که قبلا به صورت Scope تنظیم شده است)، فقط و فقط یک نسخه از آن داخل Scope مربوطه (که اول درخواست ساخته شده) ایجاد میشه؛ در نتیجه طی اون Scope ما هر چندبار هم که DbContext رو فراخوانی کنیم، توسط سیستم تزریق وابستگی، "فقط و فقط" همون یک نسخه اولیه به ما ارجاع(پاس) داده میشه و در این حالت الگوی Context Per Request تحقق میابد.
در پایان درخواست وب هم، چون Scope مربوطه ازبین میره، تمامی اشیای داخل آن(ایجاد شده توسط آن Scope) نیز از بین میروند از جمله همین DbContext ما و درواقع در پایان درخواست، DbContext ما هم Dispose میشه
منابع:
https://www.pluralsight.com/courses/ef-core-6-encapsulating-usage
https://www.dntips.ir/
https://t.me/c/1029399467/77614