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 ریال است، سیستم از استراتژی مخصوص این کار (درگاه سکانپَل) استفاده کرده است.
جمعبندی
بدین ترتیب نتیجه میگیریم در شرایطی که چندین روش برای انجام یک تَسک مشابه یا به عبارتی الگوریتمهای متعددی برای انجام عملیاتی یکسان وجود دارد، باید یک استراتژی پترن برای این منظور پیادهسازی کنیم که با استفاده از این الگو میتوان به راحتی الگوریتمهای جدیدی را به سیستم اضافه و یا از آن حذف کرد چرا که این الگوریتمها به سایر کامپوننتهای سیستم وابسته نبوده و تغییر یا حذفشان اختلالی در عملکرد کلی وب اپلیکیشن اِعمال نمیکند.