نمط سلسلة المسؤوليات
نمط سلسلة المسؤوليات (Chain of Responsibility) هو نمط تصميم سلوكي (Behavioral) يسمح لك بتمرير طلبات على سلسلة من المداوِلات (Handlers)، ويقرر كل مداوِل عند استلام الطلب أن يعالجه أو يمرره إلى العامل التالي في السلسلة.
المشكلة
تخيل أنك تعمل على نظام طلبات أونلاين، وتريد أن تقيد الوصول إلى النظام كي يكون إنشاء الطلبات مسموحًا به للمستخدمين الموثَّقين فقط. كذلك يجب أن يكون للمستخدمين الذين يملكون صلاحيات الإدارة حقُ الدخول إلى كل الطلبات.
ثم إنك أدركت بعد قليل من التخطيط أن عمليات التحقق تلك يجب أن تتم بشكل متسلسل، ويمكن للتطبيق أن يحاول توثيق المستخدم في النظام كلما استلم طلبًا يحتوي اعتماديات المستخدم (User Credentials). لكن إن لم تكن تلك الاعتماديات صحيحة وفشل التوثيق فما الداعي إلى إتمام أي من خطوات التحقق التالية؟ ضع الصورة. يجب أن ينجح الطلب في سلسلة من عمليات التحقق قبل أن يعالجه النظام بنفسه.
ولنفرض أنك في خلال الأشهر التالية لتلك الخطوة قد استخدمت مزيدًا من عمليات التحقق التسلسلية تلك على النحو التالي:
- اقتراح من أحد أصدقائك أنه من الخطر تمرير بيانات صريحة مباشرة إلى نظام الطلبات، وعليه فقد أضفتَ خطوة تحقق إضافية لتعقيم (Sanitize) البيانات داخل الطلب.
- لاحقًا، يلاحظ شخص أن النظام عرضة لاختراق من نوع (Brute Force) -محاولات إدخال كلمات مرور كثيرة-. ولتجنب هذا أضفتَ عملية تحقق ترشِّح الطلبات الفاشلة المتكررة من نفس عنوان الـ IP.
- اقتراح آخر بتسريع النظام من خلال إعادة النتائج المحفوظة للطلبات المكررة التي تحتوي نفس البيانات، وعليه فقد أضفتَ عملية تحقق إضافية لا تسمح للطلب بالوصول إلى النظام إلا إن لم تكن هناك نتيجة مناسبة محفوظة من قبل.
الصورة. كلما زاد حجم الشيفرة زاد تعقيدها. شيفرة التحققات التي صارت فوضى بالصورة الحالية لها، ستتضخم أكثر وأكثر كلما أضفت مزية جديدة، بل قد يؤثر تعديل عملية تحقق واحدة أحيانًا في عمل عمليات تحقق أخرى غيرها. وأسوأ من ذلك كله أن تلجأ لتكرار شيفرتك الفوضوية تلك حين تحاول إعادة استخدام عمليات التحقق لحماية أجزاء أخرى من النظام، بما أن تلك الأجزاء تحتاج بعض عمليات التحقق التي لديك لكن ليس جميعها.
ويصعب حينها استيعاب النظام وتزيد تكلفة صيانته، وستجد نفسك تعاني معه وتصبر عليه إلى أن يأتي يوم وتعيد هيكلته بالكامل.
الحل
يعتمد نمط سلسلة المسؤوليات كغيره من أنماط التصميم السلوكية على تحويل سلوكيات بعينها إلى كائنات مستقلة بذاتها تدعى مُداوِلات، وفي حالتنا فإن كل عملية تحقق يجب أن تُستخرَج إلى فئتها الخاصة بأسلوب وحيد ينفذ التحقق. ويُمرَّر الطلب مع بياناته إلى هذا الأسلوب على أنه وسيط (Argument).
ويقترح النمط أن تربط بين هذيْن المُداوِليْن في سلسلة، ولكل مداوِل مرتبط حقلٌ لتخزين مرجع إلى المداوِل التالي في السلسلة، وتعالج المُداوِلات الطلبَ وتمرره بينها قُدُمًا في السلسلة حتى يكون قد مر عليها جميعًا وحصلت جميعها على فرصة لمعالجته. والجميل في الأمر أن المداوِل قد يقرر ألا يمرر الطلب إلى ما بعده في السلسلة، ويوقف أي معالجة لاحقة.
وفي مثالنا مع نظام الطلبات، فإن المداوِل (Handler) ينفذ عملية المعالجة ثم يقرر ما إن كان سيمرر الطلب لما بعده في السلسلة أم لا، وبافتراض أن الطلب يحتوي على البيانات المناسبة فإن كل المداوِلات تستطيع تنفيذ سلوكها الأساسي سواء كان ذلك السلوك حفظًا أو تحققًا من التوثيق.
الصورة. تصطف المداوِلات واحدًا تلو الآخر لتكون سلسلة. لكن هناك منظور مختلف قليلًا (ورسمي أكثر)، يقرر فيه المداوِل حين يستلم الطلب ما إن كان يستطيع معالجة الطلب أم لا، وعليه فإن الطلب إما أن يُعالج بواسطة مداوِل واحد أو لا يُعالَج على الإطلاق. يشيع هذا المنظور عند التعامل مع الأحداث (Events) في مكدَّسات العناصر داخل واجهة المستخدم الرسومية.
فمثلًا، حين ينقر مستخدم على زر فإن الحدث يُنشر في سلسلة عناصر الواجهة التي تبدأ بالزر وتنتهي بنافذة التطبيق الأساسية مرورًا بالحاويات -كالاستمارات أواللوحات (Panels)-. ويعالَج الحدث بواسطة أول عنصر يستطيع معالجته في السلسلة، وهذا المثال جدير بالذكر لأنه يظهر إمكانية استخراج سلسلة من داخل شجرة كائنات، (انظر ش.5).
الصورة. يمكن تشكيل سلسلة من فرع داخل شجرة كائنات.
من المهم أن تستخدم كل فئات المعالِج نفسَ الواجهة، ويجب أن يهتم كل معالِج حقيقي (Concrete Handler) بالمعالج التالي له فحسب، ذلك الذي يحتوي على أسلوب execute
. وهكذا يمكنك تركيب السلاسل أثناء التشغيل باستخدام معالِجات مختلفة دون ربط شيفرتك بفئاتها الحقيقية.
مثال واقعي
الصورة. مكالمة واحدة مع خدمة الدعم الفني قد تمر على أكثر من موظف. لنفرض أنك اشتريت قطعة عتاد في حاسوبك، وتحاول الآن أن تجرب أداء هذه القطعة الجديدة على أنظمة التشغيل المختلفة التي لديك، ووجدت أن ويندوز يلتقط وجودها ويفعّلها تلقائيًا، بينما لا يشعر بها لينكس، فتقرر التحدث مع الدعم الفني.
أول شيء تسمعه في المكالمة هو الرسالة المسجلة الآلية التي تقترح بضعة حلول للمشاكل المشهورة، ولمّا لم تجد حل مشكلتك قررت التحدث إلى أحد الموظفين في الدعم الفني، لكن موظف الدعم الفني لا يفعل سوى تلاوة الردود المكررة على العملاء دون الاستماع إلى وصف مشكلتك أو تقديم حل لها، وفي النهاية لا تخرج منه بشيء نافع فتطلب التحدث مع أحد المهندسين.
وعندما يوصلك الموظف بالمهندس المختص الذي يرشدك إلى موقع تحميل التعريفات المناسبة لقطعة العتاد، وكيفية تثبيتها على لينكس، لتجد مشكلتك قد حُلت في النهاية.
البنية
- تصرح فئة Handler عن الواجهة المشتركة لكل المعالِجات الحقيقية، وتحتوي في العادة على أسلوب واحد فقط لمعالجة الطلبات، لكن قد تحتوي أحيانًا على أسلوب آخر لتهيئة المعالِج التالي في السلسلة.
- فئة Base Handler هي فئة اختيارية تستطيع وضع شيفرة أساسية مشتركة بين كل فئات المعالِجات. وعادة ما تحدد هذه الفئة حقلًا لتخزين مرجع إلى المعالِج التالي، ويستطيع العميل بناء سلسلة بتمرير معالِج إلى المنشئ (Constructor) أو محدِّد (Setter) المعالِج السابق. كذلك قد تستخدمُ الفئةُ سلوك المعالجة الافتراضي، بأن تمرر التنفيذ إلى المعالِج التالي بعد التحقق من وجوده.
- تحتوي Concrete Handlers على الشيفرة الحقيقية لمعالجة الطلبات، ويجب أن يقرر كل معالِج عند استلام الطلب ما إن كان سيعالجه أم لا، وكذلك ما إن كان سيمرره لما بعده في السلسلة أم لا. وتكون المعالجات في العادة مستقلة بذاتها وغير قابلة للتغيير، وتقبل البيانات التي تحتاجها مرة واحدة فقط من خلال المنشئ.
- قد يركب العميل (Client) سلاسل مرة واحدة فقط أو يركبها بشكل ديناميكي، وفقًا لمنطق التطبيق. لاحظ أن الطلب يمكن إرساله إلى أي معالِج في السلسلة، ولا يشترط أن يكون أول معالج.
مثال توضيحي
في هذا المثال، يكون نمط سلسلة المسؤوليات مسؤولًا عن عرض معلومات المساعدة السياقية لعناصر واجهة رسومية نشطة. الصورة. بُنيت فئات الواجهة الرسومية بنمط المركّب. وكل عنصر يرتبط بعنصره الحاوي. وتستطيع بناء سلسلة عناصر في أي وقت تبدأ بالعنصر نفسه وتمر بكل عناصره الحاوية.
تُبنى واجهة التطبيق الرسومية عادة على أنها شجرة كائنات، فمثلًا ستكون فئة Dialog
التي تخرِج (Render) نافذة التطبيق الرئيسية، ستكون جذر شجرة الكائنات. وتحتوي فئة Dialog على Panels
التي قد تحتوي لوحات أخرى بداخلها أو عناصر بسيطة منخفضة المستوى مثل Buttons
و TextFields
.
قد يظهر مكوِّن بسيط نصائح سياقية مختصرة طالما أن المكوِّن قد خُصص له بعض نصوص المساعدة، لكن المكونات الأكثر تعقيدًا تحدد طريقتها الخاصة في إظهار المساعدة السياقية، مثل إظهار مقتطف من دليل الاستخدام أو فتح صفحة في متصفح. الصورة. كيفية مرور طلب مساعدة على كائنات الواجهة الرسومية.
حين يقف مستخدم بمؤشر الماوس على عنصر ويضغط مفتاح F1، فإن التطبيق يلتقط المكون الذي تحت المؤشر ويرسل إليه طلب مساعدة، ويمر الطلب على كل حاويات العنصر حتى يصل إلى العنصر القادر على عرض معلومات المساعدة.
// تصرح واجهة المداول عن أسلوب لبناء سلسلة من المداوِلات، كما تصرح عن
// أسلوب لتنفيذ الطلب.
interface ComponentWithContextualHelp is
method showHelp()
// الفئة الأساسية للمكونات البسيطة.
abstract class Component implements ComponentWithContextualHelp is
field tooltipText: string
// تتصرف حاوية المكون كالرابط التالي في سلسلة المداوِلات.
protected field container: Container
// يعرض المكون تلميحًا إن كان قد خُصص له نص مساعدة، وإلا فإنه يرحِّل
// الاستدعاء إلى الحاوية إن وجدت.
method showHelp() is
if (tooltipText != null)
// اعرض تلميحًا.
else
container.showHelp()
// تستطيع الحاويات احتواء كل من المكونات البسيطة والحاويات الأخرى
// في صورة فروع لها. وتتم علاقات السلسلة هنا.
// من الفئة الأم لها (showHelp) كما تكتسب الفئة سلوك أسلوب.
abstract class Container extends Component is
protected field children: array of Component
method add(child) is
children.add(child)
child.container = this
// قد لا يمثل الاستخدام الافتراضي للمساعدة مشكلة مع المكونات الأولية البسيطة.
class Button extends Component is
// ...
// لكن قد تتخطى المكوناتُ المعقدةُ الاستخدام الافتراضي، وإن لم يكن
// ممكنًا إثبات نص المساعدة بشكل آخر، فإن المكون الأساسي يستطيع
// استدعاء الاستخدام الأساسي
// Component انظر فئة.
class Panel extends Container is
field modalHelpText: string
method showHelp() is
if (modalHelpText != null)
// اعرض نافذة مشروطة مع نص مساعدة.
else
super.showHelp()
// ...تمامًا كما في الأعلى...
class Dialog extends Container is
field wikiPageURL: string
method showHelp() is
if (wikiPageURL != null)
// افتح صفحة مساعدة ويكي.
else
super.showHelp()
// شيفرة العميل.
class Application is
// يهيئ كل تطبيق السلسلة بشكل مختلف.
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)
// تخيل ما سيحدث هنا.
method onF1KeyPress() is
component = this.getComponentAtMouseCoords()
component.showHelp()
قابلية التطبيق
استخدم نمط سلسلة المسؤوليات عندما يُفترض ببرنامجك أن يعالج أنواعًا مختلفة من الطلبات بطرق متعددة، لكن نفس تلك الأنواع وتسلسلاتها لا تكون معروفة بشكل مسبق.
يسمح لك النمط بربط مداوِلات متعددة في سلسلة واحدة، وعند استلام طلب فإنك تستطيع سؤال كل مداوِل عما إن كان يستطيع معالجة الطلب، وبهذه الطريقة تحصل كل المداوِلات على فرصة لمعالجة الطلب.
استخدم النمط حين يكون من الضروري تنفيذ عدة مداوِلات في ترتيب محدد.
بما أنك تستطيع ربط المداوِلات في السلسلة بأي ترتيب، فإن كل الطلبات ستمر خلال السلسلة كما خططت تمامًا.
استخدم نمط سلسلة المسؤوليات عندما يُفترض بمجموعة مداوِلات وترتيبها أن يتغيروا عند وقت التشغيل (Runtime).
إن كنت قد زودتَ حقلًا مرجعيًا داخل فئات المداوِل بمحدِّدات فستكون قادرًا على إدخال أو حذف أو إعادة ترتيب المداوِلات بمرونة.
كيفية الاستخدام
- صرِّح عن واجهة المداوِل وَصِف توقيع الأسلوب (Signature of the Method) لمعالجة الطلبات. قرر الكيفية التي سيمرر العميلُ بها بيانات الطلب إلى الأسلوب، وأكثر طريقة مرونةً هي تحويل الطلب إلى كائن وتمريره إلى أسلوب المعالجة كوسيط (Argument).
- لتلافي تكرار الشيفرة الأساسية في المداوِلات الحقيقية، سيكون من المجدي إنشاء فئة مداوِل أساسي مجرد (Abstract Base Handler)، متفرعة من واجهة المداوِل. ويجب أن يكون في تلك الفئة حقلٌ لتخزين مرجع إلى المداوِل التالي في السلسلة، ولا تنس جعل الفئة غير قابلة للتغيير (immutable)، لكن إن كنت تريد تعديل السلاسل أثناء وقت التشغيل فستحتاج إلى تعريف محدِّد (Setter) لتغيير قيمة الحقل المرجعي. كذلك تستطيع أيضًا تطبيق السلوك الافتراضي المناسب لأسلوب المعالجة، بأن توجه الطلب إلى الكائن التالي إلا أن يكون هو الكائن الأخير، وستكون المداوِلات الحقيقية (Concrete Handlers) قادرة على استخدام هذا السلوك من خلال استدعاء الأسلوب الأم.
- أنشئ فئات فرعية للمداوِلات الحقيقية واحدة تلو الأخرى، ثم طبِّق أساليب المعالجة الخاصة بها، يجب أن يتخذ كل مداوِل قرارين عند استلام الطلب:
- هل سيعالِج الطلب؟
- هل سيمرِّر الطلب في السلسلة؟
- قد يجمِّع العميل سلاسل بنفسه أو يستلم سلاسل مبنية مسبقًا من كائنات أخرى، وفي الحالة الثانية فيجب أن تستخدم بعض فئات المصنع (factory classes) من أجل بناء سلاسل وفقًا لإعدادات التهيئة أو البيئة.
- يستطيع العميل أن ينبِّه (trigger) أي مداوِل في السلسلة، وليس شرطًا أن ينبه المداوِل الأساسي فقط، وسيمرَّر الطلب في السلسلة حتى يرفض أحد المداوِلات أن يمرر الطلب لما بعده في السلسلة أو حتى يصل إلى نهاية السلسلة.
- بسبب طبيعة السلسلة المرنة فإن العميل يجب أن يكون مستعدًا للتعامل مع السيناريوهات التالية:
- قد تتكون السلسلة من رابط واحد.
- قد لا تصل بعض الطلبات نهاية السلسلة.
- قد يصل بعضها الآخر إلى نهاية السلسلة دون معالجة.
المزايا والعيوب
المزايا
- تستطيع التحكم في ترتيب معالجة الطلبات.
- مبدأ المسؤولية الواحدة. تستطيع فصل الفئات التي تطلب (Invoke) العمليات عن الفئات التي تنفذ (Perform) العمليات.
- مبدأ المفتوح/المغلق. تستطيع إدخال مداوِلات جديدة في التطبيق دون تعطيل شيفرة العميل الحالية.
العيوب
- قد لا تُعالج بعض الطلبات.
العلاقات مع الأنماط الأخرى
- تختلف أنماط سلسلة المسؤوليات (Chain of Responsibility) والأمر (Command) والوسيط (Mediator) والمراقب (Observer) في طرق توصيل مستقبلي الطلبات ومرسليها ببعضهم، وترى ذلك الاختلاف فيما يلي:
- تمرِّر سلسلة المسؤوليات الطلب بشكل تسلسلي في سلسلة مرنة من المستقبلين المحتملين إلى أن يعالج أحدهم الطلب.
- ينشئ نمط الأمر وصلات أحادية الاتجاه (Unidirectional) بين المرسلين والمستقبلين.
- يلغي نمط الوسيط الاتصالات المباشرة بين المرسلين والمستقبلين مجبرًا إياهم على التواصل بشكل غير مباشر من خلال كائن وسيط.
- يسمح نمط المراقب للمستقبلين بالاشتراك في استلام الطلبات وكذلك إلغاء الاشتراك بمرونة.
- يستخدم نمط سلسلة المسؤوليات بالتزامن غالبًا مع نمط المركَّب (Composite)، وعندئذ يمرر العنصر الفرعي (Leaf Component - أصغر وحدة فرعية في شجرة الكائنات) الطلبَ في السلسلة التي تحتوي على كل المكونات الرئيسية (Parent Components) وصولًا إلى جذر شجرة الكائنات.
- يمكن استخدام المداوِلات في نمط سلسلة المسؤوليات ككائنات من نمط الأمر (Commands)، وفي تلك الحالة تستطيع تنفيذ عمليات كثيرة على نفس الكائنا السياقي الممثَّل في الطلب. لكن هناك منظور آخر يكون فيه الطلب نفسه كائنًا من نمط الأمر (Command)، وهنا تستطيع تنفيذ نفس العملية في سلسلة من السياقات المختلفة المرتبطة ببعضها في سلسلة.
- يتشابه نمطا سلسلة المسؤوليات والمزخرِف في بنية الفئات إلى حد كبير، فكلا النمطين يعتمدان على التركيب التكراري (Recursive Composition) لتمرير التنفيذ في سلسلة من الكائنات، لكن بينهما عدة اختلافات جوهرية.
- فمداوِالات نمط سلسلة المسؤوليات تستطيع تنفيذ عمليات متعاقبة (Arbitrary) مستقلة عن بعضها، كما يمكنها إيقاف تمرير الطلب عند أي نقطة في السلسلة.
- أما في نمط المزخرِف، فتستطيع مزخرِفات عديدة أن توسع سلوك الكائن مع الحفاظ على ثباته مع الواجهة الأساسية، ولا يُسمح للمزخرِفات أن تعطل سير الطلب.
الاستخدام في لغة جافا
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات. لكن أحد الاستخدامات المشهورة للنمط هو إرسال أحداث إلى المكونات الرئيسية (Parent Components) في فئات الواجهة الرسومية، كذلك يُستخدم في مرشِّحات الوصول التسلسلية (Sequential Access Filters). إليك بعض الأمثلة على النمط في مكتبات جافا:
يمكن ملاحظة النمط من خلال الأساليب السلوكية لكائن يستدعي -بشكل غير مباشر - نفس الأساليب في الكائنات الأخرى، بينما تتبع كل الكائنات نفس الواجهة المشتركة.
ترشيح الوصول Filtering Access
يبين هذا المثال كيف يمكن لطلب يحتوي على بيانات المستخدم أن يمرر سلسلة متعاقبة من المداوِلات التي تنفذ أمورًا مختلفة كالتصديق (Authentification) والتصريح (Authorization) والتحقق (Validation).
سيكون هذا المثال مختلفًا قليلًا عن النمط الذي يعرضه المؤلفون عادة، فأغلب الأمثلة الموجودة مبنية على فكرة البحث عن المداوِل المناسب وإطلاقه ثم تنفيذ سلسلة بعد ذلك، لكننا هنا ننفذ كل مداوِل إلى أن نجد واحدًا لا يمكنه معالجة الطلب. انتبه إلى أن لا زلنا نشرح نمط سلسلة المسؤوليات حتى لو كان سياق الشرح مختلفًا قليلًا.
البرمجيات الوسيطة (Middleware)
middleware/Middleware.java: واجهة تحقق أساسية
package refactoring_guru.chain_of_responsibility.example.middleware;
/**
* الأساسية middleware فئة.
*/
public abstract class Middleware {
private Middleware next;
/**
* middleware تبني سلاسل من كائنات.
*/
public Middleware linkWith(Middleware next) {
this.next = next;
return next;
}
/**
* ستستخدم الفئات الفرعية هذا الأسلوب مع عمليات
* (Concrete Checks) التحقق الحقيقية.
*/
public abstract boolean check(String email, String password);
/**
* تتحقق من الكائن التالي في السلسلة أو تنهي المضي قدمًا في السلسلة
* إن كنا مع الكائن الأخير في السلسلة.
*/
protected boolean checkNext(String email, String password) {
if (next == null) {
return true;
}
return next.check(email, password);
}
}
middleware/ThrottlingMiddleware.java: التحقق من حد كمية الطلبات
package refactoring_guru.chain_of_responsibility.example.middleware;
/**
* ConcreteHandler. تحقق إن كانت هناك الكثير من طلبات تسجيل الدخول الفاشلة.
*/
public class ThrottlingMiddleware extends Middleware {
private int requestPerMinute;
private int request;
private long currentTime;
public ThrottlingMiddleware(int requestPerMinute) {
this.requestPerMinute = requestPerMinute;
this.currentTime = System.currentTimeMillis();
}
/**
* يمكن إدخاله في هذا الأسلوب ()checkNext رجاءً، لاحظ أن استدعاء.
*
* هذا يعطي مرونة أكبر من مجرد حلقة تكرارية بسيطة على
* فمثلًا يستطيع عنصر من سلسلة أن يغير ترتيب ،middleware كل كائنات
* عمليات التحقق من خلال إجراء تحققه بعد كل التحققات الأخرى.
*/
public boolean check(String email, String password) {
if (System.currentTimeMillis() > currentTime + 60_000) {
request = 0;
currentTime = System.currentTimeMillis();
}
request++;
if (request > requestPerMinute) {
System.out.println("Request limit exceeded!");
Thread.currentThread().stop();
}
return checkNext(email, password);
}
}
middleware/UserExistsMiddleware.java: التحقق من اعتماديات المستخدم
package refactoring_guru.chain_of_responsibility.example.middleware;
import refactoring_guru.chain_of_responsibility.example.server.Server;
/**
* ConcreteHandler. تحقق من وجود مستخدم بالاعتماديات المعطاة.
*/
public class UserExistsMiddleware extends Middleware {
private Server server;
public UserExistsMiddleware(Server server) {
this.server = server;
}
public boolean check(String email, String password) {
if (!server.hasEmail(email)) {
System.out.println("This email is not registered!");
return false;
}
if (!server.isValidPassword(email, password)) {
System.out.println("Wrong password!");
return false;
}
return checkNext(email, password);
}
}
middleware/RoleCheckMiddleware.java: التحقق من دور المستخدم
package refactoring_guru.chain_of_responsibility.example.middleware;
/**
* ConcreteHandler. تحقق من دور المستخدم.
*/
public class RoleCheckMiddleware extends Middleware {
public boolean check(String email, String password) {
if (email.equals("admin@example.com")) {
System.out.println("Hello, admin!");
return true;
}
System.out.println("Hello, user!");
return checkNext(email, password);
}
}
الخادم
server/Server.java: هدف التصريح (Authorization Target)
package refactoring_guru.chain_of_responsibility.example.server;
import refactoring_guru.chain_of_responsibility.example.middleware.Middleware;
import java.util.HashMap;
import java.util.Map;
/**
* فئة الخادم.
*/
public class Server {
private Map<String, String> users = new HashMap<>();
private Middleware middleware;
/**
* يمرر العميل سلسلة من الكائنات إلى الخادم، يزيد هذا من المرونة ويسهِّل من اختبار الخادم.
*/
public void setMiddleware(Middleware middleware) {
this.middleware = middleware;
}
/**
* يحصل الخادم على البريد وكلمة المرور من العميل ويرسل طلب التصريح إلى السلسلة.
*/
public boolean logIn(String email, String password) {
if (middleware.check(email, password)) {
System.out.println("Authorization have been successful!");
// اصنع شيئًا مفيدًا هنا للمستخدمين المصرح لهم.
return true;
}
return false;
}
public void register(String email, String password) {
users.put(email, password);
}
public boolean hasEmail(String email) {
return users.containsKey(email);
}
public boolean isValidPassword(String email, String password) {
return users.get(email).equals(password);
}
}
Demo.java: شيفرة العميل
package refactoring_guru.chain_of_responsibility.example;
import refactoring_guru.chain_of_responsibility.example.middleware.Middleware;
import refactoring_guru.chain_of_responsibility.example.middleware.RoleCheckMiddleware;
import refactoring_guru.chain_of_responsibility.example.middleware.ThrottlingMiddleware;
import refactoring_guru.chain_of_responsibility.example.middleware.UserExistsMiddleware;
import refactoring_guru.chain_of_responsibility.example.server.Server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* يجتمع كل شيء هنا ، Demo فئة.
*/
public class Demo {
private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
private static Server server;
private static void init() {
server = new Server();
server.register("admin@example.com", "admin_pass");
server.register("user@example.com", "user_pass");
// جميع عمليات التحقق مربوطة ببعضها، يستطيع العميل أن يبني سلاسل متعددة
// باستخدام نفس المكونات.
Middleware middleware = new ThrottlingMiddleware(2);
middleware.linkWith(new UserExistsMiddleware(server))
.linkWith(new RoleCheckMiddleware());
// يحصل الخادم على سلسلة من شيفرة العميل.
server.setMiddleware(middleware);
}
public static void main(String[] args) throws IOException {
init();
boolean success;
do {
System.out.print("Enter email: ");
String email = reader.readLine();
System.out.print("Input password: ");
String password = reader.readLine();
success = server.logIn(email, password);
} while (!success);
}
}
OutputDemo.txt: نتائج التنفيذ
Enter email: admin@example.com
Input password: admin_pass
Hello, admin!
Authorization have been successful!
Enter email: user@example.com
Input password: user_pass
Hello, user!
Authorization have been successful!
الاستخدام في لغة #C
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات.
يمكن ملاحظة النمط من خلال الأساليب السلوكية لكائن يستدعي -بشكل غير مباشر - نفس الأساليب في الكائنات الأخرى، بينما تتبع كل الكائنات نفس الواجهة المشتركة.
مثال تصوري
يوضح هذا المثال بنية نمط سلسلة المسؤوليات، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
Program.cs: مثال تصوري
using System;
using System.Collections.Generic;
namespace RefactoringGuru.DesignPatterns.ChainOfResponsibility.Conceptual
{
// تصرح واجهة المداول عن أسلوب لبناء سلسلة من المداوِلات، كما تصرح عن
// أسلوب لتنفيذ الطلب.
public interface IHandler
{
IHandler SetNext(IHandler handler);
object Handle(object request);
}
// يمكن تطبيق السلوك الافتراضي للتسلسل داخل فئة مداوِل أساسية.
abstract class AbstractHandler : IHandler
{
private IHandler _nextHandler;
public IHandler SetNext(IHandler handler)
{
this._nextHandler = handler;
// إعادة مداوِل من هنا ستسمح لنا بربط المداوِلات بشكل مناسب كما يلي:
// monkey.SetNext(squirrel).SetNext(dog);
return handler;
}
public virtual object Handle(object request)
{
if (this._nextHandler != null)
{
return this._nextHandler.Handle(request);
}
else
{
return null;
}
}
}
class MonkeyHandler : AbstractHandler
{
public override object Handle(object request)
{
if ((request as string) == "Banana")
{
return $"Monkey: I'll eat the {request.ToString()}.\n";
}
else
{
return base.Handle(request);
}
}
}
class SquirrelHandler : AbstractHandler
{
public override object Handle(object request)
{
if (request.ToString() == "Nut")
{
return $"Squirrel: I'll eat the {request.ToString()}.\n";
}
else
{
return base.Handle(request);
}
}
}
class DogHandler : AbstractHandler
{
public override object Handle(object request)
{
if (request.ToString() == "MeatBall")
{
return $"Dog: I'll eat the {request.ToString()}.\n";
}
else
{
return base.Handle(request);
}
}
}
class Client
{
// تُهيَّأ شيفرة العميل في الغالب لتعمل مع مداوِل واحد، ولا تكون مدركة أصلًا في أغلب الحالات
// أن المداوِل جزء من السلسلة.
public static void ClientCode(AbstractHandler handler)
{
foreach (var food in new List<string> { "Nut", "Banana", "Cup of coffee" })
{
Console.WriteLine($"Client: Who wants a {food}?");
var result = handler.Handle(food);
if (result != null)
{
Console.Write($" {result}");
}
else
{
Console.WriteLine($" {food} was left untouched.");
}
}
}
}
class Program
{
static void Main(string[] args)
{
// ينشئ الجزءُ الآخر من شيفرة العميلِ السلسلةَ الحقيقية.
var monkey = new MonkeyHandler();
var squirrel = new SquirrelHandler();
var dog = new DogHandler();
monkey.SetNext(squirrel).SetNext(dog);
// يجب أن يكون العميل قادرًا على إرسال طلب إلى أي مداوِل، وليس إلى أول
// واحد في السلسلة فقط.
Console.WriteLine("Chain: Monkey > Squirrel > Dog\n");
Client.ClientCode(monkey);
Console.WriteLine();
Console.WriteLine("Subchain: Squirrel > Dog\n");
Client.ClientCode(squirrel);
}
}
}
Output.txt: نتائج التنفيذ
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
الاستخدام في لغة PHP
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات. ولعل أحد أشهر الأمثلة لاستخدام هذا النمط في PHP هو البرمجية الوسيطة لطلب HTTP أو ما يُعرف باسم ( HTTP Request Middleware) الموصوف في معيار PHP القياسي الخامس عشر (PSR-15).
مثال تصوري
يوضح هذا المثال بنية نمط سلسلة المسؤوليات، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP.
index.php: مثال تصوري
<?php
namespace RefactoringGuru\ChainOfResponsibility\Conceptual;
/**
* تصرح واجهة المداول عن أسلوب لبناء سلسلة من المداوِلات، كما تصرح عن
* أسلوب لتنفيذ الطلب.
*/
interface Handler
{
public function setNext(Handler $handler): Handler;
public function handle(string $request): ?string;
}
/**
* يمكن تطبيق السلوك الافتراضي للتسلسل داخل فئة مداوِل أساسية.
*/
abstract class AbstractHandler implements Handler
{
/**
* @var Handler
*/
private $nextHandler;
public function setNext(Handler $handler): Handler
{
$this->nextHandler = $handler;
// إعادة مداوِل من هنا ستسمح لنا بربط المداوِلات بشكل مناسب كما يلي:
// $monkey->setNext($squirrel)->setNext($dog);
return $handler;
}
public function handle(string $request): ?string
{
if ($this->nextHandler) {
return $this->nextHandler->handle($request);
}
return null;
}
}
/**
* جميع المداوِلات الحقيقية تعالج الطلب أو تمرره إلى المداوِل التالي في السلسلة.
*/
class MonkeyHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "Banana") {
return "Monkey: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
class SquirrelHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "Nut") {
return "Squirrel: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
class DogHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "MeatBall") {
return "Dog: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
/**
* تُهيَّأ شيفرة العميل في الغالب لتعمل مع مداوِل واحد، ولا تكون مدركة أصلًا في أغلب الحالات
* أن المداوِل جزء من السلسلة.
*/
function clientCode(Handler $handler)
{
foreach (["Nut", "Banana", "Cup of coffee"] as $food) {
echo "Client: Who wants a " . $food . "?\n";
$result = $handler->handle($food);
if ($result) {
echo " " . $result;
} else {
echo " " . $food . " was left untouched.\n";
}
}
}
/**
* ينشئ الجزءُ الآخر من شيفرة العميلِ السلسلةَ الحقيقية.
*/
$monkey = new MonkeyHandler;
$squirrel = new SquirrelHandler;
$dog = new DogHandler;
$monkey->setNext($squirrel)->setNext($dog);
/**
* يجب أن يكون العميل قادرًا على إرسال طلب إلى أي مداوِل، وليس إلى أول واحد في السلسلة فقط.
*/
echo "Chain: Monkey > Squirrel > Dog\n\n";
clientCode($monkey);
echo "\n";
echo "Subchain: Squirrel > Dog\n\n";
clientCode($squirrel);
Output.txt: نتائج التنفيذ
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
مثال واقعي
كما ذكرنا قبل قليل أن أكثر مثال مشهور لاستخدام نمط سلسلة المسؤوليات في PHP هو البرمجية الوسيطة لطلب HTTP، وهذه البرمجية الوسيطة تستخدمها أشهر أطُر العمل في لغة PHP، بل صارت معيارًا في PSR-15.
وفيها يجب أن يمر طلب HTTP خلال صف من كائنات البرمجية الوسيطة كي يُعالج من قِبل التطبيق، وكل برمجية وسيطة تمرره إلى البرمجية التي تليها أو ترفض معالجته، ويعالَج الطلبُ من قِبل المداوِل الرئيسي (Primary handler) للتطبيق عند مروره بنجاح خلال كل البرمجيات الوسيطة.
ولعلك لاحظت أن هذا المنظور عكسي نوعًا ما عن الهدف الأصلي من النمط، وذلك صحيح تمامًا، ففي الاستخدام المعتاد للنمط لا يكمل الطلبُ المرور في السلسلة إن كان المداوِل الحالي لا يستطيع معالجته، بينما تمرِر البرمجية الوسيطةُ الطلبَ إن ظنت أن التطبيق يستطيع معالجة الطلب. ومع هذا فبما أن البرمجيات الوسيطة مرتبطة في سلسلة فإن المبدأ العام لا يزال مثالًا على نمط سلسلة المسؤوليات.
index.php: مثال واقعي
<?php
namespace RefactoringGuru\ChainOfResponsibility\RealWorld;
/**
* يصرح نمط سلسلة المسؤوليات التقليدي وظيفة واحدة للكائنات التي تكوّن
* بينما في حالتنا فسنفرق بين البرمجية (Handler) السلسلة، وهي وظيفة المداوِل
* الوسيطة والمداوِل النهائي للبرنامج، والذي نُفِّذ عند مرور الطلب خلال كل كائنات
* البرمجية الوسيطة.
*
* تصرح قاعدة البرمجية الوسيطة الأساسية عن واجهة لربط كائنات البرمجيات
* الوسيطة في سلسلة.
*/
abstract class Middleware
{
/**
* @var Middleware
*/
private $next;
/**
* يمكن استخدام هذا الأسلوب لبناء سلسلة من كائنات البرمجيات الوسيطة.
*/
public function linkWith(Middleware $next): Middleware
{
$this->next = $next;
return $next;
}
/**
* يجب أن تتخطى الفئات الفرعية هذا الأسلوب لتجري اختباراتها الخاصة، ويجوز
* (Parent Implementation) أن ترجع الفئة الفرعية إلى الاستخدام الأم
* إن لم تستطع معالجة الطلب.
*/
public function check(string $email, string $password): bool
{
if (!$this->next) {
return true;
}
return $this->next->check($email, $password);
}
}
/**
* تتحقق هذه البرمجية الوسيطة الحقيقية من وجود مستخدم بالاعتماديات المُعطاة.
*/
class UserExistsMiddleware extends Middleware
{
private $server;
public function __construct(Server $server)
{
$this->server = $server;
}
public function check(string $email, string $password): bool
{
if (!$this->server->hasEmail($email)) {
echo "UserExistsMiddleware: This email is not registered!\n";
return false;
}
if (!$this->server->isValidPassword($email, $password)) {
echo "UserExistsMiddleware: Wrong password!\n";
return false;
}
return parent::check($email, $password);
}
}
/**
* تتحقق هذه البرمجية الوسيطة الحقيقية إن كان المستخدم المرتبط بالطلب لديه.
* الصلاحيات الكافية
*/
class RoleCheckMiddleware extends Middleware
{
public function check(string $email, string $password): bool
{
if ($email === "admin@example.com") {
echo "RoleCheckMiddleware: Hello, admin!\n";
return true;
}
echo "RoleCheckMiddleware: Hello, user!\n";
return parent::check($email, $password);
}
}
/**
* تتحقق هذه البرمجية الوسيطة الحقيقية من وجود محاولات تسجيل دخول كثيرة فاشلة.
*/
class ThrottlingMiddleware extends Middleware
{
private $requestPerMinute;
private $request;
private $currentTime;
public function __construct(int $requestPerMinute)
{
$this->requestPerMinute = $requestPerMinute;
$this->currentTime = time();
}
/**
* يمكن إدخاله في بداية هذا الأسلوب أو نهايته parent::check لاحظ أن استدعاء.
*
* هذا يعطيك مرونة أكثر من مجرد تكرار كل كائنات البرمجيات الوسيطة، فمثلًا تستطيع
* البرمجية الوسيطة أن تغير ترتيب التحققات من خلال تنفيذ تحققها في النهاية.
*/
public function check(string $email, string $password): bool
{
if (time() > $this->currentTime + 60) {
$this->request = 0;
$this->currentTime = time();
}
$this->request++;
if ($this->request > $this->requestPerMinute) {
echo "ThrottlingMiddleware: Request limit exceeded!\n";
die();
}
return parent::check($email, $password);
}
}
/**
* هذه فئة تطبيق تتصرف كمداوِل حقيقي، وتستخدم فئة الخادم نمط سلسلة المسؤوليات لتنفيذ
* المختلفة قبل إطلاق بعض (Authentication) مجموعة من برمجيات التصديق
* المرتبط بالطلب (Business Logic) المنطق التجاري.
*/
class Server
{
private $users = [];
/**
* @var Middleware
*/
private $middleware;
/**
* يستطيع العميل تهيئة الخادم بواسطة سلسلة من كائنات البرمجيات الوسيطة.
*/
public function setMiddleware(Middleware $middleware): void
{
$this->middleware = $middleware;
}
/**
* يحصل الخادم على عنوان البريد وكلمة المرور من العميل ويرسل طلب التصريح
* إلى البرمجية الوسيطة.
*/
public function logIn(string $email, string $password): bool
{
if ($this->middleware->check($email, $password)) {
echo "Server: Authorization has been successful!\n";
// اصنع شيئًا مفيدًا للمستخدمين المصرح لهم.
return true;
}
return false;
}
public function register(string $email, string $password): void
{
$this->users[$email] = $password;
}
public function hasEmail(string $email): bool
{
return isset($this->users[$email]);
}
public function isValidPassword(string $email, string $password): bool
{
return $this->users[$email] === $password;
}
}
/**
* شيفرة العميل.
*/
$server = new Server();
$server->register("admin@example.com", "admin_pass");
$server->register("user@example.com", "user_pass");
// جميع البرمجيات الوسيطة مربوطة في سلسلة، ويستطيع العميل بناء تشكيلات مختلفة من
// السلاسل وفقًا لاحتياجاته.
$middleware = new ThrottlingMiddleware(2);
$middleware
->linkWith(new UserExistsMiddleware($server))
->linkWith(new RoleCheckMiddleware);
// يحصل الخادم على سلسلة من شيفرة العميل.
$server->setMiddleware($middleware);
// ...
do {
echo "\nEnter your email:\n";
$email = readline();
echo "Enter your password:\n";
$password = readline();
$success = $server->logIn($email, $password);
} while (!$success);
Output.txt: نتائج التنفيذ
Enter your email:
asd
Enter your password:
123
UserExistsMiddleware: This email is not registered!
Enter your email:
admin@example.com
Enter your password:
wrong
UserExistsMiddleware: Wrong password!
Enter your email:
admin@example.com
Enter your password:
letmein
ThrottlingMiddleware: Request limit exceeded!
Enter your email:
admin@example.com
Enter your password:
admin_pass
RoleCheckMiddleware: Hello, admin!
Server: Authorization has been successful!
الاستخدام في لغة بايثون
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات.
يمكن ملاحظة النمط من خلال الأساليب السلوكية لكائن يستدعي -بشكل غير مباشر - نفس الأساليب في الكائنات الأخرى، بينما تتبع كل الكائنات نفس الواجهة المشتركة.
مثال تصوري
يوضح هذا المثال بنية نمط سلسلة المسؤوليات، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
main.py: مثال تصوري
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Optional
class Handler(ABC):
"""
تصرح واجهة المداول عن أسلوب لبناء سلسلة من المداوِلات، كما تصرح عن
أسلوب لتنفيذ الطلب.
"""
@abstractmethod
def set_next(self, handler: Handler) -> Handler:
pass
@abstractmethod
def handle(self, request) -> Optional[str]:
pass
class AbstractHandler(Handler):
"""
داخل فئة مداوِل أساسية (Chaining) يمكن تطبيق السلوك الافتراضي للربط.
"""
_next_handler: Handler = None
def set_next(self, handler: Handler) -> Handler:
self._next_handler = handler
# إعادة مداوِل من هنا ستسمح لنا بربط المداوِلات بشكل مناسب كما يلي:
# monkey.set_next(squirrel).set_next(dog)
return handler
@abstractmethod
def handle(self, request: Any) -> str:
if self._next_handler:
return self._next_handler.handle(request)
return None
"""
جميع المداوِلات الحقيقية تعالج الطلب أو تمرره إلى المداوِل التالي في السلسلة.
"""
class MonkeyHandler(AbstractHandler):
def handle(self, request: Any) -> str:
if request == "Banana":
return f"Monkey: I'll eat the {request}"
else:
return super().handle(request)
class SquirrelHandler(AbstractHandler):
def handle(self, request: Any) -> str:
if request == "Nut":
return f"Squirrel: I'll eat the {request}"
else:
return super().handle(request)
class DogHandler(AbstractHandler):
def handle(self, request: Any) -> str:
if request == "MeatBall":
return f"Dog: I'll eat the {request}"
else:
return super().handle(request)
def client_code(handler: Handler) -> None:
"""
تُهيَّأ شيفرة العميل في الغالب لتعمل مع مداوِل واحد، ولا تكون مدركة أصلًا في أغلب الحالات
أن المداوِل جزء من السلسلة.
"""
for food in ["Nut", "Banana", "Cup of coffee"]:
print(f"\nClient: Who wants a {food}?")
result = handler.handle(food)
if result:
print(f" {result}", end="")
else:
print(f" {food} was left untouched.", end="")
if __name__ == "__main__":
monkey = MonkeyHandler()
squirrel = SquirrelHandler()
dog = DogHandler()
monkey.set_next(squirrel).set_next(dog)
# يجب أن يكون العميل قادرًا على إرسال طلب إلى أي مداوِل، وليس إلى أول واحد في السلسلة فقط.
print("Chain: Monkey > Squirrel > Dog")
client_code(monkey)
print("\n")
print("Subchain: Squirrel > Dog")
client_code(squirrel)
Output.txt: نتائج التنفيذ
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut
Client: Who wants a Banana?
Monkey: I'll eat the Banana
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
الاستخدام في لغة Ruby
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات.
يمكن ملاحظة النمط من خلال الأساليب السلوكية لكائن يستدعي -بشكل غير مباشر - نفس الأساليب في الكائنات الأخرى، بينما تتبع كل الكائنات نفس الواجهة المشتركة.
مثال تصوري
يوضح هذا المثال بنية نمط سلسلة المسؤوليات، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
main.rb: مثال تصوري
# تصرح واجهة المداول عن أسلوب لبناء سلسلة من المداوِلات، كما تصرح عن
# أسلوب لتنفيذ الطلب.
class Handler
# @abstract
#
# @param [Handler] handler
def next_handler=(handler)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# @abstract
#
# @param [String] request
#
# @return [String, nil]
def handle(request)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# داخل فئة مداوِل أساسية (Chaining) يمكن تطبيق السلوك الافتراضي للربط.
class AbstractHandler < Handler
# @return [Handler]
attr_writer :next_handler
# @param [Handler] handler
#
# @return [Handler]
def next_handler(handler)
@next_handler = handler
# إعادة مداوِل من هنا ستسمح لنا بربط المداوِلات بشكل مناسب كما يلي:
# monkey.next_handler(squirrel).next_handler(dog)
handler
end
# @abstract
#
# @param [String] request
#
# @return [String, nil]
def handle(request)
return @next_handler.handle(request) if @next_handler
nil
end
end
# جميع المداوِلات الحقيقية تعالج الطلب أو تمرره إلى المداوِل التالي في السلسلة.
class MonkeyHandler < AbstractHandler
# @param [String] request
#
# @return [String, nil]
def handle(request)
if request == 'Banana'
"Monkey: I'll eat the #{request}"
else
super(request)
end
end
end
class SquirrelHandler < AbstractHandler
# @param [String] request
#
# @return [String, nil]
def handle(request)
if request == 'Nut'
"Squirrel: I'll eat the #{request}"
else
super(request)
end
end
end
class DogHandler < AbstractHandler
# @param [String] request
#
# @return [String, nil]
def handle(request)
if request == 'MeatBall'
"Dog: I'll eat the #{request}"
else
super(request)
end
end
end
# تُهيَّأ شيفرة العميل في الغالب لتعمل مع مداوِل واحد، ولا تكون مدركة أصلًا في أغلب الحالات
# أن المداوِل جزء من السلسلة.
def client_code(handler)
['Nut', 'Banana', 'Cup of coffee'].each do |food|
puts "\nClient: Who wants a #{food}?"
result = handler.handle(food)
if result
print " #{result}"
else
print " #{food} was left untouched."
end
end
end
monkey = MonkeyHandler.new
squirrel = SquirrelHandler.new
dog = DogHandler.new
monkey.next_handler(squirrel).next_handler(dog)
# يجب أن يكون العميل قادرًا على إرسال طلب إلى أي مداوِل، وليس إلى أول واحد في السلسلة فقط.
puts 'Chain: Monkey > Squirrel > Dog'
client_code(monkey)
puts "\n\n"
puts 'Subchain: Squirrel > Dog'
client_code(squirrel)
output.txt: نتائج التنفيذ
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut
Client: Who wants a Banana?
Monkey: I'll eat the Banana
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
الاستخدام في لغة Swift
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات.
يمكن ملاحظة النمط من خلال الأساليب السلوكية لكائن يستدعي -بشكل غير مباشر - نفس الأساليب في الكائنات الأخرى، بينما تتبع كل الكائنات نفس الواجهة المشتركة.
مثال تصوري
يوضح هذا المثال بنية نمط سلسلة المسؤوليات، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
Example.swift: مثال تصوري
import XCTest
/// تصرح واجهة المداول عن أسلوب لبناء سلسلة من المداوِلات، كما تصرح عن
/// أسلوب لتنفيذ الطلب.
protocol Handler: class {
@discardableResult
func setNext(handler: Handler) -> Handler
func handle(request: String) -> String?
var nextHandler: Handler? { get set }
}
extension Handler {
func setNext(handler: Handler) -> Handler {
self.nextHandler = handler
/// إعادة مداوِل من هنا ستسمح لنا بربط المداوِلات بشكل مناسب كما يلي:
/// monkey.setNext(handler: squirrel).setNext(handler: dog)
return handler
}
func handle(request: String) -> String? {
return nextHandler?.handle(request: request)
}
}
/// جميع المداوِلات الحقيقية تعالج الطلب أو تمرره إلى المداوِل التالي في السلسلة.
class MonkeyHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
if (request == "Banana") {
return "Monkey: I'll eat the " + request + ".\n"
} else {
return nextHandler?.handle(request: request)
}
}
}
class SquirrelHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
if (request == "Nut") {
return "Squirrel: I'll eat the " + request + ".\n"
} else {
return nextHandler?.handle(request: request)
}
}
}
class DogHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
if (request == "MeatBall") {
return "Dog: I'll eat the " + request + ".\n"
} else {
return nextHandler?.handle(request: request)
}
}
}
/// تُهيَّأ شيفرة العميل في الغالب لتعمل مع مداوِل واحد، ولا تكون مدركة أصلًا في أغلب الحالات
/// أن المداوِل جزء من السلسلة.
class Client {
// ...
static func someClientCode(handler: Handler) {
let food = ["Nut", "Banana", "Cup of coffee"]
for item in food {
print("Client: Who wants a " + item + "?\n")
guard let result = handler.handle(request: item) else {
print(" " + item + " was left untouched.\n")
return
}
print(" " + result)
}
}
// ...
}
/// لنرى كيف سيعمل كل هذا معًا.
class ChainOfResponsibilityConceptual: XCTestCase {
func test() {
/// ينشئ الجزءُ الآخر من شيفرة العميلِ السلسلةَ الحقيقية.
let monkey = MonkeyHandler()
let squirrel = SquirrelHandler()
let dog = DogHandler()
monkey.setNext(handler: squirrel).setNext(handler: dog)
/// يجب أن يكون العميل قادرًا على إرسال طلب إلى أي مداوِل، وليس إلى أول واحد في السلسلة فقط.
print("Chain: Monkey > Squirrel > Dog\n\n")
Client.someClientCode(handler: monkey)
print()
print("Subchain: Squirrel > Dog\n\n")
Client.someClientCode(handler: squirrel)
}
}
Output.txt: نتائج التنفيذ
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
مثال واقعي
Example.swift: شيفرة العميل
import Foundation
import UIKit
import XCTest
protocol Handler {
var next: Handler? { get }
func handle(_ request: Request) -> LocalizedError?
}
class BaseHandler: Handler {
var next: Handler?
init(with handler: Handler? = nil) {
self.next = handler
}
func handle(_ request: Request) -> LocalizedError? {
return next?.handle(request)
}
}
class LoginHandler: BaseHandler {
override func handle(_ request: Request) -> LocalizedError? {
guard request.email?.isEmpty == false else {
return AuthError.emptyEmail
}
guard request.password?.isEmpty == false else {
return AuthError.emptyPassword
}
return next?.handle(request)
}
}
class SignUpHandler: BaseHandler {
private struct Limit {
static let passwordLength = 8
}
override func handle(_ request: Request) -> LocalizedError? {
guard request.email?.contains("@") == true else {
return AuthError.invalidEmail
}
guard (request.password?.count ?? 0) >= Limit.passwordLength else {
return AuthError.invalidPassword
}
guard request.password == request.repeatedPassword else {
return AuthError.differentPasswords
}
return next?.handle(request)
}
}
class LocationHandler: BaseHandler {
override func handle(_ request: Request) -> LocalizedError? {
guard isLocationEnabled() else {
return AuthError.locationDisabled
}
return next?.handle(request)
}
func isLocationEnabled() -> Bool {
return true /// تستدعي أسلوبًا خاصًا
}
}
class NotificationHandler: BaseHandler {
override func handle(_ request: Request) -> LocalizedError? {
guard isNotificationsEnabled() else {
return AuthError.notificationsDisabled
}
return next?.handle(request)
}
func isNotificationsEnabled() -> Bool {
return false /// تستدعي أسلوبًا خاصًا
}
}
enum AuthError: LocalizedError {
case emptyFirstName
case emptyLastName
case emptyEmail
case emptyPassword
case invalidEmail
case invalidPassword
case differentPasswords
case locationDisabled
case notificationsDisabled
var errorDescription: String? {
switch self {
case .emptyFirstName:
return "First name is empty"
case .emptyLastName:
return "Last name is empty"
case .emptyEmail:
return "Email is empty"
case .emptyPassword:
return "Password is empty"
case .invalidEmail:
return "Email is invalid"
case .invalidPassword:
return "Password is invalid"
case .differentPasswords:
return "Password and repeated password should be equal"
case .locationDisabled:
return "Please turn location services on"
case .notificationsDisabled:
return "Please turn notifications on"
}
}
}
protocol Request {
var firstName: String? { get }
var lastName: String? { get }
var email: String? { get }
var password: String? { get }
var repeatedPassword: String? { get }
}
extension Request {
/// الاستخدامات الافتراضية.
var firstName: String? { return nil }
var lastName: String? { return nil }
var email: String? { return nil }
var password: String? { return nil }
var repeatedPassword: String? { return nil }
}
struct SignUpRequest: Request {
var firstName: String?
var lastName: String?
var email: String?
var password: String?
var repeatedPassword: String?
}
struct LoginRequest: Request {
var email: String?
var password: String?
}
protocol AuthHandlerSupportable: AnyObject {
var handler: Handler? { get set }
}
class BaseAuthViewController: UIViewController, AuthHandlerSupportable {
/// لتطبيق السلوك الأساسي (Extensions) يمكن استخدام الفئة الأساسية أو إحدى إضافاتها.
var handler: Handler?
init(handler: Handler) {
self.handler = handler
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
class LoginViewController: BaseAuthViewController {
func loginButtonSelected() {
print("Login View Controller: User selected Login button")
let request = LoginRequest(email: "smth@gmail.com", password: "123HardPass")
if let error = handler?.handle(request) {
print("Login View Controller: something went wrong")
print("Login View Controller: Error -> " + (error.errorDescription ?? ""))
} else {
print("Login View Controller: Preconditions are successfully validated")
}
}
}
class SignUpViewController: BaseAuthViewController {
func signUpButtonSelected() {
print("SignUp View Controller: User selected SignUp button")
let request = SignUpRequest(firstName: "Vasya",
lastName: "Pupkin",
email: "vasya.pupkin@gmail.com",
password: "123HardPass",
repeatedPassword: "123HardPass")
if let error = handler?.handle(request) {
print("SignUp View Controller: something went wrong")
print("SignUp View Controller: Error -> " + (error.errorDescription ?? ""))
} else {
print("SignUp View Controller: Preconditions are successfully validated")
}
}
}
class ChainOfResponsibilityRealWorld: XCTestCase {
func testChainOfResponsibilityRealWorld() {
print("Client: Let's test Login flow!")
let loginHandler = LoginHandler(with: LocationHandler())
let loginController = LoginViewController(handler: loginHandler)
loginController.loginButtonSelected()
print("\nClient: Let's test SignUp flow!")
let signUpHandler = SignUpHandler(with: LocationHandler(with: NotificationHandler()))
let signUpController = SignUpViewController(handler: signUpHandler)
signUpController.signUpButtonSelected()
}
}
Output.txt: نتائج التنفيذ
Client: Let's test Login flow!
Login View Controller: User selected Login button
Login View Controller: Preconditions are successfully validated
Client: Let's test SignUp flow!
SignUp View Controller: User selected SignUp button
SignUp View Controller: something went wrong
SignUp View Controller: Error -> Please turn notifications on
الاستخدام في لغة TypeScript
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات.
يمكن ملاحظة النمط من خلال الأساليب السلوكية لكائن يستدعي -بشكل غير مباشر - نفس الأساليب في الكائنات الأخرى، بينما تتبع كل الكائنات نفس الواجهة المشتركة.
مثال تصوري
يوضح هذا المثال بنية نمط سلسلة المسؤوليات، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
index.ts: مثال تصوري
/**
* تصرح واجهة المداول عن أسلوب لبناء سلسلة من المداوِلات، كما تصرح عن
* أسلوب لتنفيذ الطلب.
*/
interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): string;
}
/**
* داخل فئة مداوِل أساسية (Chaining) يمكن تطبيق السلوك الافتراضي للربط.
*/
abstract class AbstractHandler implements Handler
{
private nextHandler: Handler;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
// إعادة مداوِل من هنا ستسمح لنا بربط المداوِلات بشكل مناسب كما يلي:
// monkey.setNext(squirrel).setNext(dog);
return handler;
}
public handle(request: string): string {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
/**
* جميع المداوِلات الحقيقية تعالج الطلب أو تمرره إلى المداوِل التالي في السلسلة.
*/
class MonkeyHandler extends AbstractHandler {
public handle(request: string): string {
if (request === 'Banana') {
return `Monkey: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
class SquirrelHandler extends AbstractHandler {
public handle(request: string): string {
if (request === 'Nut') {
return `Squirrel: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
class DogHandler extends AbstractHandler {
public handle(request: string): string {
if (request === 'MeatBall') {
return `Dog: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
/**
* تُهيَّأ شيفرة العميل في الغالب لتعمل مع مداوِل واحد، ولا تكون مدركة أصلًا في أغلب
* الحالات أن المداوِل جزء من السلسلة.
*/
function clientCode(handler: Handler) {
const foods = ['Nut', 'Banana', 'Cup of coffee'];
for (const food of foods) {
console.log(`Client: Who wants a ${food}?`);
const result = handler.handle(food);
if (result) {
console.log(` ${result}`);
} else {
console.log(` ${food} was left untouched.`);
}
}
}
/**
* ينشئ الجزءُ الآخر من شيفرة العميلِ السلسلةَ الحقيقية.
*/
const monkey = new MonkeyHandler();
const squirrel = new SquirrelHandler();
const dog = new DogHandler();
monkey.setNext(squirrel).setNext(dog);
/**
* يجب أن يكون العميل قادرًا على إرسال طلب إلى أي مداوِل، وليس إلى أول واحد
* في السلسلة فقط.
*/
console.log('Chain: Monkey > Squirrel > Dog\n');
clientCode(monkey);
console.log('');
console.log('Subchain: Squirrel > Dog\n');
clientCode(squirrel);
Output.txt: نتائج التنفيذ
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.