5 الگوی رایج طراحی در برنامه‌های PHP

5 الگوی رایج طراحی در برنامه‌های PHP
آفلاین
user-avatar
عرفان حشمتی
31 شهریور 1399, خواندن در 8 دقیقه

اگر فکر می‌کنید الگوی شماره یک سینگلتون است، سخت در اشتباه هستید! الگوی سینگلتون دیگر از مد افتاده و جوابگو نیست و کسی آن را دنبال نمی‌کند.

اکنون بیایید به 5 الگوی طراحی رایج در دنیای پی اچ پی نگاهی بیندازیم.

Factory

شما هنگام ساختن یک شی باید از فکتوری‌ها استفاده کنید. شما نمی‌خواهید فقط یک فکتوری داشته باشید تا یک شی جدید ایجاد کنید. هنگام ساختن شی، ابتدا آن را ایجاد می‌کنید و سپس آن را استفاده می‌کنید. معمولاً لازم است چندین مرحله انجام شود و منطق خاصی اعمال شود. با این کار، معقول است که همه موارد را در یک ظرف داشته باشید و هر زمان که بخواهید یک شی جدید به همان روش ساخته شده و دوباره از آن استفاده کنید. در اصل، این نکته الگوی فکتوری است.

این ایده خوبی است که یک رابط برای فکتوری خود داشته باشید و کد شما به آن وابسته باشد و نه به چیز دیگری. با این کار، هر زمان که بخواهید می‌توانید به راحتی یک فکتوری دیگر را جایگزین آن کنید.

در ادامه، رابط فکتوری خود را با کلاس زیر پیاده‌سازی می‌کنیم:

interface FriendFactoryInterface {
    public function create() : Friend
}

این الگوی طراحی بسیار ساده و در عین حال قدرتمند است.

Strategy

برای مخفی کردن جزئیات الگوریتم‌های مورد نیاز برای انجام یک عمل استفاده می‌شود. با داشتن استراتژی‌ها، کاربر می‌تواند الگوریتم مورد نیاز خود را بدون پیاده‌سازی واقعی انتخاب کند و آن را برای انجام عملیات بکار ببرد.

در اینجا ما نیاز به ایجاد یک کتابخانه داریم که داده‌ها را از یک منبع داده به منبع دیگری منتقل کند. به عنوان مثال، ما باید داده‌ها را از دیتابیس به فایل csv یا از صفحه گسترده به فایل json انتقال دهیم. اما چگونه این کار را انجام دهیم؟

در مرحله اول، برای خواندن داده‌ها باید استراتژی‌های مربوطه را ایجاد کنیم. بیایید آن‌ها را ریدر بنامیم. در مرحله بعد هم برای نوشتن داده‌ها باید استراتژی‌های مربوطه را ایجاد کنیم و آن‌ها را رایتر می‌نامیم.

بنابراین، ما 2 ریدر برای خواندن داده‌ها یا از پایگاه داده یا از صفحه گسترده داریم. بر این اساس، ما 2 رایتر نیز برای نوشتن داده‌ها یا در فایل csv یا در فایل json خواهیم داشت.

نکته مهم: کاربری که با استراتژی‌های ما همکاری می‌کند، نباید به اجرای آن‌ها اهمیت دهد. بنابراین، ما باید رابط‌هایی را برای استراتژی‌های خود تعریف کنیم. از این طریق کاربر فقط با روش‌های تعریف شده توسط رابط‌های استراتژی آشنا خواهد شد و فقط با آن‌ها کار خواهد کرد و آنچه در پشت صحنه اتفاق می‌افتد مشکل‌ساز نیست.

سرانجام، باید کاربر را مجاب کنیم که استراتژی‌های لازم را بر اساس مکان مورد نیاز برای انتقال داده‌ها انتخاب کند.

بیایید همه این موارد را در عمل مشاهده کنید:

interface ReaderInterface { 
    public function start() : void;
    public function read() : array;
    public function stop() : void;
}
interface WriterInterface {
   public function start() : void;
   public function write(array $data) : void;
   public function stop() : void;
}
class DatabaseReader implements ReaderInterface {
    ...
}
class SpreadsheetReader implements ReaderInterface {
    ...
}
class CsvWriter implements WriterInterface {
    ...
}
class JsonWriter implements WriterInterface {
    ...
}
class Transformer {
    
    ...
    public function transform(string $from, string $to) : void {
        $reader = $this->findReader($from);
        $writer = $this->findWriter($to);
        
        $reader->start();
        $writer->start();
        try {
            foreach ($reader->read() as $row) {
                $writer->write($row);
            }
         } finally {
             $writer->stop();
             $reader->stop();
         }
     }
     ...
}

همانطور که مشاهده می‌کنید، کاربر واقعا به پیاده سازی‌هایی که با آن کار می‌کند اهمیتی نمی‌دهد. تمام آنچه که به آن اهمیت می‌دهد روش‌هایی است که توسط رابط‌های استراتژی ما تعریف شده است.

Adapter

برای تبدیل یک رابط خارجی به یک رابط مشترک استفاده می‌شود. فرض کنیم که در این پروژه با استفاده از کلاس زیر داده‌ها را از فضای ذخیره دریافت می‌کنید.

class Storage {
    private $source;
    
    public function __constructor(AdapterInterface $source) {
        $this->source = $source;
    }
    public function getOne(int $id) : ?object {
        return $this->source->find($id);
    }
    
    public function getAll(array $criteria = []) : Collection {
        return $this->source->findAll($criteria);
    }
}

توجه داشته باشید که ذخیره‌سازی به طور مستقیم با منبع کار نمی‌کند بلکه در عوض با آداپتور منبع کار می‌کند.

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

در اینجا مثالی از رابط آداپتور آورده شده است:

interface AdapterInterface {
    public function find(int $id) : ?object;
    public function findAll(array $criteria = []) : Collection;
}

حال فرض کنیم که ما از یک کتابخانه برای دسترسی به پایگاه داده MySQL استفاده می‌کنیم. این کتابخانه رابط کاربری خودش را دیکته می‌کند و به صورت زیر به نظر می‌رسد:

$row = $mysql->fetchRow(...);
$data = $mysql->fetchAll(...);

همانطور که می‌بینید، ما نمی‌توانیم این کتابخانه را دقیقا مانند فضای ذخیره‌سازی خود ادغام کنیم. بلکه باید مانند زیر آداپتوری برای آن ایجاد کنیم:

class MySqlAdapter implements AdapterInterface {
    
     ...
     public function find(int $id) : ?object {
         
         $data = $this->mysql->fetchRow(['id' => $id]);
         // some data transformation
     }
     public function findAll(array $criteria = []) : Collection {
              
         $data = $this->mysql->fetchAll($criteria);
         // some data transformation
     }
   
     ...
}

سپس می‌توانیم دقیقا مثل زیر به storage تزریق کنیم:

$storage = new Storage(new MySqlAdapter($mysql));

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

Observer

برای اطلاع به سیستم در مورد وقایع خاص استفاده می‌شود. برای درک بهتر مزایای این الگو، دو راه‌حل در مورد همین مشکل را مرور می‌کنیم.

در اینجا نمایش فیلم به منتقدین را مثال می‌زنیم. کلاس تئاتر را با روش موجود تعریف می‌کنیم. قبل از نمایش فیلم می‌خواهیم برای تلفن‌های منتقدان پیام ارسال کنیم. سپس در وسط فیلم می‌خواهیم 5 دقیقه فیلم را متوقف کنیم تا منتقدین فرصت استراحت داشته باشند. سرانجام، پس از پایان فیلم می‌خواهیم از منتقدین بخواهیم بازخورد خود را ارسال کنند.

بیایید ببینیم که این مثال در کد چگونه به نظر می‌رسد:

class Theater {
   
    public function present(Movie $movie) : void {
       
        $critics = $movie->getCritics();
        $this->messenger->send($critics, '...');

        $movie->play();

        $movie->pause(5);
        $this->progress->break($critics)
        $movie->finish();

        $this->feedback->request($critics);
    }
}

تمیز و امیدوارکننده به نظر می‌رسد.

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

خوب، یکی از موضوعاتی که در اینجا وجود دارد این است که برای رسیدن به آن باید کلاس تئاتر خود را اصلاح کنیم و این اصول SOLID را می‌شکند. به ویژه، این اصل باز / بسته را نیز می‌شکند. علاوه بر آن، این رویکرد باعث می‌شود کلاس تئاتر به چندین سرویس اضافی وابسته شود که چندان خوب نیست.

چه می‌شود اگر همه چیز را وارونه کنیم. به جای اضافه کردن پیچیدگی و وابستگی هر چه بیشتر به کلاس تئاتر، پیچیدگی را در کل سیستم پخش خواهیم کرد و با این کار، وابستگی‌های کلاس تئاتر را به عنوان یک امتیاز کاهش می‌دهیم.

در اینجا اینگونه به نظر می‌رسد:

class Theater {
    
    public function present(Movie $movie) : void {
        
        $this->getEventManager()
            ->notify(new Event(Event::START, $movie));
        $movie->play();

        $movie->pause(5);
        $this->getEventManager()
            ->notify(new Event(Event::PAUSE, $movie));
        $movie->finish();

        $this->getEventManager()
            ->notify(new Event(Event::END, $movie));
    }
}
$theater = new Theater();
$theater
    ->getEventManager()
    ->listen(Event::START, new MessagesListener())
    ->listen(Event::START, new LightsListener())
    ->listen(Event::PAUSE, new BreakListener())    
    ->listen(Event::PAUSE, new AdvertisementListener())
    ->listen(Event::END, new FeedbackListener())
    ->listen(Event::END, new CleaningListener());
$theater->present($movie);

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

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

امیدواریم الگوی Observer برایتان مفید واقع شده باشد.

Decorator

هنگامی که می‌خواهید رفتار یک شی را در زمان اجرا تنظیم کنید، استفاده می‌شود و با این کار ارث‌بری‌های اضافی و تعداد کلاس‌ها کاهش پیدا می‌کند. ممکن است بپرسید چرا اصلا به آن احتیاج داریم؟ خوب، بهتر است با مثال‌هایی توضیح دهیم.

بیایید بگوییم که ما کلاس Window و Door را داریم و هر دو OpenerInterface را پیاده‌سازی می‌کنند.

interface OpenerInterface {
    public function open() : void;
}
class Door implements OpenerInterface {
    public function open() : void {
        // opens the door
    }
}
class Window implements OpenerInterface {
    public function open() : void {
        // opens the window
    }

هم پنجره‌ها و هم درها رفتاری یکسان دارند که باید باز شود. اکنون ما به درها و پنجره‌های دیگری با قابلیت‌های اضافی نیز احتیاج داریم که هنگام باز کردن درها یا پنجره‌ها، دمای بیرون آن را به کاربران نشان دهد. ما می‌توانیم این مسئله را با ارث‌بری درست مانند زیر حل کنیم:

class SmartDoor extends Door {
    public function open() : void {
        parent::open();
        $this->temperature();
    }
}
class SmartWindow extends Window {
    public function open() : void {
        parent::open();
        $this->temperature();
    }
}

اکنون 4 کلاس در کل داریم. با این حال، با الگوی دکوراتور می‌توانیم این مشکل را فقط با 3 کلاس حل کنیم که در اینجا آمده است:

class SmartOpener implements OpenerInterface  {
    
    private $opener;
    public function __construct(OpenerInterface $opener) {
        $this->opener = $opener;
    }
    
    public function open() : void {
        $this->opener->open();
        $this->temperature();
    }
}
$door = new Door();
$window = new Window();
$smartDoor = new SmartOpener($door);
$smartWindow = new SmartOpener($window);

ما نوع جدیدی از درب بازکن را معرفی کرده‌ایم که مانند پروکسی عمل می‌کند اما دارای عملکردی اضافی در بالای آن است. این همان چیزی است که این کار را می‌کند.

در صورت داشتن هر گونه نظر، سوال یا توصیه‌ای، آن را در بخش نظرات زیر ارسال کنید.

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
عرفان حشمتی @heshmati74
مهندس معماری سیستم های کامپیوتری، طراح و توسعه دهنده وب سایت
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو