ملخص
الغرض من هذه المقالة هو محاولة ، من وجهة نظر مختلفة ، لعرض وصف لأنظمة توزيع الأحداث.
في وقت كتابة هذا التقرير ، تقوم معظم أطر عمل php الرائدة بتطبيق نظام الأحداث بناءً على وصف EventObject .
أصبح هذا هو المعيار في php ، والذي تم تأكيده مؤخرًا من خلال اعتماد معيار psr / event-dispatcher .
لكن اتضح أن وصف كائن الحدث لا يساعد إلا قليلاً في تطوير المستمع. لمزيد من التفاصيل تحت القط.
ما هي المشكلة
دعونا نلقي نظرة على أدوار وأهداف أولئك الذين يستخدمون EventObject في التطوير.
مطور (أ) يضع القدرة على ضخ تعليمات الطرف الثالث في عمليته عن طريق إنشاء حدث.
يصف المطور EventObject أو توقيعه من خلال واجهة.
عند وصف EventObject ، فإن هدف المطور هو إعطاء المطورين الآخرين وصفًا لكائن البيانات ، وفي بعض حالات الاستخدام ، لوصف آلية التفاعل مع سلسلة الرسائل الرئيسية من خلال هذا الكائن.
المطور (B) الذي يصف "المستمع".
المطور يشترك في الحدث المحدد. في معظم الحالات ، يجب أن يفي وصف المستمع بالنوع الذي يمكن الاتصال به.
في نفس الوقت ، المطور ليس خجولًا في تسمية الفئات أو طرق المستمع. لكن هناك قيودًا أكثر وفقًا للقواعد التي يتلقى المعالج معالج EventObject كوسيطة لها.
عندما تم اعتماد psr / event-dispatcher بواسطة مجموعة العمل ، تم تحليل العديد من الخيارات لاستخدام أنظمة توزيع الأحداث.
يذكر معيار psr حالات الاستخدام التالية:
- إشعار أحادي الاتجاه - "لقد فعلت شيئًا إذا كنت مهتمًا"
- تحسين الكائن - "هنا شيء ، يرجى تغييره قبل أن أفعل شيئًا به"
- جمع - "أعطني كل ما تبذلونه من الأشياء حتى أتمكن من فعل شيء مع هذه القائمة"
- سلسلة بديلة - "هنا هو الشيء ، أول من يتعامل معها ، افعلها ، ثم توقف"
في الوقت نفسه ، أثارت مجموعة العمل العديد من الأسئلة حول الاستخدام المماثل لأنظمة توزيع الأحداث ، المتعلقة بحقيقة أن كل حالة من حالات الاستخدام لديها "حالة عدم يقين" والتي تعتمد على تنفيذ كائن المرسل .
في الأدوار الموضحة أعلاه للمطور (B) ، لا توجد طريقة ملائمة وسهلة القراءة لفهم أي من الخيارات لاستخدام نظام الحدث تم اختياره بواسطة المطور (A). سيتعين على المطور دائمًا أن ينظر إلى رمز الوصف ليس فقط لـ EventObject ، ولكن أيضًا في الكود حيث يتم إنشاء هذا الحدث.
نتيجة لذلك ، يعد التوقيع وصفًا لكائن الحدث ، والذي تم تصميمه لتسهيل عمل المطور (B). لا تؤدي هذه المهمة أداءً جيدًا.
هناك مشكلة أخرى تتمثل في عدم وجود ما يبرر دائمًا لكائن منفصل ، والذي يصف بالإضافة إلى ذلك الكيانات الموصوفة بالفعل في الأنظمة.
namespace MyApp\Customer\Events; use MyApp\Customer\CustomerNameInterface; use MyFramevork\Event\SomeEventInterface; class CustomerNameChangedEvent implements SomeEventInterface { public function getCustomerId(): int; public function getCustomerName(): CustomerNameInterface; }
في المثال أعلاه ، تم وصف كائن CustomerNameInterface بالفعل في النظام.
هذا يذكرنا بالإفراط في تقديم مفاهيم جديدة. بعد كل شيء ، عندما نحتاج إلى تطبيق طريقة ، على سبيل المثال ، الكتابة إلى سجل تغيير اسم العميل لا نجمع بين وسيطات الطريقة في كيان منفصل ، نستخدم الوصف القياسي لطريقة النموذج:
function writeToLogCustomerNameChange( int $customerId, CustomerNameInterface $customerName ) {
نتيجة لذلك ، نرى المشاكل التالية:
- توقيع رمز مستمع سيء
- عدم اليقين المرسل
- عودة عدم اليقين نوع
- مقدمة العديد من الكيانات الإضافية مثل SomeEventObject
دعونا ننظر في الأمر من منظور مختلف
إذا كانت إحدى المشكلات هي وصف ضعيف للمستمع ، فلنلقِ نظرة على نظام توزيع الأحداث ليس من وصف كائن الحدث ، ولكن من وصف المستمع.
يصف Developer (A) كيفية وصف المستمع.
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ); }
تمكن المطور الممتاز (A) من تقديم وصف للمستمع والبيانات المنقولة إلى مطوري الطرف الثالث.
عند كتابة مستمع ، يقوم المطور (B) بكتابة CustomerNameChangedListener في البيئة ، ويمكن لـ IDE إضافة وصف لطريقة الاستماع إلى صفه. اكتمال التعليمات البرمجية رائع.
دعنا نلقي نظرة على توقيع طريقة المستمع الجديد. حتى النظرة السريعة تكفي لفهم أن إصدار نظام توزيع الأحداث المستخدم هو: "إخطار أحادي الاتجاه".
لا يتم إرسال بيانات الإدخال بالرجوع إليها ، مما يعني أنه لا توجد وسيلة لتعديلها بأي شكل من الأشكال بحيث تدخل التغييرات في التيار الرئيسي. قيمة الإرجاع المفقودة ؛ لا توجد تعليقات من الخيط الرئيسي.
ماذا عن حالات الاستخدام الأخرى؟ دعنا نلعب مع وصف واجهة المستمع الحدث.
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): CustomerNameInterface; }
هناك حاجة لقيمة الإرجاع ، مما يعني أن المستمع يمكنه (ولكن ليس مطلوبًا) إرجاع قيمة مختلفة إذا كان يطابق الواجهة المحددة. استخدام الحالة: "تحسين الكائن".
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): array; }
كان هناك شرط لقيمة الإرجاع من نوع معين يمكن من خلالها فهم أن هذا عنصر من عناصر المجموعة. استخدام حالة: "جمع".
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): VoteInterface; }
استخدام الحالة: "سلسلة بديلة ، التصويت."
namespace MyFramework\Events; interface EventControllInterface { public function stopPropagation(); }
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName, EventControllInterface &$eventControll ); }
دون مناقشة ، فإن وقف نشر الأحداث أمر جيد أو سيء.
تتم قراءة هذا الخيار بشكل لا لبس فيه ، ويوفر مؤشر الترابط الرئيسي فرصة للمستمع لإيقاف هذا الحدث.
لذلك ، إذا انتقلنا إلى وصف المستمع ، فسنحصل على توقيعات أفضل للقراءة لأساليب الاستماع عند تطويرها.
بالإضافة إلى ذلك ، لدينا الفرصة لمؤشر الترابط الرئيسي للإشارة بوضوح إلى:
- قبول التغييرات البيانات الواردة
- إرجاع أنواع أخرى غير الأنواع الواردة
- نقل صريح لكائن لإيقاف انتشار الحدث
كيفية تنفيذ اشتراك الحدث
قد تكون الخيارات مختلفة. يعود الإحساس العام بكل الخيارات إلى حقيقة أننا بحاجة إلى إعلام كائن ListenerProvider بطريقة أو بأخرى (الكائن الذي يوفر الفرصة للاشتراك في الحدث) ، وهو الحدث الذي تنتمي إليه الواجهة المحددة.
يمكنك اعتبار تحويل الكائن الذي تم تمريره إلى نوع يمكن الاتصال به كمثال. يجب أن يكون مفهوما أنه قد يكون هناك العديد من الخيارات للحصول على معلومات التعريف الإضافية:
- يمكن تمريرها بشكل صريح ، كما في المثال
- يمكن تخزينها في شروح واجهات المستمع
- يمكنك استخدام اسم واجهة المستمع كاسم للأحداث
مثال على تنفيذ الاشتراك
namespace MyFramework\Events; class ListenerProvider { private $handlerAssociation = []; public function addHandlerAssociation( string $handlerInterfaceName, string $handlerMethodName, string $eventName ) { $this->handlerAssociation[$handlerInterfaceName] = [ 'methodName' => $handlerMethodName, 'eventName' => $eventName ]; } public function addHandler(object $handler) { $hasAssociation = false; foreach( $this->handlerAssociation as $handlerInterfaceName => $handlerMetaData ) { if ( $handler interfaceof $handlerInterfaceName ) { $methodName = $handlerMetaData['methodName']; $eventName = $handlerMetaData['eventName']; $this->addListener($eventName, [$handler, $methodName]); $hasAssociation = true; } } if ( !$hasAssociation ) { throw new \Exception('Unknown handler object'); } } }
نضيف طريقة تكوين إلى كائن الاشتراك ، والذي يصف كل واجهة مستمع بيانات التعريف الخاصة به ، مثل الطريقة التي تم استدعاؤها واسم الحدث.
وفقًا لهذه البيانات ، في وقت الاشتراك ، نقوم بتحويل معالج $ الذي تم تمريره إلى كائن قابل للاستدعاء يشير إلى الطريقة التي تم استدعاءها.
إذا لاحظت ، يشير الرمز إلى أن كائن معالج $ واحد يمكنه تنفيذ العديد من واجهات مستمع الأحداث وسيتم الاشتراك في كل منها. هذا هو التناظرية من SubscriberInterface للاشتراك الشامل لكائن إلى عدة أحداث. كما ترون ، في التطبيق أعلاه ، لا يلزم وجود آلية منفصلة على أنها addSubscriber(SubscriberInterface $subscriber)
فقد تحولت إلى حل خارج الصندوق.
المرسل
للأسف ، فإن الطريقة الموصوفة تتعارض مع الواجهة المقبولة كمعيار psr / event-dispatcher
لأننا لسنا بحاجة لتمرير أي كائن إلى Dispatcher. نعم ، يمكنك تمرير كائن مثل السكر:
class Event { public function __construct(string $eventName, ...$arguments) {
واستخدمه عند إنشاء حدث على واجهة psr ، لكنه مجرد أمر قبيح.
بطريقة جيدة ، ستبدو واجهة Dispatcher بالشكل التالي:
interface EventDispatcherInterface { public function dispatch(string $eventName, ...$arguments); public function dispatchStopabled(string $eventName, ...$arguments); }
لماذا طريقتين؟ من الصعب دمج جميع حالات الاستخدام في تطبيق واحد. من الأفضل إضافة الطريقة الخاصة بك لكل حالة استخدام ، سيكون هناك تفسير لا لبس فيه لكيفية معالجة Dispatcher للقيم التي تم إرجاعها من المستمعين.
هذا كل شيء. سيكون من المثير للاهتمام أن نناقش مع المجتمع ما إذا كان النهج الموصوف له الحق في الحياة.