همه چیز در مورد نحوه استفاده از Repository pattern در فریم ورک Laravel

به نام ایزد یکتا

سلام خدمت همه دوستان عزیز

این مقاله در خصوص یکی از انواع الگوهای طراحی در برنامه نویسی یا به عبارت دیگر Design pattern ها هستش که در اینجا به توضیح یکی این الگوها یعنی Repository pattern میپردازیم.

امیدوارم که در انتهای این مطلب تونسته باشم قدمی کوچک در جهت پیشرفت و آگاهی شما عزیزان برداشته باشم.

قبل از اینکه به سراغ توضیح این الگو بریم یه نیم نگاهی به تعریف Design pattern داشته باشیم اینکه چیه و چه کاربردی داره و ....

چی هست این Design Pattern..؟

الگوهای طراحی مثل دستورالعمل‌هایی برای توسعه‌دهندگان در برنامه‌نویسی شی‌گرا، و راه‌حل‌هایی برای حل مشکلات متداول در طراحی نرم‌افزار هستند(چقد سخت شد!!). الگوهای طراحی برای اولین بار توسط یک گروه چهار نفره به نام GOF معرفی شدند. این گروه در کتابی به معرفی 23 الگوی طراحی پرداختند و این 23 الگو را در 3 دسته الگوهای ایجادی(Creational)، الگوهای ساختاری(Structural) و الگوهای رفتاری(Behavioral) قرار دادند.

نکته :

الگوهای طراحی جزو معماری های نرم افزاری نیستند و فقط شیوه ای صحیح از کدنویسی شی گرا را ارائه میدن. بنابراین این الگوها فقط در قلمرو کدنویسی شی گرا وارد میشن و مستقل از زبان های برنامه نویسی هستند.(در هر زبانی که از شی گرایی پشتیبانی میکنه میشه ازشون استفاده کرد)

پیشنهاد میکنم حتماااااااااااااااااااااااااا به عنوان یه برنامه نویس حداقل یکبار هم که شده این الگوهارو مطالعه کرده و با ساختار اونها آشنا بشین.


خب حالا بریم سر وقت الگوی طراحی خودمون یعنی Repository pattern

روالی که توضیح خواهیم به این صورت خواهد بود :

1- یه تعریفی از این الگو میکنیم اینکه چیه و تاریخچش و ...

2- چرایی استفاده ازش

3- انواع پیاده سازیش

4- چالش¬های مطرح در این الگو

4- نحوه استفاده ازش رو در فریم روک لاراول توضیح میدیم.


معنی لغوی Repository pattern، مخزن یا لایه ای جهت ذخیره کردن اطلاعات میشه. Repositoryیک لایه میان لایه بیزنس (Business logic layer) و لایه دیتا (Data access layer) در پروژه شما ایجاد میکنه و هدفش جلوگیری از دسترسی مستقیم لایه بیزنس به لایه دیتاتون هستش. در واقع این لایه(Repository) مسئولیت کار با لایه دیتا رو به عهده داره و تمامی کدهای مربوط به کوئری ها و ... در این لایه نوشته میشن که در صورت نبودن این لایه تمامی این کارها بایستی در لایه بیزنس پروژتون انجام بشه.(برای درک بیشتر لایه بیزنس رو کلاس های Controller و لایه دیتا رو Model ها در لاراول در نظر بگیرین!)

Repository pattern structure
Repository pattern structure

طبق اولین اصل از اصول SOLID، یعنی Single responsibility هر کلاس مسئول انجام یک وظیفه هستش که با این وجود در لایه ای مثل Controller در ساختار MVC نبایستی منطق مربوط به لایه دیتا پیاده سازی بشه. بنابراین در الگوی Repository pattern این لایه(Controller) برای اجرای دستورات و یا فراخوانی داده ها از طریق Repository مورد نظرش درخواست های خودش رو مطرح میکنه.


چرایی استفاده از Repository Pattern

به صورت کلی اگر بخوایم مزایای استفاده از این الگو رو بگیم به این صورت میشه :

1- از تکرار شدن کوئری ها (مخصوصا کوئری های پیچیده) در سراسر برنامه جلوگیری میکنه و اونهارو در یک جا متمرکز می کنه

2- در آینده اگر به هر دلیلی بخواید معماری دسترسی به داده های دیتابیستون(ORM) رو تغییر بدین ،به راحتی میتونین با انجام یکسری تغییرات این کار رو انجام بدین

3- در پروژه های بزرگ ممکنه شما از انواع مختلفی از دیتابیس ها در پروژتون استفاده کنید با استفاده از پیاده سازی این الگو میتونید بدون هیچ استرس و تغییرات زیادی این کار رو انجام بدین.

4- با توجه به رعایت شدن اصل Single Responsibility ، به راحتی میتونیم برای کلاس هامون Unit Test بنویسیم.


انواع پیاده سازیش

برای پیاده سازی این الگو در چارچوب لاراول تفکرات زیادی وجود داره.

روش اول

یکی از این تفکرات ساخت یک کلاس جامع برای تمامی مدل هامون در پروژه هستش که این کلاس بایستی از یک قانون(contract/interface) به صورت کلی پیروی کنه. عملکردهایی که در ارتباط با دیتابیس(منظور کوئری هایی که داریمه) وجود دارند در این کلاس نوشته میشه.

در نهایت با استفاده از Service Container لاراول این دو کلاس عزیزمون رو بهم در هسته لاراول(provider) bind میکنیم و در آخر هم قانون ساخته شده رو در کنترلر مد نظرمون Inject میکنیم که با اینکار به راحتی میتونیم از قابلیت های کلاس Repository مون استفاده کنیم.


مزایا :

در این سناریو تمامی متدهای مورد نیازمون در یک کلاس جمع آوری میشه که با این کار یه جورایی از پراکندگی کدها جلوگیری میشه!(همه نیازمندی¬ها داخل یک کلاس هستند)

بر اساس نظر خودم و برخی از اساتید این روش رو معمولا در پروژه هایی به کار میگیرن که عملیات خوندن روی دیتاهاشون زیاد ندارن و بیشتر علاقه به نوشتن دارن.

معایب :

یکی از بزرگترین معایبی که این سناریو داره محدود کردن مدل¬ها به استفاده از متدهای تعریف شده در کلاس ایجاد شده هستش. همچنین باعث میشه کلاس ساخته شده به شدت بزرگ و شلوغ بشه!

چون ممکنه هر مدل سناریو و ساز و کارهای(کوئری) مربوط به خودش رو برای ارتباط با دیتابیس داشته باشه پس در نتیجه منطقی به نظر نمیرسه که برای کلیه مدل هامون از یک کلاس استفاده کنیم.

باز بر اساس نظر خودم و برخی دیگر از اساتید، استفاده از این روش رو معمولا در پروژه های با اندازه بزرگ که عملیات خواندن دیتاهاشون زیاده هستش پیشنهاد میدم.

روش دوم

به ازای هر مدل یک Repository و حداقل یک قانون(contract/interface) داریم که این کلاس بایستی از یک کلاس انتزاعی(abstract) ارث بری(extend) کند و همچنین این کلاس انتزاعی هم بایستی به صورت جداگانه برای خودش یک قانون رو پیاده سازی کنه.

حال به توضیح هر کدام از کارهای گفته شده می¬پردازیم.

با کلاس انتزاعی شروع کنیم...(چرا و به چه دلیل؟)

این کلاس در واقع یک بیسی برای تمامی کلاس های Repository مون هستش که داخلش عملیات CRUD و همچنین برخی از کوئری های پر تکرار به صورت جامع پیاده سازی میشه.

این کلاس همچنین وظیفه ساخت آبجکت از مدل هامون رو هم به عهده داره که در واقع با این کارش هست که میتونه کوئری های مختلف رو داخل خودش هندل کنه.

قانونی که این کلاس پیاده سازی میکنه در واقع بدنه متدهای CRUD جهت ارتباط Repository ها با دیتابیس ها هستش.(هر دیتابیسی عملیات CRUD رو برای جداولش داره).

گفتیم هر Repository حداقل یک قانون رو بایستی پیاده سازی کنه که در این قانون عملیات های مختص هر Repository نوشته میشه و بسته به مدلش و دید برنامه نویس میتونه هر چیزی باشه!

همچنین این قانون در آخر بایستی در هسته لاراول به کلاس Repository مون bind بشه تا در کانستراکتور Controller مربوطش بتونیم از کلاسش استفاده کنیم.


چالش های مطرح در استفاده از این الگو

از مباحثی که حین استفاده از این الگو وجود داره موضوع ذخیره دیتاها در دیتابیس های رابطه ای هستش. یکسری از اساتید موافق پیاده سازی متدهای مربوط به ذخیره داده های رابطه ای درون خود کلاس های Repository هستن و برخی این امر رو بیهوده و اشتباه میدونن! حالا چرا؟؟

چون زمانیکه بخوایم این متدها رو ایجاد کنیم بایستی از همان روابط پیاده شده در مدل هامون برای ذخیره اطلاعات استفاده کنیم(دوباره کاری میشه، تکرار کد و ...)

اگر کسی برای اولین بار بخواد اینکار رو انجام بده احتمالا با یک دیوار فکری مواجه میشه که : هدف از این الگو کاهش کدهای تکراری هستش، پس چرا ما باید این تکرار رو جهت ذخیره داده هامون داشته باشیم؟؟

اما موضوع جالبتر اینجاست که ما نخوایم عملیات ذخیره این داده هارو درون کلاسRepository مون انجام بدیم!! خب پس کجا باید بنویسیمشون؟؟!

داخل کنترلر!! واقعا؟؟ اینجوری که نمیشه!! ساختار الگو زیر سوال میره!!

من هم خودم این موضوع رو مثل شما قبول دارم اما برخی از اساتید استفاده از این الگو رو بیشتر جهت فراخوانی یا Read کردن اطلاعات پیشنهاد میدن تا ذخیره یا Write کردنشون! البته که منظور از ذخیره کردن اینجا بیشتر برای داده هایی که نیاز به استفاده از relation ها برای ذخیره دارند به کار میره.

با این تفسیر شما بایستی پروژتون رو از لحاظ حجم read or write کردن تحلیل کنین و با استفاده از نتیجه به دست اومده تصمیم بگیرین که آیا از این الگو استفاده کنین یا خیر!


خب تا اینجا سعی کردیم مباحث تئوری مربوط به این الگو رو براتون توضیح بدم، از اینجا به بعد بایستی دست به کد بشین و کارایی که من انجام میدم رو شما هم مرحله به مرحله انجام بدین.

اولین کاری که باید انجام بدیم تنظیم کردن ساختار دایرکتوری هامون برای ساخت کلاس های مربوط به این الگو هستش.

- app
--- Http
--- Repositories
------ ModelName
--------- ModelRepository.php
--------- Contracts
------------ Interface1.php
------------ Interface2.php
------ RepositoryServiceProvider.php
------ BaseRepositoryInterface.php
------ BaseRepository.php

بحث ساختار دایرکتوریها سلقیه ای هستش اما این ساختاری که اینجا میبینید یه جورایی best practice هستش!

کلیت این ساختار هم به این صورت هستش که به ازای هر مدل یک دایرکتوری وجود داره که داخلش یک کلاس Repository با عنوان ModelRepository هست و همچنین یک دایرکتوری با عنوان Contracts باید داشته باشیم که داخلش قانون های مختص اون ModelRepository وجود داره.

3 تا کلاس هم با عناوین BaseRepository & BaseInterfaceRepository & RepositoryServiceProvier داریم که توضیحات دوتا از این کلاس هارو بالاتر گفتم و فقط توضیح یکیش مونده که اون رو هم در ادامه توضیح خواهم داد.

اول از کلاس BaseInterfaceRepository شروع میکنیم که گفته بودیم داخلش یکسری متدهای ثابت(CRUD) رو مینویسیم.

interface BaseRepositoryInterface
{
public function read($id);
public function readAll();
public function add(array $data);
public function update($id);
public function remove($id);
public function removeRange(array $id)
}

خب همونجور که میبینید متدهایی رو داخل این کلاس نوشتیم که برای کار با هر دیتابیسی پاسخگوی نیازهامون هستش.

کلاس BaseRepository علاوه بر متدهایی که بایستی(مجبورا) پیاده سازی کنه، میتونه هر متد جامع دیگه ای که برای کار با مدل ها نیاز هستش رو داشته باشه.(مثلا متد findBy یا هر متد دیگه ای که فکر میکنید نیاز هستش) .

همچنین میتونین متدهای دیگه ای رو مثل paginate یا ... برای زمانیکه که میخواین روی دیتاهاتون فیلتری اعمال کنین در نظر بگیرین.(تمامی عملکردهایی که تا دیروز داخل کنترلرهامون انجام میدادیم میتونیم تحت عنوان یک متد داخل Repository هامون داشته باشیم و هر کدوم رو بسته به نیازمون ازش استفاده کنیم.)


abstract class BaseRepository implements BaseRepositoryInterface
{
private $model;
protected function setModel(Model $model)
{
      $this->model = $model;
      return $this;
}
public function getModel()
{
      return $this->model;
}
public function read($id){}
public function readAll()
{
      $this->model->sortBy(‘created_at’)->all();
}
public function add(array $data) {}
public function remove($id){}
public function removeRange(array $id) {}
public function update($id){}
}


حال به سراغ پیاده سازی یکی از کلاس های Repository مون میریم. من در اینجا مدل فرضی Challenge رو در نظر گرفتم.

class ChallengeRepository extends BaseRepository implements ChallengeRepositoryContract
{
public function __construct(Challenge $challenge)
{
    $this->setModel($challenge);
}
public function getChallengesWithProducts(Request $request){}
public function add(array $data)
{
   parent::add($data);
}
}


شما در این کلاس میتونین از متدهای پیاده شده در کلاس والد(BaseRepository) استفاده کنید یا اگر ساز و کار دیگه ای رو از تابع پیاده شده مد نظر دارین میتونین اون متد رو override کنید و بجاش ساز و کار خودتون رو پیاده سازی کنین.

در کلاس یک متد نام getChallengesWithProducts وجود داره که این متد ساخته شده در
ChallengeRepositoryContract هستش که بسته به نیاز مدلمون براش در نظر گرفتیم.

بعد از ساخت این کلاس و همچنین قانونش کافیه که این دوتارو در هسته لاراول بهم Bind کنیم.

کلاس RepositoryServiceProvider در واقع برای انجام همچنین کاری ساخته شده.

class RepositoryServiceProvider extends ServiceProvider
{
public function register(){}
public function boot()
{
   $this->app->bind(
        ChallengeRepositoryContract::class,
        ChallengeRepository::class);
}
}

به ورودی های متد bind دقت کنین، چرا که در صورت جابجا نوشتن کلاس ها با خطای لاراول مواجه میشین.

(کلاس اول در واقع abstract class شما هستش و کلاس دوم کلاسی هست که از این abstract class ارث بری کرده!)

برای اینکه این موضوع کامل تر براتون جا بیوفته در خصوص Service Container لاراول قشنگ تحقیق کنین.

نکته ای که باید بهش توجه بکنین این هستش که بعد از ساخت این ServiceProvider بایستی به لیست سرویس های لاورال اضافش کنین تا حین بوت شدن پروژتون بتونین از این سرویس هم استفاده کنین...(داخل فایل app.php)

خب دیگه همه چی اماده هستش برای اینکه ما بتونیم از Repository مون داخل کنترلر استفاده کنیم. فقط کافیست داخل متد کانستراکتور کنترلمون، ChallengeRepositoryContract رو اینجکت کنیم تا به راحتی بتونیم به متدهای مورد نیازمون دسترسی داشته باشیم.

ممنون از توجهتون.

راستی تا یادم نرفته اینرو هم بگم که برای استفاده از این سناریو من پکیجی رو اماده کردم که پیاده سازی رو براتون راحتتر میکنه و میتونید داخل پروژه هاتون ازش استفاده کنید.

https://github.com/matin-kh73/RepositoryPattern

موفق باشید.