نمط الوكيل (Proxy)

من موسوعة حسوب

نمط الوكيل (Proxy) هو نمط تصميم إنشائي يسمح لك بتوفير بديل لكائن موجود لديك، ويتحكم في الوصول إلى الكائن الأصلي، مما يمكنك من تنفيذ إجراءات قبل أو بعد وصول الطلب إلى الكائن الأصلي.

المشكلة

إليك مثالًا يوضح الحالة التي قد ترغب فيها بتقييد الوصول إلى إلى كائن ما، لنفرض أن لديك كائنًا كبيرًا جدًا يستهلك موارد النظام بشراهة، وأنت لا تريده أصلًا بشكل مستمر، بل من وقت لآخر.

ضع الصورة. استعلامات قاعدة البيانات قد تكون بطيئة جدًا. قد تستفيد من أسلوب البدء الكسول/المؤجَّل (Lazy Initialization) بأن تنشئ هذا الكائن عند الحاجة إليه فقط، وسيحتاج كل عملاء هذا الكائن أن ينفذوا شيفرة بدء مؤجل (Deferred Initialization) غير أننا سنخرج من هذا بشيفرات مكررة. وفي الحالات المثالية فإننا نريد أن نضع هذه الشيفرة مباشرة في فئة الكائن الخاص بنا، لكن هذا لا يمكن ضمانه في كل الحالات، ذلك أن الفئة قد تكون مثلًا جزءًا من مكتبة طرف ثالث مغلقة.

الحل

يقترح نمط الوكيل أن تنشئ فئة وكيل جديدة بنفس الواجهة التي يستخدمها كائن الخدمة (Service Object)، ثم تحدِّث تطبيقك ليمرِّر كائن الوكيل إلى كل عملاء الكائن الأصلي، وعند استلام طلب من عميل ينشئ الوكيلُ كائنَ خدمةٍ حقيقي ويفوض كل المهام إليه.

الصورة. يتخفى الوكيل في هيئة قاعدة بيانات، ويمكنه التعامل مع البدء الكسول وحفظ النتائج دون ملاحظة العميل أو قاعدة البيانات الحقيقية.

لكن ما الفائدة؟ إن كنت تحتاج إلى تنفيذ إجراء قبل أو بعد المنطق الأساسي للفئة (Primary Logic) فإن الوكيل يسمح لك بهذا دون تغيير تلك الفئة، وبما أنه -أي الوكيل- يستخدم نفس الواجهة كالفئة الأصلية، فيمكن تمريره إلى أي عميل يتوقع كائن خدمة حقيقي.

مثال واقعي

الصورة. يمكن استخدام بطاقات الائتمان للدفع مثل النقود الورقية تمامًا.

تمثل بطاقات الائتمان وكيلًا للحساب البنكي الذي يكون وكيلًا لكمية مالية، وكلاهما يستخدم نفس الواجهة، أنهما يُستخدمان للدفع مقابل المشتريات. وذلك يسعد العميل لا شك لأنه ليس عليه حمل أمواله كلما حل وارتحل، كما يسعد البائع أيضًا لأنه يقيه مخاطر السرقة أثناء نقل المال إلى البنك، أو مخاطر فقد المال داخل المتجر.

البنية

  1. تصرح فئة Service Interface عن واجهة للخدمة، ويجب أن يتبع الوكيل هذه الواجهة ليتمكن من التخفي في صورة كائن خدمة.
  2. فئة الخدمة Service هي فئة تعطي بعض المنطق التجاري المفيد.
  3. فئة Proxy لها حقل مرجعي يشير إلى كائن الخدمة، وبعد أن ينهي الوكيل عمليات معالجته (البدء الكسول والتسجيل والتحكم في الوصول والحفظ، إلخ) فإنه يمرر الطلب إلى كائن الخدمة، وعادة يدير الوكلاء دورة الحياة كاملة لكائنات الخدمة الخاصة بها.
  4. يجب أن تعمل فئة Client مع كل من الخدمات والوكلاء من خلال نفس الواجهة، وهكذا يمكنك تمرير وكيل إلى أي شيفرة تتوقع كائن خدمة.

مثال توضيحي

يبين هذا المثال كيف يمكن لنمط الوكيل Proxy أن يساهم في إدخال البدء الكسول (Lazy Initialization) والحفظ (Caching) إلى مكتبة تكامل من طرف ثالث ليوتيوب.

الصورة. نتائج حفظ خدمة مع وكيل (Proxy)

تزودنا المكتبة بفئة تحميل للفيديو، لكنها ليست بالكفاءة المطلوبة، ذلك أنه إن طلب تطبيق العميل نفس الفيديو أكثر من مرة فإن المكتبة تحمِّله من جديد في كل مرة بدلًا من حفظه وإعادة استخدام الملف المحفوظ. وتستخدم فئة الوكيل نفس الواجهة التي يستخدمها المحمِّل الأصلي، وتفوض إليها كل المهام، لكنها تتابع الملفات المحمَّلة وتعيد النتائج المحفوظة عندما يطلب التطبيق نفس الفيديو أكثر من مرة.

// (Remote Service) واجهة خدمة بعيدة.
interface ThirdPartyYoutubeLib is
    method listVideos()
    method getVideoInfo(id)
    method downloadVideo(id)

// تستطيع أساليب هذه الفئة ،(Service Connector) الاستخدام الحقيقي لموصل الخدمة
// أن تطلب المعلومات من يوتيوب، وتعتمد سرعة الطلب على سرعة اتصال كل من المستخدم
// ويوتيوب بالإنترنت.
// سيبطئ أداء البرنامج إن حدثت طلبات كثيرة في نفس الوقت حتى لو طلبت كلها
// نقس البيانات.
class ThirdPartyYoutubeClass implements ThirdPartyYoutubeLib is
    method listVideos() is
        // إلى يوتيوب API أرسل طلب.

    method getVideoInfo(id) is
        // اجلب البيانات الوصفية لمقطع فيديو.

    method downloadVideo(id) is
        // حمِّل ملف فيديو من يوتيوب.

// نستطيع حفظ نتائج الطلب لبعض الوقت من أجل توفير البيانات، لكن قد يستحيل وضع 
// شيفرة كهذه في فئة الخدمة، فقد تكون جزءًا من مكتبة طرف ثالث و-أو معرّفة لفئة
// لهذا نضع الشيفرة المحفوظة في فئة وكيل جديدة تستخدم نفس الواجهة (Final) نهائية
// التي تستخدمها فئة الخدمة، وهي تفوض المهام إلى كائن الخدمة عند إرسال الطلبات
// الحقيقية فقط.
class CachedYoutubeClass implements ThirdPartyYouTubeLib is
    private field service: ThirdPartyYouTubeClass
    private field listCache, videoCache
    field needReset

    constructor CachedYoutubeClass(service: ThirdPartyYouTubeLib) is
        this.service = service

    method listVideos() is
        if (listCache == null || needReset)
            listCache = service.listVideos()
        return listCache

    method getVideoInfo(id) is
        if (videoCache == null || needReset)
            videoCache = service.getVideoInfo(id)
        return videoCache

    method downloadVideo(id) is
        if (!downloadExists(id) || needReset)
            service.downloadVideo(id)

// تظل فئة الواجهة الرسومية كما هي دون تغيير طالما أنها تعمل مع كائن الخدمة من خلال
// واجهة، هذه الفئة كانت تعمل مباشرة مع كائن الخدمة.
// ونستطيع تمرير كائن وكيل بدلًا من كائن خدمة حقيقي بما أنهما يستخدمان نفس الواجهة.
class YoutubeManager is
    protected field service: ThirdPartyYouTubeLib

    constructor YoutubeManager(service: ThirdPartyYouTubeLib) is
        this.service = service

    method renderVideoPage(id) is
        info = service.getVideoInfo(id)
        // صفحة الفيديو (Render) أخرج.

    method renderListPanel() is
        list = service.listVideos()
        // أخرج قائمة مصغَّرات الفيديو.

    method reactOnUserInput() is
        renderVideoPage()
        renderListPanel()

// يستطيع البرنامج إعداد الوكلاء عند الحاجة.
class Application is
    method init() is
        aYouTubeService = new ThirdPartyYouTubeClass()
        aYouTubeProxy = new CachedYouTubeClass(aYouTubeService)
        manager = new YouTubeManager(aYouTubeProxy)
        manager.reactOnUserInput()

قابلية التطبيق