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

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

در این مقاله قصد داریم تا دیزاین پترنی تحت عنوان 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 را در کلاس‌ها و متدهای متناظر آن‌ها در اپلیکیشن خود نیز اِعمال کنیم.

online-support-icon