سرفصل‌های آموزشی
آموزش الگوهای طراحی (Design Pattern)
آشنایی با الگوی طراحی Factory

آشنایی با الگوی طراحی Factory

در این آموزش قصد داریم تا یک الگوی طراحی تحت عنوان Factory Design Pattern را معرفی کرده و همچنین لزوم به‌کارگیریِ آن در برنامه‌نویسی شیئ‌گرا را شرح دهیم. به طور کلی، این الگوی طراحی زیرشاخۀ الگوهای Creational قرار می‌گیرد که در ادامه و در قالب مثالی کاربردی در زبان برنامه‌نویسی PHP آن را پیاده‌سازی خواهیم کرد.

در پاسخ به این پرسش که «اساساً در چه شرایطی از دیزاین پترن فکتوری در توسعۀ اپلیکیشن استفاده می‌کنیم؟» باید گفت در صورتی که بخواهیم هر یک از کلاس‌های پیاده‌سازی‌شده در اپلیکیشن‌مان وظایف واحدی را بر عهده داشته باشند که این امر خود بر قانونی تحت عنوان Single Responsibility Principle یا به اختصار SRP اشاره دارد که برای کسب اطلاعات بیشتر در این رابطه می‌توانید به دورهٔ آموزش قوانین SOLID مراجعه نمایید.

در همین راستا، دیزاین پترن فکتوری را برای پیاده‌سازی سناریویی فرضی به کار خواهیم برد که در آن سفارش‌های مربوط به تولید خودروهایی با ویژگی‌های مد نظر مشتریان هندل می‌شوند به طوری که وظیفۀ ساخت نمونه‌های مختلف ماشین بر اساس ویژگی‌های متمایز آن‌ها بر عهدۀ یک کلاس بوده و هندل کردن سفارش‌های دریافتی از مشتریان را به کلاسی دیگر واگذار کرده‌ایم تا بدین وسیله از وابستگی اپلیکیشن به تنها یک کلاس خاص برای هندل کردن انواع و اقسام تَسک‌ها جلوگیری کنیم.

در ابتدا و به منظور پیاده‌سازی الگوی طراحی مذکور لازم است تا فایل‌های مربوط به پروژۀ خود را بر اساس ساختار زیر در لوکال‌هاست ایجاد کنیم و در ادامه تک‌تک آن‌ها را توسعه خواهیم داد:

factory-design-pattern/
├── Car.php
├── CarFactory.php
├── CarModelNotDefined.php
├── CarModelR.php
├── CarModelS.php
├── CarOrder.php
└── index.php

پیش از ادامۀ بحث، توجه داشته باشیم که کلیهٔ فایل‌های این آموزش با دستور php?> شروع می‌شوند که به دلیل شلوغ نشدن کدها، از نوشتن آن‌ها در تمامی اسکریپت‌ها خودداری کرده‌ایم.

برای شروع فایلی می‌سازیم تحت عنوان Car.php و در آن اینترفیسی به نام Car را پیاده‌سازی می‌کنیم که توسط تمامی کلاس‌های اپلیکیشن در دسترس است و در آن گفته‌ایم تمامی کلاس‌هایی که از این اینترفیس به اصطلاح implements می‌شوند باید از فانکشن ()getModel برخوردار باشند:

interface Car {
    function getModel();
}

در ادامه فایلی به نام CarModelS.php ساخته‌ایم و داخل آن کلاسی تحت عنوان CarModelS را از روی اینترفیس فوق ایمپلیمنت می‌کنیم که این وظیفه را دارا است تا ویژگی‌های مد نظر مشتریان برای تولید ماشین مدل «s» را در خروجی ریترن کند:

require_once 'Car.php';
class CarModelS implements Car {
    protected $model = 's';
    public function getModel() {
        return $this->model;
    }
}

همان‌طور که در کد فوق می‌بینید، در ابتدا فایل مربوط به اینترفیس فوق‌الذکر را ایمپورت کرده و در ادامه کلاس CarModelS را از روی اینترفیس Car ایمپلیمنت کرده‌ایم و سپس یک پراپرتی تحت عنوان model$ تعریف کرده‌ایم که استرینگ مربوط به مدل ماشین را نگاه‌داری می‌کند و استرینگ «s» را به عنوان مقدارش به آن اختصاص داده‌ایم و در ادامه متد ()getModel را پیاده‌سازی کرده و در آن گفته‌ایم در صورت فراخوانی، مقدار اختصاص‌یافته به پراپرتی this->model$ را در خروجی ریترن کند؛ به عبارتی،‌ استرینگ «s» را در خروجی نمایش خواهد داد.

همچنین فایلی دیگر تحت عنوان CarModelR.php ساخته و در آن کلاسی به نام CarModelR را از روی اینترفیس Car ایمپلیمنت می‌کنیم که این کلاس نیز وظیفۀ ساخت خودرویی با یکسری ویژگی منحصربه‌فرد دیگر را دارا است که تحت عنوان مدل «r» شناخته می‌شود:

require_once 'Car.php';
class CarModelR implements Car {
    protected $model = 'r';
    public function getModel() {
        return $this->model;
    }
}

همان‌طور که مشاهده می‌کنید، نحوۀ عملکرد کلاس فوق نیز مشابه کلاس قبل بوده و تنها تفاوت آن در مدلِ خودرویی است که از روی کلاس فوق ساخته خواهد شد. در واقع، بر اساس ویژگی‌های بیان‌شده در کلاس CarModelR مدل ماشین برابر با استرینگ «r» خواهد بود که در فانکشن ()getModel گفته‌ایم در صورت فراخوانی، استرینگ مربوط به آن را در خروجی ریترن کند.

در مرحلۀ بعد فایلی تحت عنوان CarFactory.php ساخته و در آن یک اصطلاحاً فکتوری (کارخانه) به نام CarFactory پیاده‌سازی می‌کنیم که این قابلیت را دارا است تا بر اساس مدل ماشین مد نظر مشتریان، آبجکتی متناسب با نیاز ایشان را از روی کلاس مربوطه بسازد و در ادامه این آبجکت را به کلاسی خواهیم داد که وظیفۀ هندل کردن نمونۀ ماشین‌های ساخته‌شده را دارا است:

require_once 'CarModelR.php';
require_once 'CarModelS.php';
class CarFactory {
    protected $car;
    public function make($model) {
        $model = strtolower($model);
        if ($model == 'r') {
            return $this->car = new CarModelR();
        } elseif($model == 's') {
            return $this->car = new CarModelS();
        }
    }
}

در کد فوق ابتدا فایل‌های مربوط به دو کلاس CarModelR و CarModelS را ایمپورت می‌کنیم چرا که قرار است تا متناسب با شرایط مختلف از هر دو کلاس آبجکت‌های جداگانه‌ای بسازیم و در ادامه کلاس CarFactory را پیاده‌سازی می‌کنیم که در آن یک پراپرتی تحت عنوان car$ تعریف کرده‌ایم که قرار است تا آبجکت‌های ساخته‌شده از کلاس‌های مذکور را نگاه‌داری کند سپس متدی تحت عنوان ()make با پارامتر ورودی model$ تعریف کرده‌ایم که این متد وظیفۀ ساخت آبجکتی متناسب با ویژگی‌های مد نظر مشتری از روی یکی از دو کلاس مذکور را بر عهده دارد.

در ادامه، پیش از دستورات شرطی ابتدا مقدار پراپرتی model$ را با به‌کارگیری فانکشن ()strtolower به حروف کوچک تبدیل کرده و گفته‌ایم چنانچه مقدار آن برابر با استرینگ «r» بود، آبجکتی از روی کلاس CarModelR ساخته و به پراپرتی this->car$ منتسب کرده و آن را ریترن کند و چنانچه مقدار آن برابر با استرینگ «s» بود، آبجکتی از روی کلاس CarModelS ساخته که این آبجکت هم به پراپرتی this->car$ منتسب شده و در خروجی ریترن خواهد شد.

در گام بعدی و در فایل CarOrder.php کلاسی تحت عنوان CarOrder پیاده‌سازی می‌کنیم که وظیفۀ هندل کردن تمامی آبجکت‌های ساخته‌شده از روی دو کلاس فوق‌الذکر را دارد:

require_once 'CarFactory.php';
class CarOrder {
    protected $carOrders = [];
    protected $car;
    public function __construct() {
        $this->car = new CarFactory();
    }
    public function order($model = null) {
        $car = $this->car->make($model);
        $this->carOrders[] = $car->getModel();
    }
    public function getCarOrders() {
        return $this->carOrders;
    }
}

همان‌طور که می‌بینید، در این کلاس ابتدا یک پراپرتی تحت عنوان carOrders$ از جنس آرایه تعریف کرده‌ایم که قرار است تا ویژگی‌های ماشین مد نظر مشتری را در خود ذخیره سازد و در ادامه پراپرتی دیگری به نام car$ تعریف کرده‌ایم که آبجکت ساخته‌شده از روی کلاس CarFactory در آن نگاه‌داری خواهد شد سپس داخل کانستراکتور، که به محض ساخت آبجکتی از روی این کلاس فراخوانی می‌شود، گفته‌ایم آبجکتی از روی کلاس CarFactory ساخته و آن را از طریق this->car$ به پراپرتی مذکور منتسب کند که بدین طریق به تمامی متدهای این کلاس دسترسی داشته و می‌توانیم آن‌ها را فراخوانی کنیم.

در ادامه، فانکشنی به نام ()order با پارامتر ورودی model$ تعریف کرده و مقدار آن را برابر با null قرار داده‌ایم که داخلش ابتدا متد مربوط به کلاس CarFactory به نام ()make را فراخوانی کرده و آرگومان ورودی مد نظر مشتری برای مدل ماشین را از طریق پارامتر model$ به آن پاس می‌دهیم تا بر اساس آرگومان ورودی آبجکت مناسب را از روی کلاس متناظرش ساخته و آن را به پراپرتی car$ منتسب کند که در نهایت متد ()getModel کلاس مربوطه فراخوانی شده و مقدار بازگشتی از آن به آرایۀ carOrders$ منتسب می‌شود و در ادامه مقادیر منتسب به این پراپرتی، که هم‌اکنون یک آرایه است، به متد ()getCarOrders داده شده و از طریق دستور return در خروجی بازگردانده می‌شود.

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

حال جهت تست عملکرد این اپلیکیشن، کدهای زیر را در فایل index.php نوشته و آن را اجرا می‌کنیم:

require_once 'CarOrder.php';
$carOrder = new CarOrder;
var_dump($carOrder->getCarOrders());

در تفسیر کد فوق باید بگوییم که در ابتدا فایل‌ مورد نیاز را ایمپورت کرده و در ادامه آبجکتی تحت عنوان carOrder$ از روی کلاس CarOrder ساخته‌ایم و در سطر بعد متد ()getCarOrders از این کلاس را از طریق آبجکت مذکور فراخوانی کرده‌ایم و چون این کلاس هیچ مقداری به عنوان آرگومان ورودی نداشته است، بنابراین خروجی حاصل از اجرای کد فوق یک آرایۀ خالی به ترتیب زیر خواهد بود:

array(0) { }

حال قصد داریم تا اپلیکشین خود را به ازای مقدار استرینگ ورودیِ «r» تست کنیم:

require_once 'CarOrder.php';
$carOrder = new CarOrder;
$carOrder->order('r');
var_dump($carOrder->getCarOrders());

همان‌طور که در کد فوق مشاهده می‌کنید، از طریق carOrder$ به فانکشن ()order از کلاس CarOrder دسترسی یافته و استرینگ «r» را به عنوان آرگومان ورودی به آن داده‌ایم که فراخوانی این فانکشن با ورودی مذکور منجر به انتساب آرایه‌ای از استرینگ مربوطه به پراپرتی carOrder$ خواهد شد که در نهایت این مقدار را به عنوان ورودی به فانکشن ()getCarOrders از کلاس CarOrder می‌دهیم که در نتیجۀ فراخوانی این متد نیز مقادیر آرایۀ مذکور ریترن شده و از همین روی حاصل را با به‌کارگیری فانکشن ()var_dump در خروجی چاپ می‌کنیم که خروجی آرایه‌ای به ترتیب زیر خواهد بود:

array(1) { [0]=> string(1) "r" }

همچنین به منظور تست اپلیکیشن برای مقدار استرینگ ورودیِ «s» داریم:

require_once 'CarOrder.php';
$carOrder = new CarOrder; 
$carOrder->order('s');
var_dump($carOrder->getCarOrders());

نحوۀ عملکرد کد فوق نیز به همان ترتیب قبل است اما اکنون می‌خواهیم پارامتر ورودی «S» را پاس دهیم:

require_once 'CarOrder.php';
$carOrder = new CarOrder;
$carOrder->order('S');
var_dump($carOrder->getCarOrders());

که در صورت اجرای اسکریپت فوق، باز هم خروجی همچون مورد قبل خواهد بود چرا که دستور ;(model = strtolower($model$ که پیش از این نوشتیم، دقیقاً همین وظیفه را دارا است تا کلیهٔ‌ مدل‌های ورودی را به حرف کوچک انگلیسی مبدل سازد. حال مجدد کدهای فوق را به صورت زیر تغییر می‌دهیم:

require_once 'CarOrder.php';
$carOrder = new CarOrder;
$carOrder->order('206');
var_dump($carOrder->getCarOrders());

به عنوان خروجی خواهیم داشت:

This page isn’t working 
factory-design-pattern is currently unable to handle this request.
HTTP ERROR 500

می‌بینیم که با اِکسپشن (ارور) مواجه شدیم که برای مشاهدهٔ ارور در سیستم‌عامل اوبونتو می‌توانید به مسیر var/log/apache2/error.log/ مراجعه نمایید که احتمالاً پیامی مشابه آنچه در ادامه مشاهده می‌کنید خواهد بود:

PHP Fatal error:  
Uncaught Error: Call to a member function getModel() on null in /var/www/factory-design-pattern/CarOrder.php:11\nStack trace:\n#0 /var/www/factory-design-pattern/index.php(4): CarOrder->order('206')\n#1 {main}\n  thrown in /var/www/factory-design-pattern/CarOrder.php on line 11

در واقع، این ارور از آنجا ناشی می‌شود که ما در کلاس CarFactory این قضیه را هندل نکرده‌ایم که اگر مشتری مدلی به غیر از «r» و «s» انتخاب کرد چه اتفاقی روی خواهد داد! به عبارتی، نیاز داریم تا کلاسی مثلاً تحت عنوان CarModelNotDefined درست کنیم که مسئول مدیریت مدل‌های تعریف‌نشده باشد که در همین راستا فایل جدیدی می‌سازیم به اسم CarModelNotDefined.php و آن را به صورت زیر تکمیل می‌کنیم:

require_once 'Car.php';
class CarModelNotDefined implements Car {
    public function getModel() {
        return 'this model is not defined';
    }
}

این کلاس وظیفه دارد هر وقت متدش تحت عنوان ()getModel فراخوانی شد، استرینگ «this model is not defined» را ریترن کند. حال در ادامه باید کلاس CarFactory را نیز به صورت زیر تغییر دهیم:

require_once 'CarModelR.php';
require_once 'CarModelS.php';
require_once 'CarModelNotDefined.php';
class CarFactory {
    protected $car;
    public function make($model) {
        $model = strtolower($model);
        if ($model == 'r') {
            return $this->car = new CarModelR();
        } elseif($model == 's') {
            return $this->car = new CarModelS();
        } else {
            return $this->car = new CarModelNotDefined();
        }
    }
}

پس از دستورات if و elseif دستور else را نیز قرار داده‌ایم که در صورتی کدهای داخلش اجرا خواهند شد که هیچ‌کدام از شروط فوق برآورده نشده باشند به طوری که اگر فایل index.php حاوی مدلی به غیر از «r» و «s» را در لوکال‌هاست اجرا کنیم، این بار خروجی زیر را مشاهده خواهیم کرد:

array(1) { [0]=> string(25) "this model is not defined" }

می‌بینیم که وارد دستور else شده و این دستور هم آبجکتی از روی کلاس CarModelNotDefined ساخته که این وظیفه را دارا است تا استرینگ «this model is not defined» را ریترن کند. 

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

online-support-icon