نمط الوكيل (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()

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

توجد عشرات الاستخدامات لنمط الوكيل، إليك بعض أكثر هذه الاستخدامات شهرة:

  • البدء الكسول (الوكيل الوهمي Virtual Proxy)، عندما يكون لديك كائن خدمة كبير الحجم يهدر موارد النظام بكونه نشطًا دائمًا رغم أنك لا تحتاجه إلا من وقت لآخر، فبدلًا من إنشاء الكائن عند فتح البرنامج، تستطيع تأخير بدء الكائن إلى حين الحاجة إليه.

تقييد الوصول (وكيل الحماية Protection Proxy)، حين تريد لعملاء بعينهم أن يستخدموا كائن الخدمة، كأن تمثل كائناتك أجزاءً مهمة من نظام تشغيل والعملاء هم برامج متعددة (بما في ذلك البرامج الخبيثة). ويمرر الوكيل الطلب إلى كائن الخدمة فقط في حالة إن كانت اعتماديات الوكيل (Proxy Credentials) تطابق بعض المعايير.

  • التنفيذ المحلي لخدمة بعيدة (الوكيل البعيد Remote Proxy)، في حالة وجود كائن الخدمة على خادم بعيد، إذ في تلك الحالة يمرر الوكيلُ طلبَ العميلِ إلى الشبكة مسلِّمًا كل التفاصيل الخاصة بالعمل مع الشركة.

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

  • حفظ نتائج الطلب (وكيل الحفظ Caching Proxy)، حين تحتاج أن تحفظ نتائج طلبات عميل وتدير دورة حياة هذه المحفوظات، خاصة إن كانت النتائج كبيرة جدًا. يستطيع الوكيل أن يستخدم الحفظ للطلبات المتكررة التي تخرج نفس النتائج دومًا، وقد يستخدم معامِلات الطلبات كمفاتيح للحفظ (Cache Keys).

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

كذلك يستطيع الوكيل متابعة ما إن كان العميل قد عدَّل كائن الخدمة أم لا، ثم يمكن للعملاء الآخرين إعادة استخدام الكائنات التي لم تتغير.

كيفية الاستخدام

  1. إن لم تكن واجهة الخدمة موجودة فأنشئ واحدة لجعل الوكيل وكائنات الخدمة متبادلين (Interchangeable)، واعلم أن استخراج الواجهة من فئة الخدمة قد يكون غير ممكن أحيانًا، ذلك أنك ستحتاج إلى تغيير كل عملاء الخدمة لاستخدام تلك الواجهة، والخيار البديل لك هنا هو جعل الوكيل فئةً فرعية لفئة الخدمة كي يكتسب واجهتها.
  2. أنشئ فئة الوكيل، يجب أن تحتوي على حقل لتخزين مرجع إلى الخدمة، وعادة ينشئ الوكلاء ويديروا