اگر بخواهیم جایگاه الگوی طراحی Proxy را در طبقه بندی الگو های طراحی بررسی کنیم، مشخص میشود که این الگو بر اساس هدف، جزء الگو های ساختاری یا Structural بوده و بر اساس حوزه، در دسته ی Object قرار گرفته است.
این الگو به شما امکان ایجاد جایگزین یا نگهدارنده برای یک شی اصلی را فراهم کرده و دسترسی به شی اصلی را بوسیله ی آن کنترل میکند، تا بتوان قبل یا بعد از درخواست به شی اصلی، کاری را انجام داد. واژه پروکسی به معنی نماینده یا واسط است و در اینجا شی پروکسی واسطی بین ما و شی اصلی یا نماینده ای برای دسترسی به شی اصلی میباشد.
الگوی طراحی Proxy، استفاده از یک واسطه به منظور جلوگیری از دسترسی مستقیم به شی اصلی و متمرکز کردن کنترل دسترسی میباشد. به صورتی که بوسیله ی آن میخواهیم چالشهای دسترسی به اشیا برنامه را حل کنیم و در عین حال interface کلاس اصلی را نیز تغییر ندهیم، تا بتوانیم با آن کلاس مانند قبل رفتار کنیم. شکل زیر نمایی از این الگو را نشان میدهد.
عناصر اصلی در الگوی Proxy عبارتند از:
· RealSubject: همان کلاس اصلی است که قبل از پیاده سازی Proxy در برنامه ما وجود داشته و اکنون میخواهیم دسترسی به آن را کنترل کنیم.
· Subject: یک interface مشترک برای کلاسهای RealSubject و Proxy که امکان استفاده از این دو را به طور مشابه فراهم میکند. کلاسهای RealSubject و Proxy این interface را پیاده سازی میکنند.
· Proxy: کلاسی که دسترسی به کلاس RealSubject را ایجاد، کنترل، و یا اعتبارسنجی میکند.
· Request: یک نمونه متدی است که در interface اعلام و در کلاسهای RealSubject و Proxy پیاده سازی شده است و ما دسترسی به این متد در کلاس RealSubject را از طریق متد همنامش در Proxy کنترل خواهیم کرد.
· Client: درخواست ها از سمت کلاینت خواهد بود یا مربوط به آن میباشد و کلاسی است که استفاده ی از این الگو در آن پیاده میشود
ممکن است این سؤال در ذهن ما ایجاد شود که چرا باید دسترسی به یک شی را کنترل کنیم؟ جواب سؤال را میتوان با مثالی ساده بیان کرد. فرض کنید یک شی عظیم دارید که منابع زیادی از سیستم را مصرف میکند و گاهی به آن احتیاج دارید، اما نه همیشه.
برای این کار میتوان، این شی را در همه ی مواقعی که لازم است، مرتبا ایجاد کرد. در این حالت، همه شی های کلاینت باید برخی از کد های اولیه را اجرا کنند که این کار باعث تکرار زیاد کدها میشود. (مانند شکل زیر)
الگوی Proxy پیشنهاد میکند که یک کلاس proxy با همان interface شی عظیم ایجاد کنید. سپس کدهای خود را بر اساس ساختار جدید به روز کنید، به طوری که شی proxy در همه کلاینت هایی که از شی اصلی استفاده میکنند جایگزین شود. حالا به محض دریافت درخواست از سمت کلاینت برای استفاده از منبع اصلی، proxy یک شی از سرویس اصلی را ایجاد کرده و کلیه کارها را به آن واگذار میکند. شکل زیر استفاده از پروکسی را نشان میدهد.
یک مثال عملی از الگوی طراحی Proxy موضوع Lazy Loading در ORM ها میباشد. در این حالت، در ارتباط شی های مختلف با شی داده، یک Proxy از آن، درون سایر شی ها قرار داده میشود و فراخوانی(load) آن تا زمان استفاده از شی داده به تعویق میافتد.
1- کاربرد های الگوی طراحی Proxy
برای الگوی پروکسی کاربردهای متعددی وجود دارد که به رایجترین آنها اشاره میکنیم:
· Lazy initialization یا Virtual Proxy: زمانی کاربرد دارد که یک شی اصلی و سنگین دارید که هر از گاهی به آن احتیاج دارید و این شی همیشه بخش زیادی از منابع سیستم را درگیر میکند. برای جلوگیری از این اتفاق، به جای ایجاد شی هنگام راه اندازی برنامه، میتوانید ایجاد آن را به زمانی که واقعا لازم است به تاخیر بیاندازید.
· کنترل دسترسی یا Protection Proxy: در شرایطی کاربرد دارد که می خواهید فقط کلاینت خاصی قادر به استفاده از شی اصلی باشد. به عنوان مثال، هنگامی که اشیاء اصلی، بخشهای مهم یک سیستمعامل هستند و کلاینت ها، برنامههای مختلف راه اندازی شده باشند که حتی میتوانند برنامههای مخرب هم باشند، در این حالت پروکسی میتواند کاری کند که تنها در صورتی که اعتبار کلاینت با معیارهای امنیتی سیستمعامل مطابقت داشته باشد، درخواست را به شی اصلی ارسال کند.
· اجرای لوکال یک سرویس از راه دور یا Remote Proxies: زمانی کاربرد دارد که شی اصلی در سرور از راه دور قرار دارد. در این حالت Proxy، وظایف ارسال درخواست کلاینت به شی اصلی و تمام جزییات و چالشهای ارتباط از طریق شبکه را بر عهده میگیرد.
· لاگ گیری در برنامه یاLogging Proxy: زمانی کاربرد دارد که می خواهید سابقه درخواست های مربوط به سرویسی را نگهداری کنید. پروکسی میتواند هر درخواست را قبل از انتقال به سرویس ثبت کند.
· Caching proxy: در شرایطی کاربرد دارد که باید نتایج درخواست های کلاینت به شی اصلی را در حافظه ای ذخیره کرده و چرخه عمر این حافظه را مدیریت کنید.
· مرجع هوشمند : در شرایطی کاربرد دارد که شی اصلی منابع زیادی را درگیر کرده است و باید وضعیت کلاینت هایی که از آن شی اصلی استفاده میکنند را زیر نظر داشت. پروکسی در این شرایط میتواند لیستی از کلاینت هایی را که از شی اصلی استفاده میکنند، به همراه وضعیت آنها نگه دارد و برای مثال در صورتی که همه ی کلاینت ها غیر فعال بودند، شی اصلی را حذف و منابع درگیر شده را آزاد کند. همچنین میتواند پیگیری کند که آیا کلاینتی شی اصلی را تغییر داده است یا خیر. که اگر تغییری صورت نگرفته است، سایر کلاینت ها از آن استفاده مجدد کنند.
2- مزایا و معایب استفاده از الگوی طراحی Proxy
مزایای استفاده از الگوی طراحی Proxy عبارت است از:
1. بدون اطلاع کلاینت ها، میتوان شی اصلی و چرخه ی عمر آن را کنترل کرد.
2. حتی اگر شی اصلی آماده ی به کار یا در دسترس نباشد، Proxy کار میکند.
3. میتوانید Proxy های جدید را بدون تغییر در شی اصلی یا کلاینت های آن تعریف کنید. (Open/Closed Principle)
4. بر خلاف شی اصلی هزینه بر نیست و زمانی که واقعا به شی اصلی نیاز داشته باشیم آن را ایجاد میکند و در دسترس قرار میدهد (در حقیقت با به تاخیر انداختن ایجاد شی اصلی، از منابع سیستمی به خوبی استفاده میکند).
5. از آنجا که proxy همان interface کلاس اصلی را پیاده سازی میکند، میتوان آن را در هر کلاینت که انتظار یک شی اصلی را دارد، جایگزین کرد.
استفاده از الگوی طراحی Proxy ممکن است دارای معایبی باشد:
· باعث پیچیدهتر شدن کد شود زیرا باید کلاسهای جدید زیادی را معرفی کرد.
· پاسخ از سمت سرویس دهنده ممکن است به تاخیر بیافتد.
3- مقایسه با الگو های دیگر
الگوی طراحی Proxy با برخی از الگو ها دارای شباهت ها و تفاوتهایی میباشد:
· الگوی طراحی Adapter یک interface متفاوت برای شی اصلی فراهم میکند و میخواهد نحوه دسترسی به شی اصلی را تغییر دهد ، اما Proxy با interface یکسان این کار را انجام میدهد و الگوی Decorator آن را با یک interface پیشرفته تر فراهم میکند. Interface پروکسی ها همیشه مطابق interface کلاس اصلی است.
· الگوی Facade و Proxy از این نظر شبیه به هم هستند که هر دو یک شی پیچیده را درون خود تعریف کرده و نگه میدارند. اما بر خلاف Facade، Proxy یک interface مشابه شی اصلی دارد که باعث میشود شی پروکسی و شی اصلی، قابل تغییر و تبدیل به هم باشند.
· الگوی طراحی Decorator و الگوی طراحی Proxy، ساختارهای مشابه اما اهداف کاملا متفاوتی دارند. در هر دو الگو قرار است یک شی برخی از کارها را به شی دیگری واگذار کند و رفتاری به شی اصلی اضافه شود بدون این که کد آن تغییری کند. اما ما با پیاده سازی Decorator میتوانیم هر نوع رفتار عمومی را به شی خود اضافه کنیم، در صورتی که هنگامی که از Proxy استفاده میکنیم، هدف ما اضافه کردن رفتاری است که دسترسی به شی را کنترل میکند.
4- نمونه کد جهت استفاده از الگوی طراحی Proxy
در این بخش میخواهیم پیاده سازی یک virtual proxy را مثال بزنیم. فرض کنید برنامهای داریم که در مکان های مختلف، تصویری را load و نمایش میدهد و برای این کار یک کلاس جداگانه ای نوشته شده باشد. حال اگر این تصویر بر روی یک دیسک باشد و قرار باشد به دفعات زیاد در مکان های مختلفی از برنامه، از این کلاس، شی ایجاد شده و تصویر load شود، منابع سیستمی زیادی درگیر شده و هزینه بر خواهد بود. برای رفع این مشکل میتوانیم از الگوی طراحی Proxy استفاده کنیم. برای این منظور مراحل زیر را دنبال خواهیم کرد:
1- ابتدا یک interface ایجاد کرده و متدهای مورد نیاز را در آن اعلام میکنیم. در این جا چون تنها میخواهیم یک تصویر را نمایش دهیم، یک متد با نام display که برای نمایش image میباشد را در آن تعریف میکنیم:
<?php
namespace App\DesignPatterns\Proxies\Image;
interface ImageProxyInterface
{
public function display();
}
2- در مرحله دوم، کلاس اصلی ای که برای نمایش image میباشد را بر اساس این interface پیاده سازی میکنیم:
<?php
namespace App\DesignPatterns\Proxies\Image;
class RealImage implements ImageProxyInterface
{
protected $filename;
public function __construct($filename)
{
$this->filename = $filename;
$this->loadFromDisk();
}
protected function loadFromDisk()
{
echo "Loading {$this->filename}\n";
}
public function display()
{
echo "Display {$this->filename}\n";
}
}
3- سپس کلاس Proxy را مطابق با interface پیاده سازی میکنیم. در متد display کلاس Proxy، در صورتی که قبلا شی اصلی وجود نداشته باشد، یک شی از کلاس اصلی ایجاد میشود. همانطور که مشاهده میکنید معمولا شی اصلی در constructor کلاس پروکسی inject نمیشود زیرا این همان کار هزینه بر برای سیستم میباشد که ما تلاش میکنیم آن را کنترل کنیم.
<?php
namespace App\DesignPatterns\Proxies\Image;
class ProxyImage implements ImageProxyInterface
{
protected $id;
protected $image;
protected $filename;
public function __construct($filename)
{
$this->filename = $filename;
}
public function display()
{
if (null === $this->image) {
$this->image = new RealImage($this->filename);
}
return $this->image->display();
}
}
4- حال میخواهیم تفاوت اثر استفاده و عدم استفاده از Proxy را نشان دهیم.
· کلاس index مانند زیر ایجاد کرده و از کلاس اصلی یک شی ایجاد میکنیم. همانطور که در کد زیر می بینید، در این کلاس جایی که از RealImage یک شی ایجاد شده است، نیاز به نمایش تصویر نداشتیم و تنها میخواستیم از شی اصلی ایجاد شده در پروسه ای استفاده کنیم. برای مثال این شی را به عنوان عکس پروفایل به شی user متصل کنیم و در چند خط پایین تر نمایش تصویر اتفاق بیافتد. همانطور که مشاهده میکنید از زمان ساخته شدن شی اصلی تا قبل از نمایش تصویر، عکس بر روی رم load شده بود در حالی که ضرورتی نداشت.
<?php
namespace App\DesignPatterns\Proxies\Image;
use App\DesignPatterns\Proxies\Image\ProxyImage;
class index
{
public function run()
{
$filename = 'test.png';
$image1 = new RealImage($filename);//loading image and use system resource
// Call some other methods to attach image to related user.
echo 'image object attached to user object .';
echo $image1->display();
}
}
· حال کلاس index را به صورت زیر پیاده سازی میکنیم به طوری که از proxy برای دسترسی به کلاس اصلی استفاده شده است. همانطور مشاهده میشود، یک شی از کلاس Proxy ایجاد شده و بعد از تمام عملیات متصل کردن شی image به شی user انجام شده است و سپس با استفاده از متد display موجود در این کلاس، تصویر load شده و نمایش داده میشود.در این حالت دقیقا تا قبل از استفاده ی واقعی از شی اصلی(یعنی نمایش)، عکسload نمیشود که این باعث شده که استفاده از شی اصلی به تعویق بیافتد و منابع سرور دیرتر درگیر شوند.
<?php
namespace App\DesignPatterns\Proxies\Image;
use App\DesignPatterns\Proxies\Image\ProxyImage;
class index
{
public function run()
{
$filename = 'test.png';
$image2 = new ProxyImage($filename);
// Call some other methods to attach image to related user.
echo 'image object attached to user object .';
echo $image2->display();//loading image and use system resource
}
}
5- جمعبندی
الگوی طراحی Proxy یک الگوی طراحی ساختاری است که امکان استفاده از یک واسطه به منظور جلوگیری از دسترسی مستقیم به یک سرویس دهنده را فراهم کرده و باعث متمرکز سازی کنترل دسترسی میشود. این الگو کاربردهای متنوعی دارد که هر کدام در توسعه ی هرچه بهتر برنامه ی ما کمک میکند. در این مقاله سعی داشتیم با یک مثال، استفاده ی یکی از این کاربرد ها را آموزش دهیم.
اگر در استفاده از این الگوی طراحی تجربه ای داشته اید که میتواند در انتخاب و استفاده ی آن به دیگران کمک کند، با ما در میان بگذارید.
6- منابع
1. https://refactoring.guru/design-patterns/proxy
2. https://sourcemaking.com/design_patterns/proxy
3. https://medium.com/@devlob/proxy-design-pattern-to-speed-up-your-applications-2416816493d