الفرق بين المراجعتين لصفحة: «Design Patterns/chain of responsibility»
أسامه-دمراني (نقاش | مساهمات) 2.4 محتوى |
أسامه-دمراني (نقاش | مساهمات) 2.5 محتوى |
||
سطر 462: | سطر 462: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==الاستخدام في لغة #C== | |||
'''المستوى:''' ★ ★ ☆ | |||
'''الانتشار:''' ★ ☆ ☆ | |||
'''أمثلة الاستخدام:''' نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات. | |||
يمكن ملاحظة النمط من خلال الأساليب السلوكية لكائن يستدعي -بشكل غير مباشر - نفس الأساليب في الكائنات الأخرى، بينما تتبع كل الكائنات نفس الواجهة المشتركة. | |||
===مثال تصوري=== | |||
يوضح هذا المثال بنية نمط '''سلسلة المسؤوليات'''، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
====Program.cs: مثال تصوري ==== | |||
<syntaxhighlight lang="c#"> | |||
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); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight> | |||
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. | |||
</syntaxhighlight> | |||
== الاستخدام في لغة PHP == | |||
'''المستوى:''' ★ ★ ☆ | |||
'''الانتشار:''' ★ ☆ ☆ | |||
'''أمثلة الاستخدام:''' نمط سلسلة المسؤوليات غير شائع في برامج جافا بما أنه يتطلب أن تعمل الشيفرة مع سلسلة من الكائنات. ولعل أحد أشهر الأمثلة لاستخدام هذا النمط في PHP هو البرمجية الوسيطة لطلب HTTP أو ما يُعرف باسم ( [https://www.php-fig.org/psr/psr-15/ HTTP Request Middleware]) الموصوف في معيار PHP القياسي الخامس عشر (PSR-15). | |||
===مثال تصوري=== | |||
يوضح هذا المثال بنية نمط '''سلسلة المسؤوليات'''، ويركز على إجابة الأسئلة التالية: | |||
*ما الفئات التي يتكون منها؟ | |||
*ما الأدوار التي تلعبها هذه الفئات؟ | |||
*كيف ترتبط عناصر النمط ببعضها؟ | |||
سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP. | |||
==== index.php: مثال تصوري ==== | |||
<syntaxhighlight lang="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); | |||
</syntaxhighlight> | |||
==== Output.txt: نتائج التنفيذ ==== | |||
<syntaxhighlight> | |||
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. | |||
</syntaxhighlight> | |||
=== مثال واقعي === | |||
كما ذكرنا قبل قليل أن أكثر مثال مشهور لاستخدام نمط سلسلة المسؤوليات في PHP هو البرمجية الوسيطة لطلب HTTP، وهذه البرمجية تستخدمها أشهر أطُر العمل في لغة PHP، بل صارت معيارًا في PSR-15. | |||
و |
مراجعة 17:03، 7 مايو 2019
نمط سلسلة المسؤوليات (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.
و