آموزش دیزاین پترن ریپازیتوری در لاراول

مقدمهآموزش‌های زیادی به زبان فارسی و انگلیسی در این مورد وجود داره، ولی هیچ‌کدوم کامل نیستن و یا به نکاتی که در پروژه‌های واقعی ما رو با چالش روبرو می کنند هیچ اشاره ای نشده.ساختاری رو که قصد دارم شرح بدم توسط چندین برنامه نویس بازنگری شده. اگر دغدغه شما هم اینه که سورس کد تمیز و قابل توسعه‌ای بنویسید، این ساختار خیلی میتونه به شما کمک کنه.هیچ لزومی نداره که داخل پروژه‌های کوچیک از این الگو استفاده کنیم، اما به عنوان توسعه‌دهنده لازمه که یکی از کاربردی‌ترین الگو‌های برنامه‌نویسی رو بشناسیم.هدف دیزاین‌پترن ریپازیتوری ریپازیتوری یک استراتژی برای دسترسی به داده فراهم می‌کنه تا بتونیم عملیات مربوط به دیتابیس رو انجام بدیم. به زبان ساده‌تر؛ این الگو یک wrapper روی Model ایجاد می‌کنه و باعث میشه اصل Solid تا حد زیادی رعایت بشه.در قسمت‌های مختلف یک پروژه عملیات‌هایی پر‌تکرار برای کار با دیتابیس وجود داره. این دیزاین‌پترن که در دستهٔ الگوهای ساختاری قرار میگیره، به ما کمک می‌کنه تا این کدها رو از لایهٔ سرویس جدا کنیم و به لایهٔ Data Access Layer انتقال بدیم. خیلی مزیت در این فرآیند هست که در ادامه بهش اشاره می‌کنیم.دلایل استفاده از دیزاین‌پترن ریپازیتوریرعایت اصل Single Responsibility (اصل اول Solid): این اصل میگه "هر کلاس باید یک وظیفه واحد داشته باشه". فرض کنید سرویسی داریم که اطلاعات کاربران رو دریافت می‌کنه و عملیاتی رو روی اون‌ها انجام میده و نتیجه رو به کنترلر برمیگردونه. خب اینجا این اصل نقض شد. سرویس وظیفه دریافت اطلاعات رو باید به کلاس دیگه‌ای واگذار کنه. با کمک ریپازیتوری این کار انجام میشه. رعایت اصل Open Closed (اصل دوم Solid): این اصل میگه "موجودیت‌های یک نرم‌افزار (مثل کلاس‌ها، ماژول‌ها، توابع و ...) باید برای 'توسعه‌دادن' باز و برای 'تغییر' بسته باشند". ریپازیتوری دقیقا از این اصل پیروی میکنه و کدهای ما قابلیت باز بودن رو دارن و در عین حال نمیشه اون‌ها رو تغییر داد.رعایت اصل DRY (مخفف Don't Repeat Yourself): این اصل میگه "کدهای تکراری ننویس". البته که همیشه به این سادگی هم نیست ولی به‌هر‌حال ریپازیتوری به ما کمک می‌کنه که از تکرار کدها جلوگیری کنیم.رعایت اصل Dependency Inversion (اصل پنجم Solid): توضیح این اصل یک پست جداگانه رو میطلبه. اما می تونیم به این جمله بسنده کنیم که " کلاس‌های سطح بالا نباید به کلاس‌های سطح پایین وابسته باشن؛ هر دو باید وابسته به انتزاع (Abstractions) باشن ". فرض کنید توی یه کنترلر به طور مستقیم با Model کار کنیم. اینجا وابستگی شدید بین کنترلر و مدل بوجود میاد. ریپازیتوری اینجا به کمک ما میاد تا به اون کنترلر کمک کنیم که وابسته به Model نباشه.کد ما تست‌پذیر میشه: وقتی قسمت‌های مختلف یک پروژه مستقل از هم باشند، به راحتی می تونیم برای هر قسمت Test‌های مجزا بنویسیم.قابلیت استفاده مجدد: جدا شدن کدهای مربوط به دیتابیس از لایه سرویس باعث میشه کدهای ما قابلیت استفاده مجدد در سرویس های مختلف رو داشته باشن. پیاده‌سازی دیزاین‌پترن ریپازیتوری در لاراولمرحله اول ایجاد یک ساختار مشخص برای کدهای عمومی استفاده از دیتابیسه.| -- app | -- Console | -- Exceptions | -- Http | -- Models | -- Providers | -- Repositories | -- Base | -- BaseRepository.php | -- BaseRepositoryInterface.phpدر مسیر App لاراول یک دایرکتوری به نام Repositories می کنیم. همهٔ فایل‌های مربوط به ریپازیتوری در این قسمت قرار می‌گیره. یک دایرکتوری دیگر به‌نام Base داخل آن میسازیم. در نهایت دو فایل به نام BaseRepository.php و BaseRepositoryInterface.php ایجاد می‌کنیم.در مرحله دوم کدهای زیر رو در این دو فایل می‌نویسیم.BaseRepositoryInterface.php<?php namespace App\Repositories\Base; interface BaseRepositoryInterface{ public function all(); public function get(array $condition); public function delete(int $id); public function save(array $data,int $id = null); public function saveMany(array $data); public function truncate(); }متد‌هایی که نوشتیم میتونه بیشتر از این باشه که توضیح هر کدوم باعث میشه از بحث اصلی دور بشیم. این نکته قابل ذکر هست که متدهای این کلاس بر اساس نیازهای ما ایجاد میشه و ممکنه در پروژه‌های مختلف کم یا زیاد بشن. یا پیاده‌سازی این متد‌ها کمی تغییر کنه. به‌هر‌حال این اینترفیس رو باید در فایل BaseRepository استفاده کنیم و اون‌ها رو به شکل زیر پیاده کنیم.BaseRepository.php<?php namespace App\Repositories\Base; use Illuminate\Database\Eloquent\Model; class BaseRepository implements BaseRepositoryInterface{ public function __construct(Model $model) { $this->model = $model; } public function all() { return $this->model->all(); } public function get(array $condition) { return $this->model->where($condition[0],$condition[1],$condition[2])->get(); } public function delete(int $id) { return $this->model->findOrFail($id)->delete(); } public function save(array $data, int $id = null) { if($id){ return $this->model->find($id)->update($data); } return $this->model->create($data); } public function saveMany(array $data) { return $this->model->insert($data); } public function truncate() { return $this->model->truncate(); } }با در نظر‌گرفتن اینکه متدهای نوشته‌شده کدهای پرتکرار ما هستن، کار ما با این دو تا فایل تمومه. مرحله سوم اینه که به ازای هر Entity در پروژه، ما یک دایرکتوری در مسیر app/Repositories ایجاد کنیم و یک فایل به نام Entity مورد نظرمون بسازیم. برای مثال ما قصد داریم Entity محصولات رو با ریپازیتوری پیاده کنیم. کافیه یک دایرکتوری در مسیر app/Repositories به نام Product ایجاد کنیم و فایل‌های ProductRepository.php و ProductRepositoryInterface.php رو بسازیم و کدهای زیر رو داخلش بنویسیم. این کار رو برای تمام موجودیت هامون باید انجام بدیم.ProductRepositoryInterface.php<?php namespace App\Repositories\Product; use App\Repositories\Base\BaseRepositoryInterface; interface ProductRepositoryInterface extends BaseRepositoryInterface { }ProductRepository.php<?php namespace App\Repositories\Product; use App\Models\ProductModel; use App\Repositories\Base\BaseRepository; use Illuminate\Database\Eloquent\Model; class ProductRepository extends BaseRepository implements ProductRepositoryInterface{ public Model $model; public function __construct(ProductModel $model) { $this->model = $model; } }اینجا یکم دقت بیشتری میطلبه. در فایل ProductRepositoryInterface ما فقط از اینترفیس Base ارث بری کردیم تا این ریپازیتوری رو مجبور کنیم که متدهای عمومی رو پیاده‌سازی کنه. سپس در فایل ProductRepository از اینترفیس جدیدی که ساختیم implement کردیم تا به متدهای عمومی که قبلا ایجاد کرده‌بودیم دسترسی داشته باشیم. اما ما کدهای عمومی رو قبلا ایجاد کردیم. پس نباید دوباره اون ها تعریف بشن (مگر در صورت نیاز). فقط کافیه از BaseRepository ارث بری کنیم تا به همهٔ متدهای نوشته شده دسترسی داشته باشیم. حالا در دو صورت می تونیم متدهای دیگه ای به این ریپازیتوری اضافه کنیم. حالت اول اینه که ما یک کوئری دیگه‌ای داریم که مختص این موجودیته. پس اون رو توی اینترفیس مینویسیم و در ریپازیتوری پیاده‌سازیش می‌کنیم. حالت دوم اینه که بخواهیم متدی رو تغییر بدیم. مثلا متد all که همهٔ رکوردها رو برمیگردوند، اینجا قراره به صورت صعودی براساس id برگردونه. می تونیم این متد رو اصطلاحا override کنیم (اصل open - closed).مرحله چهارم و مرحله آخر اینه که از این ریپازیتوری ها در لایهٔ سرویس استفاده کنیم. فقط کافیه که در متد construct سرویس، یه آبجکت از ریپازیتوری مورد نظرمون بسازیم. مانند کد زیر:ProductService.phppublic ProductRepositoryInterface $productRepository; public function __construct() { $this->productRepository = App::make(ProductRepositoryInterface::class); }کلیت کاری که انجام دادیم مشخصه. اما شاید براتون این سوال در مورد نحوه آبجکت‌ساختن پیش بیاد. خب ما می‌تونستیم از این کد استفاده کنیم:$this->productRepository = new ProductRepositoryInterface();دلیل این کار Dependency Injection بود. ما هیچ پارامتری پاس ندادیم. البته تو این مثال فرقی نمیکنه، چون ریپازیتوری هیچ‌وقت قرار نیست از ما ورودی بگیره. اینترفیس‌های ما داخل construct خودشون فقط از یک Model یک شئ خالی میسازن. اما خواستم به این نکته اشاره کنم که سعی کنید آبجکت‌هاتون همیشه به این شکل ساخته بشن. این روش آبجکت‌سازی، هر پارامتری که لازم باشه رو به صورت بازگشتی میسازه و تزریق وابستگی رو خودش مدیریت میکنه.حالا ما می تونیم توی متد مورد نظرمون از این ریپازیتوری استفاده کنیم.... public function get(array $condition) { return $this->productRepository->get($condition); } ...مقدار آرایه condition بهتره که از کنترلر بیاد. اما همینجا هم می تونیم یه condition مشخصی ایجاد کنیم و به ریپازیتوری پاس بدیم.ProductController<?php namespace App\Http\Controllers; use App\Services\Product\ProductService; class ProductController extends Controller { public function __construct() { $this->productService = App::make(ProductService::class); } public function get(Request $request) { $categoryId = $request->input(&quotcategory-id&quot); return $this->productService->get([&quotcategory_id&quot,&quot=&quot,$categoryId]); } ... }دلیل استفاده از سرویس این بود که قسمت business پروژه نباید داخل کنترلر پیاده‌سازی بشه و کنترلر صرفا از طریق لایهٔ سرویس داده‌های مورد نظرش رو میسازه و به فرانت پروژه ارسال می‌کنه.خب کارهای لازم رو انجام دادیم، ولی اگه این متد رو از طریق یه route اجرا کنیم ارور زیر رو میگیریم:متن ارور که مشخصه و برای رفع این ارور باید فایل های ریپازیتوری که ایجاد می کنیم رو اصطلاحا bind کنیم. برای این کار با دستور زیر یک Provider جدید می نویسیم که کارهای مربوط به bind کردن رو انجام بده.php artisan make:provider RepositoryServiceProviderبعد از این کار برای اینکه از این provider جدید استفاده کنیم باید فایل app.php که در دایرکتوری config قرار داره رو ویرایش کنیم و این provider رو معرفی کنیم:'providers' => [ ... /* * Custom Service Providers... */ App\Providers\RepositoryServiceProvider::class, ...در مرحله بعد فقط کافیه در متد register این provider که ساختیم کد زیر رو مینویسیم$this->app->bind( ProductRepositoryInterface::class, ProductRepository::class, );کارمون تموم شد. حالا ریپازیتوری های ما instantiable هستن و دیگه بدون مشکل کد ما کار خواهد کرد.اشتباهات رایج در استفاده از الگوی ریپازیتوریتوی کنترلر مستقیما از ریپازیتوری آبجکت میسازن و Business پروژه رو میارن توی کنترلر.این الگو فقط یک لایه بین دیتابیس و سرویس شماست و وظیفه سفارشی‌کردن داده‌ها و پردازش اون‌ها رو نداره. پس متدهایی مثل GetPostForCategoryPage رو نباید توی ریپازیتوری پیاده‌سازی کنین.متدهای عمومی ریپازیتوری باید داخل فایلی به نام Base قرار بگیره و نباید در ریپازیتوری های مختلف تکرار بشه. این کار از تکرار کد جلوگیری میکنه.نتیجه گیری سعی کردم با زبان ساده این دیزاین‌پترن رو توضیح بدم. در کنارش کمی هم از اصول Solid و clean code صحبت کردم که هر کدوم نیاز به توضیح مفصل دارن. اگه این مطلب بهتون کمک کرد حتما کامنت بزارید و یا سوال خودتون رو بپرسید.این قسمت از پست رو باز میزارم تا بعدا در مورد اتصال ریپازیتوری به سیستم caching و توضیحات بیشتر در مورد clean code و آموزش های مرتبط دیگه لینک بزارم.