Open-Closed Modification
به عنوان یکی از اصول در SOLID مطرح است یعنی مدلهای ما باید طوری طراحی شده باشند که بدون تغییرات در آن قابلیت توسعه را داشته باشند. یعنی ما بتوانید با استفاده از اصول طراحی OOP و با حفظ اصول SOLID توسعه را انجام دهیم. ما باید بتوانیم فانکشنالیتی را افزایش دهیم و Featureهای جدید اضافه کنیم بدون آنکه مدل را تغییر دهیم.
برای درک این دیزاین پترن یک مثال کاربردی در دنیای واقعی مطرح میکنیم. فرض کنید که به همراه دوستانمان به یک فستفود رفتیم و هر کدام از ما بر اساس سلیقه یک نوع برگر خاص سفارش داده است. قیمت پایه برگر ۴ یورو است و بر اساس نوع سفارش قیمتها به برگر اضافه میشود، مثلا برای چیزبرگر ما باید ۲۵ سِنت دیگه پرداخت کنیم.
برای اینکار میتوانید برای هر مورد ۴ یورو را به علاوه قیمتهای اضافه شده کنیم و برای هر کلاس یک متد محاسبه هزینه داشته باشیم ولی اصول درست پیاده سازی آن با استفاده از Decorator Pattern به صورت زیر است.
با توجه به اینکه برای هر کلاس ما یک متدی برای محاسبه هزینه باید داشته باشیم پس ما باید یک قرارداد داشته باشیم از طرفی وقتی یک کلاس پایه به اسم Burger داریم و میخواهیم قیمت سایر کلاسها بر اساس این کلاس پایه مشخص باشد پس باید داخل construct این کلاس را Inject کنیم.
interface FoodItem { public function cost(); } class Burger implements FoodItem { public function cost() { return 4; } }
حالا کلاس پایه و قرارداد مشخص شد سایر کلاسها را به صورت زیر تعریف میکنیم.
class Cheese implements FoodItem { protected $burger; public function __construct(FoodItem $burger) { $this->burger = $burger; } public function cost() { return $this->burger->cost() + 0.25; } } class Special implements FoodItem { protected $burger; public function __construct(FoodItem $burger) { $this->burger = $burger; } public function cost() { return $this->burger->cost() + 1; } }
حالا که کلاسها را ایجاد کردیم به صورت زیر میتوانیم از آنها استفاده کنیم:
$burger = new Burger(); $cheese_burger = new Cheese($burger); // passing burger $special_burger = new Special($burger); // passing burger $burger->cost(); // 4 $cheese_burger->cost(); // 4 + 0.25 = 4.25 $special_burger->cost(); // 4 + 1 = 5
حالا تصور کنید اگر از صاحب فست فود بخواهیم مخلفاتی که برای برگر مخصوص لحاظ میکند برای چیزبرگر ما هم لحاظ کنید چطوری محاسبه کنیم؟ میتوانیم به صورت دستی آن را محاسبه کنیم یا یک متد جدید بسازیم و ... ولی با اینکار اصل SOLUD را نقض کردیم با ساختاری که پیاده سازی کردیم این امکان به صورت زیر وجود دارد.
$burger = new Burger(); $cheese_burger = new Cheese($burger); $special_cheese_burger = new Special($cheese_burger); // passing cheese burger $special_cheese_burger->cost(); // 4 + 0.25 + 1 = 5.25
همانطور که میبینید اگر بخواهیم نوع دیگری از برگر را اضافه کنیم به راحتی میتوانیم اینکار را انجام دهیم. با اینکار مدلها قابلیت توسعه بدون تغییر در مدل را دارند.
یکی دیگر از مثالهایی که Jeffrey Way در سایت laracasts مطرح کرد به این صورت است که ما برای تعمیر و سرویس خودرو به یک تعمیرکار نیاز داریم و قیمت پایه ای برای سرویس مطرح است و بر اساس نیازهایی که خودرو دارد مثلا تعویض روغن و تغویض لاستیک و ... قیمتها اضافه میشود.
فرض کنید در مدل user (یا هر مدل دیگری در پروژه) یک ستون به نام status داریم که وضعیت را مشخص میکند ما مقدار این ستون را با اعداد مشخص میکنیم مثلا عدد ۱ برای فعال بودن، عدد ۰ برای در انتظار و ... نمایش این اعداد در قالب وبسایت برای مدیر یا کاربر وبسایت نامفهوم است، برای رفع این مشکل در مدل User آرایهای تعریف میکنیم که این مقادیر را کلید آرایه درنظر میگیریم و مقدار آنها را عباراتی مفهومی که کاربر متوجه شود مینویسیم.
public $statuses = [ 0 => 'غیرفعال', 1 => 'فعال', 2 => 'مسدود شده' ];
سوالی که مطرح است این است که اگر برای برخی بخشهای دیگر همچین حالتهای مشابهی داشته باشیم و یا این ستون را تغییر دهیم هر سری باید مدل را تغییر دهیم و یا به مدل Feature های جدید اضافه کنیم با اینکار یکی از قوانین SOLID به نام Open-Closed Modification را نقض کرده ایم. برای مدل User ما حالتها و ویژگیهای زیادی را اضافه خواهیم کرد مثلا با first_name , last_name بخواهیم full_name کاربر را نمایش دهیم و...
برای این موضوع با استفاده از مفهومی به نام presenter این مساله را حل میکنیم. در واقع ما این دیزاینپترن را پیادهسازی میکنیم تا برای تمام Modelهایی که داریم این اصل را رعایت کنیم که آنها را توسعه دهیم بدون آنکه خود Model را تغییر دهیم تا مدل بزرگ و بزرگتر شود.
ابتدا یک قرارداد ایجاد میکنیم:
namespace App\Presenters\Contracts; abstract class Presenter { protected $entity; public function __construct($entity) { $this->entity = $entity; } }
با توجه به اینکه با Model کار میکند ما در construct مدل را میگیریم. با توجه به اینکه از این Presenter قرار است یکسری Property فراخوانی شود و معلوم نیست که این Propertyها دقیقا چی هستند بنابراین در این Abstract از Magic method php به نام __get استفاده میکنیم.
__get() : is utilized for reading data from inaccessible properties.
این Magic method بیشتر برای error handling ایجاد شده است، تصور کنید ما یک property را فراخوانی میکنیم درحالیکه وجود نداشته باشد این Magic Method همه Propertyهایی که فراخوانی میشوند را دریافت میکند که ما با استفاده از آن میتوانیم اقداماتی را بر روی آنها انجام دهیم.
namespace App\Presenters\Contracts; abstract class Presenter { protected $entity; public function __construct($entity) { $this->entity = $entity; } public function __get($property) { if(method_exists($this,$property)) { return $this->{$property}(); } return $this->entity->{$property}; } }
همانطور که در __get() مشاهده میکنید چک میکنیم که اگر برای این property که فراخوانی شده است در کلاس Presenter هیچ متدی تعریف شده باشد آن را فراخوانی میکند در غیر اینصورت داخل Model موردنظر که به presenter پاس داده شده است property را پیدا میکند و آن را return میکند یعنی property که پاس داده میشود یا داخل presenter خود object است یا داخل خود object است.
با توجه به اینکه Presenterی که تعریف کردیم یک Abstract است نمیتوانیم از خود آن استفاده کنیم پس یک UserPresenter ایجاد میکنیم که از این Abstract ارثبری کند.
namespace App\Presenters\User; use App\Presenters\Contracts\Presenter; class UserPresenter extends Presenter { public function status() { if($this->entity->status == 0) { return 'غیر فعال'; } return 'فعال'; } }
حالا که UserPresenter را تعریف کردیم چگونه در قالبی که نمایش میدهیم از آن استفاده کنیم و یا اینکه چطور آن را به User Model وصل کنیم؟! باید UserPresenter را به User Model معرفی کنیم.
در User Model به صورت زیر presenter را معرفی میکنیم:
protected $presenter = UserPresenter::class;
برای اینکه کدها را تمیزتر بنویسم و استانداردتر باشد در کنار contract که تعریف کردیم یک trait ایجاد میکنیم. اسم trait را Presentable میگذاریم یعنی یک Model که قابلیت Present دارد.
کاربرد این فایل trait و متدی که داخل آن میگذاریم چیست؟ در واقع از طریق این فایل و متد داخل آن باید به Presenter مربوط به مدل دسترسی پیدا کنیم.
ابتدا داخل User Model این trait را use میکنیم. با توجه به اینکه در User Model ما کلاس UserPresenter را داخل متغیر presenter تعریف کردیم ابتدا چک میکنیم اگر یک Model از این trait استفاده کرده بود و این متغیر و کلاس آن را معرفی نکرده بود خطایی را رخ دهد در غیر اینصورت از آن کلاس که معرفی شده است یک نمونه ایجاد میکند و this که همان Modelی است که از آن استفاده کرده است را به آن پاس میدهد و از آنجایی که UserPresenter از کلاس Presenter ارث بری کرده است و متد construct داخل Presenter تعریف شده است داخل کلاس والد Model پاس داده میشود و object مورد نظر به اسم $entity ساخته میشود.
namespace App\Presenters\Contracts; trait Presentable { protected $presenterInstance; public function present() { if(!$this->presenter || !class_exists($this->presenter)) { throw new \Exception('presenter not found!'); } if(!$this->presenterInstance) { $this->presenterInstance = new $this->presenter($this); } return $this->presenterInstance; } }
پس از انجام این موارد داخل قالب blade به صورت زیر به $status که تعریف کردیم دسترسی پیدا میکنیم.
$user->present()->status
با استفاده از این دیزاینپترن ما علاوه بر رعایت قانون SOLID به این نتایج هم میرسیم که Modelما زیاد بزرگ نمیشود و نکته مهمتر اینکه راه برای اضافه کردن ویژگیهای جدید و توسعه باز خواهد بود، فرض کنید در آینده بخواهیم قیمتها را روی وب به ریال و روی موبایل به تومان نشان دهیم برای اینکار میتوانیم از Presenter استفاده کنیم یعنی در واقع بدون تغییر Source code اصلی راه را برای توسعه باز گذاشتیم.