چرا باید از Design Patternها و اصول SOLID در توسعه نرم افزار استفاده کنیم؟
قبلا در رابطه با اصول SOLID و اینکه مفهوم الگوی طراحی یا Design pattern چیست، صحبت کردیم، وقتی با استفاده از اصول SOLID یک کلاس را پیاده سازی میکنیم باید به این نتیجه برسیم که تغییر یک کلاس به خاطر Logic آن باشد نه نحوه ارتباط با دیتابیس، دریافت خروجی، کش و … و همچنین در برنامهای که مینویسیم هرچقدر decoupled را بیشتر رعایت کنیم توسعه برنامه بهتر انجام میشود با رعایت اصول SOLID میتوان گفت یک برنامه OOP استاندارد نوشته شده است که مشکلات توسعه و بهینه سازی آن بسیار کم است. تا جایی که برای ما مقدور و امکان پذیر است تغییرات نرمافزار ما باید مربوط به تغییرات لاجیک برنامه باشد نه تغییرات ساختار برنامه نویسی.
لازم به ذکر است که رعایت اصول SOLID در برنامه نویسی بیشترین اولویت را دارد، حتی Design patternها بعد از این اصول اهمیت پیدا میکنند. در برنامههای نرم افزاری بزرگ تغییرات نرمافزار برای بهبود کارایی و پرفرمنس بسیار اتفاق میافتد و مثالهای زیادی از شرکتهای بزرگی برای این تغییرات و جود دارد.
یکی از مواردی که از ابتدای شروع کار باید به آن دقت کنیم به طوری توسعه در آینده راحتتر شود نحوه کار با دیتاها در پروژه نرمافزاری است. برای مثال ما بر روی دیتابیس Mysql کار میکنیم، در آینده به خاطر بهبود پرفرمنس بخشی یا کل دیتاها را به دیتابیس دیگری مثل MongoDB انتقال میدهیم. اینکار زمانبر، پرهزینه و خسته کننده است.
برای همچین مشکلاتی دیزاینپترنها مطرح شده اند، برای پیشبینی این مورد و تغییرات در آینده از دیزاین پترنی به نام Repository Pattern استفاده میکنیم، در واقع ما برای ارتباط با دادهها و اعمال تغییرات و… با ریپازیتوری در ارتباط خواهیم بود و نحوه ارتباط و تغییرات را ریپازیتوری مدیریت میکند. برنامه ما برای ارتباط با دادهها با ریپازیتوری ارتباط برقرار میکند و برای برنامه مهم نیست که ساختار دادهها و نوع دیتابیس چه چیزی است و ریپازیتوری خروجی را به برنامه پاس میدهد، از طرفی Modelهایی که در لاراول وجود دارند تنها برای Eloquent و لاراول هستند و به دیتابیسهایی که لاراول پشتیبانی میکند متصل میشوند پس ما باید با استفاده از این دیزاین پترن اقدامی را انجام دهیم که این وابستگی حذف شود و تمام تراکنشهای دیتابیس از طریق ریپازیتوری هندل شود.
ریپازیتوری میتواند در هرجایی باشد که داده در درون آن ذخیره شود، میخواهیم کاری واحد و مشخص را به شکلهای مختلف انجام دهیم در این مواقع ما باید قراردادی داشته باشیم که مشخص کنیم که تراکنشهای دادهای به چه شکلی باشد و یا چه قابلیتهایی وجود داشته باشد. قرارداد مشخص میکند چه کارهایی قابل انجام است ولی مشخص نمیکند به چه روشی! برای مثال تصور کنید از A میخواهیم به B برسیم، مهم حرکت و رسیدن به B است اینکه به چه روشی و از چه راهی حرکت کنیم میتواند متفاوت باشد. یکسری عملیات مشترک وجود دارد ولی هر درایور و دیتابیسی به روش خود اینکار را انجام میدهد، Mysql, Sql, MongoDB, … به روش خود اینکار را انجام میدهند، در این حالت قرارداد به ما کمک میکند و مشخص میکند که چه کارهایی قابل انجام است ولی اشاره ای به روش انجام آن ندارد.
پیاده سازی دیزاینپترن ریپازیتوری در لاراول (Laravel Repository Pattern)
فایلهای ریپازیتوری را در مسیر app/Repositories تعریف میکنیم. فولدری به نام Contracts ایجاد میکنیم و قراردادها را در داخل آنها قرار میدهیم.
نکته: وقتی صحبت از قرارداد میشود به Interfaceها میرسیم.
نکته: وقتی یک Interface را پیاده سازی میکنیم تا حد امکان تعداد متدها حداقل باشد.
interface RepositoryInterface
{
public function find(int $ID);
public function store(array $item);
public function update(int $ID, array $item);
public function delete(int $ID);
}
در قرارداد بالا ما چهار عمل اصلی که لازم است را تعریف کردیم اینکه به چه روشی انجام شود در قرارداد نامشخص است. هر کلاسی که از این قرارداد implement کند باید متدهای این قرارداد را رعایت کند.
در لاراول ما از Eloquent استفاده میکنیم،فرض کنید که دیتابیس ما عوض نشود و بخواهیم از Eloquent به Doctrine مهاجرت کنیم، در این حالت دیتابیس ثابت است و نحوه ارتباط و تعامل با دیتابیس متفاوت است. با توجه به این مورد ما قرارداد دیگری به نام EloquentBaseRepository را تعریف میکنیم که از قرارداد اصلی ما پیروی میکند.
namespace App\Repositories\Contracts;
use Illuminate\Support\Facades\DB;
class EloquentBaseRepository implements RepositoryInterface
{
protected $model;
public function find(int $ID)
{
return $this->model::find($ID);
}
public function store(array $item)
{
return $this->model::create($item);
}
public function update(int $ID, array $item)
{
// ...
}
public function delete(int $ID)
{
// ...
}
}
با توجه به اینکه این قرارداد Eloquent برای همه کنترلرهای لاراول استفاده میشود باید به شیوه ای پیاده سازی شود که برای همه قابل استفاده باشد برای مثال عمل find یک اقدامی است که نحوه انجام برای همه یکسان است و تنها Model آنها متفاوت است پس یک متغیر برای Model تعریف میکنیم که بتوانیم Model را تعریف کنیم. ( protected $model )
برای مثال وقتی کلاسی برای کاربران تعریف کنیم که از این EloquentBaseRepository پیروی کند به صورت زیر خواهد بود:
class EloquentUserRepository extends EloquentBaseRepository{
protected $model = User::class;
}
سوالی که مطرح میشود به این صورت است که اگر ما بخواهیم لیست و یا گزارشی از کاربران بگیریم که منحصرا برای کاربران باشد کجا باید به آن اطلاعات دسترسی پیدا کنیم؟ با توجه به قراردادی که داریم و ساختاری که تعریف کردیم ما باید اطلاعات را از مخرن دریافت کنیم! برای دریافت کاربران فعال متد زیر را در کلاس بالا که تعریف کردیم قرار میدهیم.
class EloquentUserRepository extends EloquentBaseRepository{
protected $model = User::class;
public function activeUser()
{
// ...
}
}
در اینجا ما در EloquentUserRepository کاربران فعال را دریافت میکنیم اما سوالی که باز پیش میآید این است اگر دیتابیس ما تغییر کرد و با توجه به اینکه ما activeUser را در EloquentUserRepository تعریف کردیم هیچ تصمینی وجود ندارد که این متد را برای دیتابیس جدید هم داشته باشیم یعنی جزو قرارداد سیستم نیست! یعنی اگر ما از Mongo استفاده کنیم Mongo از قرارداد اصلی پیروی میکند و اجباری وجود ندارد که متدهای مهمی همچون کاربران فعال را پیاده سازی کند. از طرفی اگر آن را در قرارداد هم قرار دهیم با توجه به اینکه این متد فقط برای کاربران است باید کلاسهایی همچون product هم این متد را در کلاس خود داشته باشند درحالی که activeUser برای Product, Order , … معنی ندارد! و این قوانین SOLID را نقض میکند. ( Interface Segregation هیچ کلاسی نباید قراردادی را پیاده سازی کند که نیازی به آن ندارد.)
برای حل این مشکل، ما برای Model موردنظرمان یک قرارداد ایجاد میکنیم که از قرارداد اصلی extends کند. ( interfaceها میتوانند از هم extends کنند. )
namespace App\Repositories\Contracts;
use App\Repositories\Contracts\RepositoryInterface;
interface UserRepositoryInterface extends RepositoryInterface
{
public function activeUsers();
}
و بعد از آن EloquentUserRepository ما به صورت زیر تغییر داده خواهد شد.
namespace App\Repositories\Eloquent\Users;
use App\Models\User;
use App\Repositories\Contracts\EloquentBaseRepository;
use App\Repositories\Contracts\UserRepositoryInterface;
class EloquentUserRepository extends EloquentBaseRepository implements UserRepositoryInterface
{
protected $model = User::class;
public function activeUsers()
{
// TODO: Implement getActiveUsers() method.
}
}
با استفاده از ریپازیتوری پترن، کنترل بیشتری بر روی مدیریت دیتاهای پروژه داریم.
پس تا به اینجای کار به این نتیجه رسیدیم که:
- ارتباط ما برای تراکنشهای دادهای از طریق مخزن یا ریپازیتوری خواهد بود. با توجه به اینکه ما برای انجام هر اقدامی با توجه به دیتابیس و ارتباطی که با دیتابیس داریم میتواند روشهای مختلفی وجود داشته باشد ( مثلا عمل find یا create در دیتابیسهای مختلف یا ORMهایی همچون Eloquent, Doctrin ,… ) یک قرارداد مشخص کردیم که همگی از آن قرارداد باید پیروی کنند.
- با توجه به اینکه برای موجودیتهای مختلف میتوانیم اقدامات خاص اون موجودیت را داشته باشیم ( مثلا پیدا کردن محصولات پرفروش، کاربران آنلاین و…) برای هر Model یک قرارداد تعریف کردیم تا متد خاص آنها را در قرارداد مربوط به آن پیاده سازی کنیم. با اینکار هم قوانین SOLID را رعایت کردیم و هم با تغییر دیتابیس قراردادها وجود دارند که پیاده سازی شوند.
- وقتی از Interface استفاده میکنیم، در صورت تعویض دیتابیس از این مطمئن هستیم که متدهایی که در پروژه لازم داریم قطعا وجود خواهند داشت زیرا باید از قرارداد اصلی پیروی کنند. همچنین وقتی بخواهیم داده های بخشهای مختلف را روی دیتابیسهای مختلف ذخیره کنیم به راحتی میتوانیم با تعریف آن و با استفاده از این Design Pattern این کار را انجام دهیم، مثلا کاربران روی MySql باشد و محصولات بر روی PostgreSql باشد و …
نحوه استفاده از Repository Pattern در کنترلرهای پروژه
اکنون که ساختار فایلها را ایجاد کردیم میخواهیم در کنترلرهای برنامه از آن استفاده کنیم، برای مثال در کلاس زیر که لیست نقشها را نمایش میدهد به صورت زیر از آن استفاده میکنیم.
class RolesController extends AdminCoreController
{
protected $permissionRepository;
protected $roleRepository;
public function __construct(
PermissionRepositoryInterface $permission,
RoleRepositoryInterface $role)
{
parent::__construct();
$this->permissionRepository = $permission;
$this->roleRepository = $role;
}
public function index()
{
$roles = $this->roleRepository->all();
return view('user::admin.role.index', compact('roles'));
}
}
همانطور که مشاهده میکنید در Construct کلاس، Interfaceی که مربوط به نقشها است را Inject میکنیم و پس از آن با استفاده از متدهایی که در قرارداد اصلی برنامه وجود دارد لیست کامل نقشها را دریافت میکنیم. اما چگونه این اتقاق رخ میدهد؟! ما قبل از استفاده باید هر interface را به کلاس موردنظر bind کنیم پس در فایل Service Provider برنامه و در متد boot آنها را به صورت زیر bind میکنیم.
public function boot()
{
$this->app->bind(\App\Repositories\Contracts\RoleRepositoryInterface::class, \App\Repositories\Eloquent\RoleRepositoryEloquent::class);
}