در این مقاله قصد داریم تا دیزاین پترنی تحت عنوان Adapter Design Pattern به همراه ویژگیها و لزوم استفاده از آن در توسعۀ اپلیکیشن را با بهکارگیری از مثالی در زبان برنامهنویسی پیاچپی شرح دهیم. به طور کلی، این الگوی طراحی زیرشاخۀ الگوهای Structural (ساختاری) قرار میگیرد که دولوپرها با بهکارگیری از آن میتوانند اپلیکیشنی انعطافپذیر با قابلیت نگاهداری بالا توسعه دهند و ساختار کلاسهای مد نظر خود را مدیریت کنند. بر اساس تعریف ارائهشده در ویکیپدیا برای این دیزاین پترن داریم:
در مهندسی نرمافزار، دیزاین پترن اِدَپتر یک الگوی طراحی است که این امکان را در اختیار دولوپرها قرار میدهد تا بتوانند از طریق اینترفیس کلاسی دیگر به اینترفیس کلاس مد نظر خود دسترسی داشته و بدین ترتیب اتربیوتها و متدهای آن را در کلاس مذکور مورد استفاده قرار دهند. به عبارتی، این الگو به منظور استفاده از قابلیتهای یک کلاس در داخل یک کلاس دیگر و بدون تغییر سورسکد آن به کار گرفته میشود.
در واقع، دیزاین پترنها چیزی فراتر از یک کلاس معمولی نیستند با این تفاوت که بهکارگیری از این کلاسها منجر بدین خواهد شد تا بتوانیم به روشی اصولی سایر کلاسهای اپلیکیشن را توسعه دهیم به طوری که هندل کردن رفتار و ساختار آنها به سادگی امکانپذیر خواهد بود.
در پاسخ به این سؤال که چه مواقعی باید از Adapter Design Pattern استفاده کرد هم میتوان گفت زمانی که اپلیکیشنی را توسعه میدهیم که برای انجام تَسک مد نظر به یکسری ایپیآی خارجی وابسته است یا برنامهای که در آن نیاز به استفاده از فیچرهای کلاسی داریم که ممکن است دیر یا زود دستخوش تغییر شود، بهتر است از این دیزاین پترن استفاده نماییم (جهت آشنایی با مفهوم ایپیآی، به آموزش API چیست؟ مراجعه نمایید.)
حال به منظور پیادهسازی این الگوی طراحی سناریوی فرضی مطرحشده در آموزش قبلی را مجدداً مورد بررسی قرار میدهیم که در آن فروشگاه آنلاینی با درگاههای پرداخت متعددی داریم با این تفاوت که فرض میکنیم که در کلاس مربوط به درگاه پرداخت PayPal، متد به کار گرفتهشده به منظور اتصال به ایپیآی سرویس پیپَل مستعد تغییر بوده و برای مثال توسعهدهندگان این سرویس نام متد پرداخت را از ()sendPayment
به ()payAmount
تغییر میدهند (پیش از هر توضیحی، توجه داشته باشیم که کلیهٔ فایلهای این آموزش باید با دستور php?>
شروع شوند که به دلیل شلوغ نشدن کدها، از نوشتن این دستور در تمامی اسکریپتهای این آموزش خودداری کردهایم.) حال اگر بخواهیم این کلاس را بدون در نظر گرفتن دیزاین پترن اِدَپتر پیادهسازی کنیم، کدی مانند زیر خواهیم داشت:
class PayPal {
public function sendPayment($amount) {
echo "Paying via PayPal: ". $amount;
}
}
ابتدا فایلی تحت عنوان PayPal.php
میسازیم و همانطور که میبینید کلاسی تحت عنوان PayPal
تعریف کردهایم و در ادامه فانکشنی به اسم ()sendPayment
تعریف کرده که حاوی یک پارامتر ورودی تحت عنوان amount$
است که داخل آن گفتهایم مقدار مربوط به ارزش سبد خرید کاربر (پارامتر ورودی) را با استرینگ «:Paying via PayPal» کانکت کرده و در خروجی چاپ کند. به عنوان مثال، به صورت زیر میتوانیم فانکشن مذکور را فراخوانی میکنیم:
$payPal = new PayPal();
$payPal->sendPayment('26000');
در ابتدا آبجکتی تحت عنوان payPal$
از روی کلاس PayPal
ساختهایم و همانطور که در خط دوم میبینید، متد ()sendPayment
از این کلاس را فراخوانی کرده و مقدار 26000 را به عنوان پارامتر ورودی به این متد پاس دادهایم تا این مقدار را با استرینگ مذکور کانکت کرده و در خروجی نمایش دهد.
حال فرض میکنیم که توسعهدهندگان سرویس پیپَل نام متد مربوط به اتصال به ایپیآی این سرویس یا به عبارتی متد پرداخت را از ()sendPayment
به ()payAmount
تغییر دهند که در چنین شرایطی نام تمامی متدهای ذکرشده تحت این عنوان در سورسکد باید به ()payAmount
تغییر یابند و از آنجایی که ممکن است این کلاس در جایجای وب اپلیکیشن استفاده شده باشد، تغییر تمامی آنها کاری بس دشوار و البته مستعد خطا است که در چنین شرایطی سولوشن مناسب، پیادهسازی الگوی طراحی اِدَپتر است که این قابلیت را برای دولوپرها فراهم میکند تا برنامهای بنویسند که تغییر برخی از فیچرهای کلاس مد نظر به سادگی امکانپذیر باشد. به طور کلی، ساختار پروژهای که قصد داریم بنویسیم به صورت زیر خواهد بود:
adapter-design-pattern/
├── index.php
├── PaypalAdapter.php
├── SokanPalAdapter.php
├── PayPal.php
├── SokanPal.php
└── PaymentAdapter.php
ابتدا پوشهای تحت عنوان adapter-design-pattern
در لوکالهاست ایجاد کرده سپس فایلهایی که در بالا مشاهده میشوند را داخل آن میسازیم (فایلی را هم که قبلاً با نام PayPal.php
ساختیم وارد این پوشه میکنیم.) همانطور که در ادامه میبینید، فایلی ساختهایم تحت عنوان PaymentAdapter.php
که در آن اینترفیسی را پیادهسازی میکنیم که توسط تمامی کلاسهای موجود در اپلیکیشن در دسترس باشد:
interface PaymentAdapter {
public function pay($amount);
}
به عبارتی، اینترفیسی تحت عنوان PaymentAdapter
ساخته و در آن گفتهایم که تمامی کلاسهایی که از این اینترفیس به اصطلاح implements
شوند باید متدی با نام ()pay
با پارامتر ورودی amount$
داشته باشند. در ادامه، قصد داریم تا یک کلاس اِدَپتر برای کلاس PayPal
تحت عنوان PayPalAdapter
ساخته و از روی اینترفیس فوقالذکر آن را implements
کنیم که برای این منظور ابتدا باید این اینترفیس را در فایل مربوط به کلاس PayPalAdapter
ایمپورت کنیم:
require_once 'PaymentAdapter.php';
require_once 'PayPal.php';
class PayPalAdapter implements PaymentAdapter {
private $payPal;
public function __construct(PayPal $payPal) {
$this->payPal = $payPal;
}
public function pay($amount) {
$this->payPal->sendPayment($amount);
}
}
در تفسیر کُد فوق باید بگوییم که به منظور ایمپلیمنت کردن کلاس PayPalAdapter
از اینترفیس PaymentAdapter
باید نمونهای از کلاس اصلی، که در اینجا PayPal
است، را در دسترس داشته باشیم که برای این منظور نیز فایل مربوط به کلاس PayPal
را ایمپورت کردهایم.
ابتدا به ساکن کانستراکتوری نوشته و در آن گفتهایم به محض ساخت هرگونه آبجکتی از روی کلاس PayPalAdapter
یک آبجکت از روی کلاس PayPal
تحت عنوان payPal$
ساخته شده و به پراپرتی payPal$
منتسب شود که از این پس به متدها و اتربیوتهای کلاس PayPal
دسترسی خواهیم داشت (برای آشنایی بیشتر با نحوۀ تعریف تایپهای مد نظر خود در زبان برنامهنویسی پیاچپی توصیه میکنیم به مقالۀ آشنایی با مفهوم Type Hinting در زبان PHP مراجعه نمایید.)
در ادامه، فانکشنی با نام ()pay
با پارامتر ورودی amount$
تعریف کردهایم که این وظیفه را دارا است تا فانکشن تعریفشده در کلاس PayPal
با نام ()sendPayment
را فراخوانی کرده و پراپرتی amount$
که معادل ارزش پولی سبد خرید از طریق درگاه سرویس پیپَل است را به عنوان پارامتر ورودی به این فانکشن میدهد که این فانکشن نیز مقدار عددی دریافتشده را با استرینگ مذکور کانکت کرده و در نهایت نتیجه را در پراپرتیای تحت عنوان payPal$
نگاهداری میکند.
حال فایل دیگری تحت عنوان index.php
ایجاد کرده و میبینیم که چگونه میتوان از کلاس اِدَپتر تعریفشده استفاده کرد که برای این منظور ابتدا باید فایل مورد نیاز را در این کلاس ایمپورت کنیم:
require_once 'PayPalAdapter.php';
$payPal = new PayPalAdapter(new PayPal());
$payPal->pay('26000');
همانطور که میبینید در ابتدا آبجکتی از کلاس PayPalAdapter
تحت عنوان payPal$
ساختهایم که کانستراکتور این کلاس ما را ملزم میکند آبجکتی از نوع کلاس PayPal
به عنوان آرگومان ورودی به آن پاس دهیم (نیاز به یادآوری است که در خط اول فایل PayPalAdapter.php
را ایمپورت کردهایم تا از این پس بتوانیم از آن استفاده نماییم.) با فراخوانی متد ()pay
میتوانیم به متد ()sendPayment
تعریفشده در کلاس PayPal
دسترسی داشته و آن را فراخوانی کنیم که در نتیجۀ این فراخوانی مبلغ 26000 با استرینگ «:Paying via PayPal» کانکت شده و در خروجی نمایش داده میشود:
Paying via PayPal: 26000
حال اگر چنانچه توسعهدهندگان سرویس پیپَل در متدهای مربوط به ایپیآی خود تغییری اِعمال کنند، برای مثال نام متد پرداخت را تغییر دهند، به سادگی خواهیم توانست تا نام این متد در کلاس PayPalAdapter
را تغییر دهیم چرا که متد مذکور صرفاً در این کلاس پیادهسازی شده و اِعمال تغییر در آن کار دشواری نیست به طوری که داریم:
class PayPal {
public function pleasePayThePrice($amount) {
echo "Paying via PayPal: ". $amount;
}
}
همانطور که ملاحظه میشود، متد ()sendPayment
به ()pleasePayThePrice
تغییر پیدا کرده است که در این صورت صرفاً نیاز است تا کلاس PayPalAdapter
را ریفکتور کنیم به طوری که خواهیم داشت:
require_once 'PaymentAdapter.php';
require_once 'PayPal.php';
class PayPalAdapter implements PaymentAdapter {
private $payPal;
public function __construct(PayPal $payPal) {
$this->payPal = $payPal;
}
public function pay($amount) {
$this->payPal->pleasePayThePrice($amount);
}
}
همانطور که ملاحظه میشود، داخل فانکشن ()pay
نام متد منتسب شده به پراپرتی this->payPal$
را از ()sentPayment
به ()pleasePayThePrice
تغییر دادهایم و اگر مجدد فایل index.php
را در لوکالهاست اجرا کنیم، میبینیم که برنامه بدون هیچ مشکلی اجرا خواهد شد.
نحوۀ افزودن درگاه پرداخت جدید به وب اپلیکیشن
در این مرحله قصد داریم تا متناسب با نیاز کاربران، درگاه پرداخت جدیدی به نام SokanPal
را به وب اپلیکیشن خود بیفزاییم که برای این منظور ابتدا فایلی با همین عنوان در فولدر اختصاصی این پروژه ایجاد کرده و در داخل آن کلاسی با نام SokanPal
مینویسیم:
class SokanPal {
public function doPayment($amount) {
echo "Paying via SokanPal: " . $amount;
}
}
داخل این کلاس فانکشنی تحت عنوان ()doPayment
با پارامتر ورودی amount$
تعریف کرده و گفتهایم ارزش پولی سبد خرید کاربر را دریافت کرده و با استرینگ «:Paying via SokanPal» کانکت کرده و در خروجی چاپ کند. حال اگر بخواهیم کدنویسی این درگاه پرداخت را بر اساس دیزاین پترن اِدَپتر و مبتنی بر کلاس PaymentAdapter
انجام دهیم که پیشتر تعریف کردهایم، ابتدا فایلی با نام SokanPalAdapter.php
ساخته و آن را به صورت زیر تکمیل میکنیم:
require_once 'SokanPal.php';
require_once 'PaymentAdapter.php';
class SokanPalAdapter implements PaymentAdapter {
private $sokanPal;
public function __construct(SokanPal $sokanPal) {
$this->sokanPal = $sokanPal;
}
public function pay($amount) {
$this->sokanPal->doPayment($amount);
}
}
همانطور که در کد فوق میبینید، فایلهای مورد نیاز برای این کلاس همچون SokanPal.php
و PaymentAdapter.php
را در ابتدا ایمپورت کرده و در ادامه کلاس SokanPalAdapter
را از اینترفیس PaymentAdapter
ایمپلیمنت میکنیم که در همین راستا موظف به پیادهسازی فانکشن ()pay
در این کلاس خواهیم بود اما پیش از پیادهسازی فانکشن مذکور لازم است تا آبجکتی از کلاس اصلی SokanPal
را در کلاس SokanPalAdapter
داشته باشیم که برای این منظور آبجکتی از روی کلاس SokanPal
تحت عنوان sokanPal$
به عنوان پارامتر ورودی کانستراکتور در نظر گرفته و در ادامه داخل فانکشن ()pay
گفتهایم متد تعریفشده در کلاس اصلی SokanPal
تحت عنوان ()doPayment
را به پراپرتی this->sokanPal$
منتسب کند و پارامتر amount$
را به عنوان آرگومان ورودیاش پاس دهد.
در ادامه، برای بررسی نحوۀ عملکرد درگاه جدید SokanPal
کدهای زیر را در فایل index.php
نوشته سپس کدهای قبلی را کامنت میکنیم:
// require_once 'PayPalAdapter.php';
// $payPal = new PayPalAdapter(new PayPal());
// $payPal->pay('26000');
require_once 'SokanPalAdapter.php';
$sokanPal = new SokanPalAdapter(new SokanPal());
$sokanPal->pay('100000');
در توضیح کد فوق باید بگوییم که ابتدا آبجکتی با نام sokanPal$
از روی کلاس SokanPalAdapter
ساختهایم که بر اساس عملکرد کانستراکتور این کلاس، هرگونه آبجکتی از کلاس مذکور ساخته شود به آبجکتی از کلاس SokanPal
نیاز دارد و بالتبع ()new SokanPal
را به عنوان پارامتر ورودی در نظر میگیریم. در ادامه، با فراخوانی متد ()pay
میتوانیم به متد تعریفشده در کلاس اصلی SokanPal
با نام ()doPayment
دسترسی داشته و آن را فراخوانی کنیم و در نهایت این فانکشن مقدار ورودی با عنوان ارزش سبد خرید کاربر را با استرینگ مذکور کانکت کرده و در خروجی نمایش میدهد به طوری که داریم:
Paying via SokanPal: 100000
حال اگر روزی کلاس SokanPal
به صورت زیر تغییر یابد:
class SokanPal {
public function pleaseDoPayment($amount) {
echo "Paying via SokanPal: " . $amount;
}
}
صرفاً نیاز است تا اِدَپتر مرتبط با این کلاس را به صورت زیر ریفتکتور نماییم:
require_once 'SokanPal.php';
require_once 'PaymentAdapter.php';
class SokanPalAdapter implements PaymentAdapter {
private $sokanPal;
public function __construct(SokanPal $sokanPal) {
$this->sokanPal = $sokanPal;
}
public function pay($amount) {
$this->sokanPal->pleaseDoPayment($amount);
}
}
و این در حالی است که کد ما بدون هیچ مشکلی کار خواهد کرد و حتی اگر دهها جای مختلف در سورسکد از فانکشن ()pay
استفاده کرده باشیم، صرفاً با تغییر دو جای کد میتوانیم سرویس را آپدیت کنیم. به عبارتی، متدی تحت عنوان ()pay
تعریف کردهایم که به وسیلۀ آن به راحتی میتوانیم به تمامی متدهای سایر کلاسهای برنامه دسترسی داشته باشیم و از آنجایی که متدهای مذکور صرفاً در یک کلاس پیادهسازی شده و در تمامی نقاط برنامه تکرار نمیشوند، بنابراین در مواقع لزوم میتوانیم تغییرات مد نظر خود را به راحتی در این متدها اِعمال کنیم.
جمعبندی
به طور کلی، در شرایطی که قصد داریم اپلیکیشنی توسعه دهیم که برای انجام تَسکهای مد نظر نیازمند برقراری ارتباط با یکسری لایبرری و همچنین ایپیآی یا وبسرویس است، سولوشن مناسب برای جلوگیری از هدر رفتن زمان جهت ریفکتور کردن سورسکد و مواجه با اِکسپشنهای احتمالی استفاده از دیزاین پترن اِدَپتر است به طوری که به راحتی میتوانیم تغییرات احتمالی اِعمالشده روی متدها، لایبرریها و ایپیآیهای به اصطلاح Third-party را در کلاسها و متدهای متناظر آنها در اپلیکیشن خود نیز اِعمال کنیم.