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


Design Pattern (الگوی طراحی) اشاره به راه‌کاری تست‌شده و اصولی با قابلیت استفادۀ مجدد در برنامه‌نویسی شییٔ‌گرا دارد که در پاسخ به چالش‌های توسعۀ نرم‌افزار ایجاد شده‌ است و در واقع تِمپلِیتی جهت ارائۀ سولوشنی برای حل مسائل رایج می‌باشد. در واقع، دیزاین پترن‌ها یکسری به اصطلاح Best Practice هستند که امکان توسعۀ اپلیکیشن‌هایی انعطاف‌پذیر، تغییرپذیر با قابلیت نگاه‌داری بالا را برای دولوپرها فراهم می‌کنند که یکی از آن‌ها، الگوی طراحی استراتژی است که در این آموزش خواهیم دید به چه شکل می‌توان آن را در زبان برنامه‌نویسی پی‌اچ‌پی پیاده‌سازی کرد.

به طور کلی، دیزاین‌ پترن‌ها در سه دستۀ اصلی قرار می‌گیرند که عبارتند از Structural ،Creational و Behavioral که در این مقاله قصد داریم تا به بررسی Strategy Pattern بپردازیم که تحت عنوان Policy Pattern نیز شناخته می‌شود و زیرشاخۀ دیزاین‌ پترن‌های Behavioral است که ارتباط مابین آبجکت‌ها را مدیریت می‌کنند.

آشنایی با کاربرد Strategy Design Pattern
پیش از هر چیز، بهتر است به این پرسش پاسخ دهیم که در چه مواقعی باید از دیزاین پترن استراتژی استفاده کنیم. در پاسخ به این سؤال باید گفت زمانی که چند راه یا بهتر بگوییم چند الگوریتم مختلف برای انجام تَسکی یکسان داشته باشیم که اپلیکیشن مد نظر باید یکی از این روش‌ها را در حین اجرا و بر اساس پارامترهای خاصی انتخاب کند، باید از این دیزاین پترن استفاده نماییم (برای مثال، فرض کنید که چند الگوریتم مرتب‌سازی داریم و بر اساس تعداد عناصر یک آرایه، الگوریتمی انتخاب خواهد شد که منجر به بهبود پرفورمنس اپلیکیشن شود.)

برای روشن شدن نحوهٔ پیاده‌سازی الگوی طراحی استراتژی، یک سناریوی فرضی را در نظر بگیرید بدین صورت که فروشگاه آنلاینی با درگاه‌های پرداخت متعددی داریم که علیرغم وجود چندین درگاه پرداخت، همۀ آن‌ها در قسمت فرانت‌اند برای کاربر نمایش داده نمی‌شوند و این در حالی است که درگاه پرداخت مناسب باید در لحظه و بر اساس ارزش ریالی سبد خرید کاربر انتخاب شود به طوری که مثلاً اگر ارزش سبد خرید وی کمتر از 500/000 ریال بود، پردازش تراکنش با استفاده از سرویس پرداخت پِی‌پَل انجام خواهد شد اما اگر ارزش سبد خرید 500/000 ریال یا بیشتر بود، تراکنش با استفاده از یک کارت اعتباری انجام خواهد شد. در چنین شرایطی اگر بخواهیم سرویس خود را بدون پیاده‌سازی دیزاین پترن استراتژی بنویسیم، راه‌کاری مانند زیر خواهیم داشت:

class PayByCC {
    public function pay($amount = 0) {
        echo "Paying ". $amount. " using Credit Card";
    }
}
 
// Class to pay using PayPal
class PayByPayPal {
    public function pay($amount = 0) {
        echo "Paying ". $amount. " using PayPal";
    }
}
 
// This code needs to be repeated every place where ever needed.
$amount = 5000;
if($amount >= 500000) {
    $pay = new PayByCC();
    $pay->pay($amount);
} else {
    $pay = new PayByPayPal();
    $pay->pay($amount);
}

پیش از هر توضیحی، توجه داشته باشیم که کلیهٔ فایل‌های این آموزش باید با دستور php?> شروع شوند که به دلیل شلوغ نشدن کدها، از نوشتن این دستور در تمامی اسکریپت‌های این آموزش خودداری کرده‌ایم. در تفسیر کدهای فوق باید بگوییم که در ابتدا دو کلاس اصلی تحت عناوین PayByCC و PayByPayPal برای هر دو روش پرداخت از طریق پِی‌پَل و درگاه مربوط به کارت اعتباری کاربر تعریف شده‌ که نتیجهٔ مد نظر خود را می‌توان با دستورات شرطی پیاده‌سازی کرد.

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

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

strategy-design-pattern/
├── index.php
├── PayByCC.php
├── PayByPayPal.php
├── PayBySokanPal.php
├── PayStrategy.php
└── ShoppingCart.php

ابتدا پوشه‌ای تحت عنوان strategy-design-pattern در لوکال‌هاست ایجاد کرده، سپس فایل‌هایی که در بالا مشاهده می‌شوند را داخل آن ساخته و در ادامه تک‌تک آن‌ها را توسعه می‌دهیم. همان‌طور که در ادامه می‌بینید، فایلی ساخته‌ایم تحت عنوان PayStrategy.php که نقش یک اینترفیس را بازی می‌کند و داخل آن فانکشنی تحت عنوان ()pay به صورت زیر تعریف‌ کرده‌ایم:

interface PayStrategy {
    public function pay($amount);
}

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

require_once 'PayStrategy.php';
class PayByCC implements PayStrategy {    
    public function pay($amount = 0) {
        echo "Paying " . $amount . " using Credit Card";
    }  
}

از آنجا که این کلاس از اینترفیس PayStrategy به اصطلاح implements شده است، لذا نیاز داریم تا این اینترفیس را در این فایل ایمپورت کنیم که برای این منظور از دستور require_once استفاده کرده سپس نام فایل مد نظر که در اینجا PayStrategy.php است را داخل علائم کوتِیشن نوشته‌ایم. داخل این کلاس یک فانکشن داریم تحت عنوان ()pay که یک پارامتر ورودی تحت عنوان amount$ با مقدار پیش‌فرض 0 دارا است و داخل این فانکشن هم دستور داده‌ایم که مقدار پارامتر ورودی این فانکشن به استرینگ مد نظر اصطلاحاً کانکَت (ضمیمه) شود. در ادامه، فایلی خواهیم ساخت تحت عنوان PayByPayPal.php حاوی کدهایی که در ادامه مشاهده می‌کنید:

require_once 'PayStrategy.php';
 class PayByPayPal implements PayStrategy {
    public function pay($amount = 0) {
        echo "Paying ". $amount. " using PayPal";
    }
}

در گام بعدی یک کلاس اصلی تحت عنوان ShoppingCart داخل فایل ShoppingCart.php ایجاد کرده و آن را به صورت زیر تکمیل خواهیم کرد:

require_once 'PayByPayPal.php';
require_once 'PayByCC.php';
require_once 'PayBySokanPal.php';

class ShoppingCart {
    public $amount = 0;
    public function __construct($amount = 0) {
        $this->amount = $amount;
    }
     
    public function getAmount() {
        return $this->amount;
    }
     
    public function setAmount($amount = 0) {
        $this->amount = $amount;
    }
 
    public function payAmount() {
        if($this->amount >= 500000) {
            $payment = new PayByCC();
        } else {
            $payment = new PayByPayPal();
        }
        $payment->pay($this->amount);
    }
}

در تفسیر کدهای فوق باید گفت ابتدا فایل‌هایی که در این کلاس استفاده شده‌اند را با استفاده از دستور require_once ایمپورت (وارد) کرده‌ایم. داخل بدنهٔ این کلاس یک پراپرتی تحت عنوان amount$ ساخته و مقدار اولیهٔ آن را برابر با 0 قرار داده‌ایم سپس کانستراکتوری نوشته‌ایم که این وظیفه را دارا است تا به محض ساختن آبجکتی از روی این کلاس، به صورت خودکار فراخوانی شده و پراپرتی amount$ را مقداردهی کند (برای آشنایی بیشتر با مفهوم کانستراکتور، به مقالهٔ آشنایی با مفاهیم Constructor و Destructor در PHP مراجعه نمایید.)

در توسعهٔ اصولی اپلیکیشن، دولوپر می‌باید متدهای به اصطلاح Getter و Setter بنویسید که در این کلاس هم برای این منظور از متدهای ()getAmount و ()setAmount استفاده کرده‌ایم. در حقیقت، این متدها وظیفهٔ ساده‌ای دارند مبنی بر اینکه به ترتیب مقدار پراپرتی amount$ را ریترین و یا این پراپرتی را مقداردهی می‌کنند (نیاز به توضیح نیست که برای دستیابی به پراپرتی‌ها در سراسر یک کلاس از دستور <-this$ استفاده می‌کنیم با این تفاوت که علامت $ که قبل از نام پراپرتی آمده را حذف کرده و صرفاً به درج نام پراپرتی اکتفا می‌کنیم.)

در پایان هم فانکشنی نوشته‌ایم تحت عنوان ()payAmount که این وظیفه را دارا است تا بسته به مقدار ذخیره‌شده در پراپرتی amount$، یکی از دو کلاس PayByCC یا PayByPayPal را مورد استفاده قرار دهد. به عبارتی، اگر مقدار پراپرتی amount$ بزرگ‌تر یا مساوی با ۵۰۰/۰۰۰ ریال بود، یک آبجکت از روی کلاس PayByCC ساخته شده و به متغیری تحت عنوان payment$ اختصاص خواهد یافت و در غیر این صورت، این متغیر با آبجکتی از روی کلاس PayByPayPal مقداردهی خواهد شد.

از آنجا که هر دوی کلاس‌ها از اینترفیس PayStrategy استفاده کرده‌اند، لذا هر دو دارای فانکشنی تحت عنوان ()pay هستند و از همین روی این فانکشن را فراخوانی کرده و به عنوان پارامتر ورودی‌اش هم پراپرتی amount$ را پاس داده‌ایم. حال فایل دیگری تحت عنوان index.php ساخته که به عنوان نقطهٔ شروع وب اپلیکیشن است و کدهای زیر را داخل آن می‌نویسیم:

require_once 'ShoppingCart.php';
$cart = new ShoppingCart(499999);
$cart->payAmount();

طبق روال، با استفاده از دستور require_once فایلی که قصد داریم از آن استفاده کنیم را ایمپورت کرده‌ایم. سپس متغیری ساخته‌ایم تحت عنوان cart$ و مقدار آن را برابر با آبجکتی از روی کلاس ShoppingCart قرار داده‌ایم. با توجه به اینکه پیش‌تر در طراحی کلاس ShoppingCart اقدام به نوشتن کانستراکتوری کردیم که یک پارامتر ورودی می‌گیرد، از این روی عددی را به عنوان پارامتر ورودی این کلاس در نظر می‌گیریم که در مثال فوق 499999 است و در ادامه هم فانکشن ()payAmount را فراخوانی می‌کنیم:

Paying 499999 using PayPal

از آنجا که قبلاً در فانکشن ()payAmount با استفاده از دستورات شرطی گفته‌ایم که اگر این مقدار کمتر از 500/000 ریال بود کلاس PayByPayPal مورد استفاده قرار گیرد، می‌بینیم که به درستی درگاه پِی‌پَل مورد استفاده قرار گرفته‌ است. حال اسکریپت فوق را به صورت زیر تغییر می‌دهیم:

require_once 'ShoppingCart.php';
$cart = new ShoppingCart(510000);
$cart->payAmount();

حال بار دیگر اسکریپت را اجرا می‌کنیم:

Paying 510000 using Credit Card

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

نحوۀ افزودن روش پرداخت جدید به وب اپلیکیشن
فرض کنید که در مراحل بعد می‌خواهیم تا متناسب با نیاز کاربران یک درگاه پرداخت جدید را به سیستم اضافه کنیم که انجام این کار بسیار ساده است. برای مثال، در ادامه قصد داریم تا درگاه پرداخت جدیدی به نام SokanPal را به اپلیکیشن اضافه کنیم تا پردازش تراکنش‌هایی را بر عهده داشته باشد که در آن‌ها ارزش سبد خرید کاربران بیش از 1/000/000 ریال است. در چنین شرایطی، تنها کاری که باید انجام دهیم این است که یک کلاس پرداخت جدید تحت عنوان PayBySokanPal داخل فایلی به نام PayBySokanPal.php ایجاد کنیم:

require_once 'PayStrategy.php';
class PayBySokanPal implements PayStrategy {
    public function pay($amount = 0) {
        echo "Paying ". $amount. " using SokanPal";
    }
}

در ادامه فانکشن ()pay را برای این درگاه پرداخت جدید (استراتژی جدید) کاستومایز می‌کنیم و در ادامه متد اصلی ()payAmount در کلاس ShoppingCart را متناسب با نیاز خود تغییر می‌دهیم که لازم است تا به صورت زیر ریفکتور شود:

public function payAmount() {
    if($this->amount > 1000000) {
        $payment = new PayBySokanPal();
    } elseif($this->amount >= 500000) {
        $payment = new PayByCC();
    } else {
        $payment = new PayByPayPal();
    }
    $payment->pay($this->amount);
}

تنها تفاوتی که این متد کرده این است که در اولین دستور شرطی گفته‌ایم که اگر مقدار مبلغ پرداختی برابر یا بیش از 1/000/000 ریال بود، از درگاه پرداخت PayBySokanPal استفاده شود و الباقی شروط هم که مثل گذشته است. حال کدهای داخل فایل index.php را مجدد به صورت زیر تغییر می‌دهیم تا وب اپلیکیشن را تست کنیم:

require_once 'ShoppingCart.php';
$cart = new ShoppingCart(1000000);
$cart->payAmount();

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

Paying 1000000 using SokanPal

می‌بینیم از آنجا که مقدار سبد خرید بیش از 1/000/000 ریال است، سیستم از استراتژی مخصوص این کار (درگاه سکان‌پَل) استفاده کرده است.

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

دانلود فایل‌های تمرین


اکرم امراه‌نژاد

لیست نظرات
کاربر میهمان
دیدگاه شما چیست؟
کاربر میهمان
e.kiasaty
e.kiasaty
۱۳۹۸/۰۲/۰۶
خیلی خوب بود..
فقط یک سؤال، در کلاس shoppingCart در متد payAmount اصل open-closed از اصول SOLID رعایت نشده.
چون برای اضافه کردن قابلیت، کد قبلی رو تغییر دادیم.
درست می‌گم؟!

در مورد این توضیح می‌دید؟ چه‌طور می‌شه برنامه رو طوری نوشت که این اصل رعایت بشه؟
نامشخص
نامشخص
۱۳۹۷/۱۱/۲۸
واقعا عالی بود ممنون