نمط سلسلة المسؤوليات

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

نمط سلسلة المسؤوليات (Chain of Responsibility) هو نمط تصميم سلوكي (Behavioral) يسمح لك بتمرير طلبات على سلسلة من المداوِلات (Handlers)، ويقرر كل مداوِل عند استلام الطلب أن يعالجه أو يمرره إلى العامل التالي في السلسلة.

ضع الصورة.

المشكلة

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

ثم إنك أدركت بعد قليل من التخطيط أن عمليات التحقق تلك يجب أن تتم بشكل متسلسل، ويمكن للتطبيق أن يحاول توثيق المستخدم في النظام كلما استلم طلبًا يحتوي اعتماديات المستخدم (User Credentials). لكن إن لم تكن تلك الاعتماديات صحيحة وفشل التوثيق فما الداعي إلى إتمام أي من خطوات التحقق التالية؟

ضع الصورة. يجب أن ينجح الطلب في سلسلة من عمليات التحقق قبل أن يعالجه النظام بنفسه.

ولنفرض أنك في خلال الأشهر التالية لتلك الخطوة قد استخدمت مزيدًا من عمليات التحقق التسلسلية تلك على النحو التالي:

  • اقتراح من أحد أصدقائك أنه من الخطر تمرير بيانات صريحة مباشرة إلى نظام الطلبات، وعليه فقد أضفتَ خطوة تحقق إضافية لتعقيم (Sanitize) البيانات داخل الطلب.
  • لاحقًا، يلاحظ شخص أن النظام عرضة لاختراق من نوع (Brute Force) -محاولات إدخال كلمات مرور كثيرة-. ولتجنب هذا أضفتَ عملية تحقق ترشِّح الطلبات الفاشلة المتكررة من نفس عنوان الـ IP.
  • اقتراح آخر بتسريع النظام من خلال إعادة النتائج المحفوظة للطلبات المكررة التي تحتوي نفس البيانات، وعليه فقد أضفتَ عملية تحقق إضافية لا تسمح للطلب بالوصول إلى النظام إلا إن لم تكن هناك نتيجة مناسبة محفوظة من قبل.

الصورة. كلما زاد حجم الشيفرة زاد تعقيدها.

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

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

الحل

يعتمد نمط سلسلة المسؤوليات كغيره من أنماط التصميم السلوكية على تحويل سلوكيات بعينها إلى كائنات مستقلة بذاتها تدعى مُداوِلات، وفي حالتنا فإن كل عملية تحقق يجب أن تُستخرَج إلى فئتها الخاصة بأسلوب وحيد ينفذ التحقق. ويُمرَّر الطلب مع بياناته إلى هذا الأسلوب على أنه وسيط (Argument).

ويقترح النمط أن تربط بين هذيْن المُداوِليْن في سلسلة، ولكل مداوِل مرتبط حقلٌ لتخزين مرجع إلى المداوِل التالي في السلسلة، وتعالج المُداوِلات الطلبَ وتمرره بينها قُدُمًا في السلسلة حتى يكون قد مر عليها جميعًا وحصلت جميعها على فرصة لمعالجته. والجميل في الأمر أن المداوِل قد يقرر ألا يمرر الطلب إلى ما بعده في السلسلة، ويوقف أي معالجة لاحقة.

وفي مثالنا مع نظام الطلبات، فإن المداوِل (Handler) ينفذ عملية المعالجة ثم يقرر ما إن كان سيمرر الطلب لما بعده في السلسلة أم لا، وبافتراض أن الطلب يحتوي على البيانات المناسبة فإن كل المداوِلات تستطيع تنفيذ سلوكها الأساسي سواء كان ذلك السلوك حفظًا أو تحققًا من التوثيق.

الصورة. تصطف المداوِلات واحدًا تلو الآخر لتكون سلسلة.

لكن هناك منظور مختلف قليلًا (ورسمي أكثر)، يقرر فيه المداوِل حين يستلم الطلب ما إن كان يستطيع معالجة الطلب أم لا، وعليه فإن الطلب إما أن يُعالج بواسطة مداوِل واحد أو لا يُعالَج على الإطلاق. يشيع هذا المنظور عند التعامل مع الأحداث (Events) في مكدَّسات العناصر داخل واجهة المستخدم الرسومية.

فمثلًا، حين ينقر مستخدم على زر فإن الحدث يُنشر في سلسلة عناصر الواجهة التي تبدأ بالزر وتنتهي بنافذة التطبيق الأساسية مرورًا بالحاويات -كالاستمارات أواللوحات (Panels)-. ويعالَج الحدث بواسطة أول عنصر يستطيع معالجته في السلسلة، وهذا المثال جدير بالذكر لأنه يظهر إمكانية استخراج سلسلة من داخل شجرة كائنات، (انظر ش.5).

الصورة. يمكن تشكيل سلسلة من فرع داخل شجرة كائنات.

من المهم أن تستخدم كل فئات المعالِج نفسَ الواجهة، ويجب أن يهتم كل معالِج حقيقي (Concrete Handler) بالمعالج التالي له فحسب، ذلك الذي يحتوي على أسلوب execute. وهكذا يمكنك تركيب السلاسل أثناء التشغيل باستخدام معالِجات مختلفة دون ربط شيفرتك بفئاتها الحقيقية.

مثال واقعي

الصورة. مكالمة واحدة مع خدمة الدعم الفني قد تمر على أكثر من موظف.

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

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

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

البنية

الصورة.

  1. تصرح فئة Handler عن الواجهة المشتركة لكل المعالِجات الحقيقية، وتحتوي في العادة على أسلوب واحد فقط لمعالجة الطلبات، لكن قد تحتوي أحيانًا على أسلوب آخر لتهيئة المعالِج التالي في السلسلة.
  2. فئة Base Handler هي فئة اختيارية تستطيع وضع شيفرة أساسية مشتركة بين كل فئات المعالِجات. وعادة ما تحدد هذه الفئة حقلًا لتخزين مرجع إلى المعالِج التالي، ويستطيع العميل بناء سلسلة بتمرير معالِج إلى المنشئ (Constructor) أو محدِّد (Setter) المعالِج السابق. كذلك قد تستخدمُ الفئةُ سلوك المعالجة الافتراضي، بأن تمرر التنفيذ إلى المعالِج التالي بعد التحقق من وجوده.
  3. تحتوي Concrete Handlers على الشيفرة الحقيقية لمعالجة الطلبات، ويجب أن يقرر كل معالِج عند استلام الطلب ما إن كان سيعالجه أم لا، وكذلك ما إن كان سيمرره لما بعده في السلسلة أم لا. وتكون المعالجات في العادة مستقلة بذاتها وغير قابلة للتغيير، وتقبل البيانات التي تحتاجها مرة واحدة فقط من خلال المنشئ.
  4. قد يركب العميل (Client) سلاسل مرة واحدة فقط أو يركبها بشكل ديناميكي، وفقًا لمنطق التطبيق. لاحظ أن الطلب يمكن إرساله إلى أي معالِج في السلسلة، ولا يشترط أن يكون أول معالج.

مثال توضيحي

في هذا المثال، يكون نمط سلسلة المسؤوليات مسؤولًا عن عرض معلومات المساعدة السياقية لعناصر واجهة رسومية نشطة.

الصورة. بُنيت فئات الواجهة الرسومية بنمط المركّب. وكل عنصر يرتبط بعنصره الحاوي. وتستطيع بناء سلسلة عناصر في أي وقت تبدأ بالعنصر نفسه وتمر بكل عناصره الحاوية.

تُبنى واجهة التطبيق الرسومية عادة على أنها شجرة كائنات، فمثلًا ستكون فئة Dialog التي تخرِج (Render) نافذة التطبيق الرئيسية، ستكون جذر شجرة الكائنات. وتحتوي فئة Dialog على Panels التي قد تحتوي لوحات أخرى بداخلها أو عناصر بسيطة منخفضة المستوى مثل Buttons و TextFields.

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

الصورة. كيفية مرور طلب مساعدة على كائنات الواجهة الرسومية.

حين يقف مستخدم بمؤشر الماوس على عنصر ويضغط مفتاح F1، فإن التطبيق يلتقط المكون الذي تحت المؤشر ويرسل إليه طلب مساعدة، ويمر الطلب على كل حاويات العنصر حتى يصل إلى العنصر القادر على عرض معلومات المساعدة.

// The handler interface declares a method for building a chain
// of handlers. It also declares a method for executing a
// request.
interface ComponentWithContextualHelp is
    method showHelp()


// The base class for simple components.
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string

    // The component's container acts as the next link in the
    // chain of handlers.
    protected field container: Container

    // The component shows a tooltip if there's help text
    // assigned to it. Otherwise it forwards the call to the
    // container, if it exists.
    method showHelp() is
        if (tooltipText != null)
            // Show tooltip.
        else
            container.showHelp()


// Containers can contain both simple components and other
// containers as children. The chain relationships are
// established here. The class inherits showHelp behavior from
// its parent.
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// Primitive components may be fine with default help
// implementation...
class Button extends Component is
    // ...

// But complex components may override the default
// implementation. If the help text can't be provided in a new
// way, the component can always call the base implementation
// (see Component class).
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            // Show a modal window with the help text.
        else
            super.showHelp()

// ...same as above...
class Dialog extends Container is
    field wikiPageURL: string

    method showHelp() is
        if (wikiPageURL != null)
            // Open the wiki help page.
        else
            super.showHelp()


// Client code.
class Application is
    // Every application configures the chain differently.
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPageURL = "http://..."
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does..."
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is an OK button that..."
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ...
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // Imagine what happens here.
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()