سلام دوستان. می‌خوایم با الگویی آشنا بشیم که بهمون کمک می‌کنه تا اتفاق‌هایی که برای یک آبجکت رخ میده رو بتونیم کنترل کنیم. این الگو پراکسی (Proxy) هست و معمولاً برای مدیریت کردن دسترسی و یا اضافه کردن ویژگی‌هایی به یک آبجکت استفاده میشه.

موارد زیر رو توی این پست بررسی می‌کنیم:

 

مشکل کجاست؟ 🤔

فرض کنیم آبجکتی داریم که عملیات سنگینی انجام میده و باعث مصرف مقدار زیادی از منابع میشه. برای مثال برای دانلود فایل کلاسی در اختیار داریم به صورت زیر:

class FileDownloader {
  public download(url) {
    http.get(url, function (file) {
      file.save("/home");
    });
  }
}

و به این صورت ازش استفاده می‌کنیم:

const downloader = new FileDownloader();
downloader.download('http://path-to-file.jpg');
downloader.download('http://path-to-file.jpg');
downloader.download('http://path-to-file.jpg');
downloader.download('http://path-to-file.jpg');

همونطور که می‌بینیم توی چهار خط آخر داریم یک فایل رو ۴ بار دانلود می‌کنیم که در نتیجه اگه با یک فایل حجیم سر و کار داشته باشیم باعث هدر رفتن و درگیر شدن بیش‌از حد منابع میشه. ما با الگوی Proxy می‌تونیم کاری کنیم که این فایل فقط یک بار دانلود و ذخیره بشه و توی درخواست‌های مشابه بعدی همون فایل برگردونده بشه.

 

الگوی پراکسی (Proxy) چیه؟

اگه معنی تحت‌الفظی پراکسی رو بدونیم درک این الگو برامون راحت‌تر میشه. پراکسی توی زبان پارسی یعنی نمایندهٔ رسمی شخص دیگری بودن برای انجام یک کار.

توی الگوی پراکسی، یک آبجکت، نمایندهٔ یک آبجکت دیگه برای انجام کارهای اون آبجکت میشه. پس می‌تونیم بگیم که:

یک پراکسی، آبجکتی هست که در نقش یک رابط یا واسط برای یک آبجکت دیگه عمل می‌کنه.

توی مثال بالا، کلاس FileDownloader سرویسی بود که می‌خواستیم از اون استفاده کنیم. اما این سرویس ناقص بود و یا کلاً ویژگی‌های مد نظر ما (جلوگیری از هدر رفت منابع) رو نداشت. برای حل مشکلمون، شاید به ذهنمون برسه که بیایم خود کلاس FileDownloader رو دستکاری کنیم. اما خیلی وقت‌ها این کار امکان‌پذیر نیست. مثلاً FileDownloader یک کلاس از یک پکیج خارجی هست و یا کلاً کدهای اون در دسترس نیست.

برای حل این مشکل، می‌تونیم یک کلاس دیگه بسازیم که توی خودش یک نمونه از کلاس FileDownloader رو نگهداری می‌کنه. اینطوری می‌تونیم توی کلاس جدید ویژگی‌های مد نظرمون رو پیاده‌سازی کنیم و همچنین از قابلیت‌های کلاس FileDownloader استفاده کنیم. به این کلاس جدید می‌گیم پراکسی. کافیه توی برنامه بجای کلاس FileDownloader از کلاسِ پراکسی‌شدهٔ اون استفاده کنیم. پراکسی یک رابط هست برای سرویس اصلی (FileDownloader).

الگوی پراکسی زیر مجموعهٔ الگوی‌های Structural هست. یعنی الگوهایی که کمک می‌کنن تا آبجکت‌های موجود طوری با هم در تعامل باشن تا بتونیم ساختارهای بزرگتری طراحی کنیم.

 

پیاده‌سازی الگوی پراکسی

این الگو پیاده‌سازی خیلی راحتی داره.

مرحله اول: ساختن اینترفیس

مرحله اول اینه که کاری کنیم کلاس اصلی و کلاس پراکسی توی قسمت Client (قسمتی از برنامه که می‌خوایم از این کدها و کلاس‌ها استفاده می‌کنیم) با همدیگه قابل جایگزینی و تعویض باشن. یعنی مثلاً اگه بجای کلاس اصلی از پراکسی (یا بلعکس) استفاده کردیم، برنامه دچار خطا نشه. چرا باید چنین کاری کنیم؟ چونکه برای کلاینت نباید فرقی کنه که داره با چه کلاسی (اصلی یا پراکسی) کار می‌کنه. کلاینت فقط می‌خواد فایل رو دانلود کنه و ذخیره کنه و نباید درگیر جزییاتی مثل بهینگی مصرف منابع بشه.

اینجا Abstraction به کارمون میاد. یک اینترفیس درست می‌کنیم تا هم برای کلاس اصلی و هم پراکسی پیاده‌سازی بشه:

interface DownloaderInterface {
  download(path);
}

البته اگه ساختن یک اینترفیس ممکن نبود (مثلاً سرویسی که می‌خوایم اون رو پراکسی کنیم قابل دسترسی و ویرایش نباشه) یک راه جایگزین اینکه که پراکسی از کلاس اصلی ارث‌بری کنه.

حالا کاری می‌کنیم که کلاس اصلی از این اینترفیس تبعیت کنه:

class FileDownloader <<implements DownloaderInterface>> {
  public download(path) {
      
  }
}

 

مرحله دوم: ساختن کلاس پراکسی

برای سرویس اصلی یک کلاس پراکسی می‌سازیم. این کلاس باید از اینترفیس بالا تبعیت کنه. همچنین باید توی خودش یک فیلد برای نگهداری سرویس اصلی داشته باشه:

class FileDownloaderProxy implements DownloaderInterface {
  private <<downloader>>: FileDownloader;
  
  constructor() {
    this.downloader = new FileDownloader();
  }

  public download(path) {
      
  }
}

 

مرحله سوم: پیاده‌سازی ویژگی‌های مد نظر، توی پراکسی

حالا ویژگی‌هایی که می‌خواستیم کلاس اصلی داشته باشه رو توی پراکسی می‌نویسیم:

class FileDownloaderProxy implements DownloaderInterface {
  // ...
  private cachedFiles = {};

  public download(path) {
    if (this.cachedFiles.hasOwnProperty(path)) {
      console.log(`Read ${path} from cache`);

      return this.cachedFiles[path];
    } else {
      const result = this.downloader.download(path);
      this.cachedFiles[path] = result;

      return result;
    }
  }
}

توی پراکسی گفتیم که اگه فایل از قبل دانلود و کش شده بود، همون رو برگردون. در غیر این صورت ابتدا فایل رو با سرویس اصلی (خط ۱۰) دانلود کن، بعد اون رو به کش اضافه کن و نهایتاً اون رو برگردون.

حالا باید قسمت کلاینت رو بسازیم:

function client(downloader: DownloaderInterface) {
  downloader.download('http://path-to-file.jpg');
  downloader.download('http://path-to-file.jpg');
  downloader.download('http://path-to-file.jpg');
  downloader.download('http://path-to-file.jpg');
  downloader.download('http://path-to-file.jpg');
}

client(new FileDownloaderProxy());

// Downloading http://path-to-file.jpg...
// Read http://path-to-file.jpg from cache
// Read http://path-to-file.jpg from cache
// Read http://path-to-file.jpg from cache
// Read http://path-to-file.jpg from cache

اینجا نکتهٔ مهمی که قسمت کلاینت داره اینه که داره با اینترفیس DownloaderInterface کار می‌کنه و براش مهم نیست چیزی که از بیرون داره بهش پاس داده میشه آبجکت اصلی هست یا حالت پراکسی شدهٔ اون. این بود یک نمونهٔ ساده از الگوی پراکسی 💯

(Star بزنین 😉)

 

کاربردهای الگوی پراکسی

این الگو کاربردهای خیلی زیادی داره و می‌تونه به عنوان یک رابط یا واسط برای هر چیزی عمل کنه. این واسط در نهایت و بعد از بررسی‌هایی که توی خودش انجام میده، می‌تونه درخواستی که از سمت کلاینت اومده رو منتقل می‌کنه به سرویس اصلی یا به اون ویژگی‌های دلخواه رو اضافه کنه.

 

۱. کمک توی پیاده‌سازی سرویسی که پیاده‌سازی اون منابع زیادی رو مصرف می‌کنه

با الگوی پراکسی می‌تونیم آبجکتی که پیاده‌سازی اون منابع زیادی رو مصرف می‌کنه رو به صورت Lazy Load پیاده‌سازی کنیم. یعنی به جای اینکه آبجکت رو همون ابتدا که برنامه اجرا میشه پیاده‌سازی کنیم، با پراکسی، اون رو فقط زمانی پیاده‌سازی کنیم که توی برنامه واقعاً می‌خوایم ازش استفاده کنیم.

۲. کنترل دسترسی‌ها به یک آبجکت

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

۳. گزارش‌گیری

با این الگو می‌تونیم از درخواست‌هایی که به سمت سرویس اصلی زده میشه، گزارش‌هایی ذخیره کنیم.

۴. کش (Cache) کردن نتایج یک عملیات سنگین

درست مشابه مثالی که داشتیم، از این الگو می‌تونیم برای ذخیره کردن خروجیِ یک عملیات سنگین استفاده کنیم تا استفادهٔ بهینه‌ای از منابع داشته باشیم.

 

مزایای الگوی پراکسی

۱. می‌تونیم یک سرویس رو بدون اینکه کلاینت‌ها در اطلاع داشته باشن کنترل کنیم

۲. می‌تونیم بدون اینکه سرویس اصلی رو دستکاری کنیم، پراکسی‌های متنوع بسازیم (اصل دوم سالید - OCP)

 

معایب الگوی پراکسی

۱. با اضافه کردن کلاس‌های جدید ممکنه برنامه شلوغ و پیچیده‌تر بشه

۲. با توجه به محاسبات اضافی که پراکسی انجام میده، ممکنه پاسخ از سمت سرویس اصلی با تاخیر بیشتری بیاد

 

خب دوستان امیدوارم از مطالب و نکته‌هایی که بررسی کردیم استفاده کرده باشین. روزتون خوش 😉✌️

https://refactoring.guru/design-patterns/proxy

https://en.wikipedia.org/wiki/Proxy_pattern

https://abadis.ir/entofa/proxy