تم اقتراح وضع Observer ، المعروف أيضًا باسم وضع النشر/الاشتراك ، من قبل المجموعة المكونة من أربعة أشخاص (GOF ، وهي Erich Gamma و Richard Helm و Ralph Johnson و John Vlissides) في "نمط التصميم: The Basics of Resulable Comply Orgening" (انظر الصفحات 293-313 في كتاب التفاصيل). على الرغم من أن هذا النمط له تاريخ كبير ، إلا أنه لا يزال ينطبق على نطاق واسع على مجموعة متنوعة من السيناريوهات ، وقد أصبح جزءًا لا يتجزأ من مكتبة Java القياسية. على الرغم من وجود الكثير من المقالات حول أنماط المراقب ، إلا أنها تركز جميعها على التنفيذ في Java ، ولكنها تتجاهل مختلف المشكلات التي يواجهها المطورين عند استخدام أنماط المراقب في Java.
تتمثل القصد الأصلي لكتابة هذه المقالة في ملء هذه الفجوة: تقدم هذه المقالة بشكل أساسي تنفيذ نمط المراقب باستخدام بنية Java8 ، ويستكشف المزيد من المشكلات المعقدة حول الأنماط الكلاسيكية على هذا الأساس ، بما في ذلك الفصول الداخلية المجهولة ، وتعبيرات Lambda ، وسلامة الرسائل ، وتنفيذ مراقب غير مهذب للوقت. على الرغم من أن محتوى هذه المقالة غير شامل ، إلا أنه لا يمكن تفسير العديد من المشكلات المعقدة التي ينطوي عليها هذا النموذج في مقالة واحدة فقط. ولكن بعد قراءة هذا المقال ، يمكن للقراء فهم ماهية نمط المراقب ، وعالميته في Java ، وكيفية التعامل مع بعض المشكلات الشائعة عند تنفيذ نمط المراقب في Java.
وضع المراقب
وفقًا للتعريف الكلاسيكي الذي اقترحه GOF ، فإن موضوع نمط المراقب هو:
يحدد التبعية الواحدة بين الكائنات. عندما تتغير حالة الكائن ، يتم إخطار جميع الكائنات التي تعتمد عليه وتحديثها تلقائيًا.
ماذا يعني ذلك؟ في العديد من تطبيقات البرامج ، تكون الحالات بين الكائنات مترابطة. على سبيل المثال ، إذا كان التطبيق يركز على معالجة البيانات العددية ، فقد يتم عرض هذه البيانات من خلال الجداول أو المخططات لواجهة المستخدم الرسومية (GUI) أو المستخدمة في نفس الوقت ، أي عند تحديث البيانات الأساسية ، يجب أيضًا تحديث مكونات واجهة المستخدم الرسومية المقابلة. مفتاح المشكلة هو كيفية تحديث البيانات الأساسية عند تحديث مكونات واجهة المستخدم الرسومية ، وفي الوقت نفسه تقلل من الاقتران بين مكونات واجهة المستخدم الرسومية والبيانات الأساسية.
يتمثل الحل البسيط وغير القابل للتطبيق في الإشارة إلى مكونات واجهة المستخدم الرسومية للمعلومات التي تدير هذه البيانات الأساسية ، بحيث يمكن للكائنات إخطار مكونات واجهة المستخدم الرسومية عند تغيير البيانات الأساسية. من الواضح أن هذا الحل البسيط أظهر بسرعة أوجه القصور في التطبيقات المعقدة التي تتعامل مع المزيد من مكونات واجهة المستخدم الرسومية. على سبيل المثال ، هناك 20 مكونًا من مكونات واجهة المستخدم الرسومية تعتمد جميعها على البيانات الأساسية ، وبالتالي فإن الكائنات التي تدير البيانات الأساسية تحتاج إلى الحفاظ على هذه المكونات العشرين. مع زيادة عدد الكائنات التي تعتمد على البيانات ذات الصلة ، يصعب التحكم في درجة الاقتران بين إدارة البيانات والكائنات.
حل آخر أفضل هو السماح للكائنات بالتسجيل للحصول على أذونات لتحديث البيانات ذات الاهتمام ، والتي يقوم مدير البيانات بإخطار هذه الكائنات عند تغيير البيانات. بشروط Layman ، دع كائن البيانات ذي الاهتمام يخبر المدير: "يرجى إخطارني عندما تتغير البيانات". بالإضافة إلى ذلك ، لا يمكن لهذه الكائنات التسجيل فقط للحصول على إشعارات التحديث ، ولكن أيضًا إلغاء التسجيل للتأكد من أن مدير البيانات لم يعد يخطر الكائن عند تغيير البيانات. في التعريف الأصلي لـ GOF ، يسمى الكائن المسجل للحصول على تحديثات "Observer" ، ويسمى مدير البيانات المقابل "الموضوع" ، والبيانات التي يهتم بها المراقب تسمى "الحالة المستهدفة" ، وتسمى عملية التسجيل "ADD" وعملية مراقبة التراجع تسمى "disach". كما ذكر أعلاه ، يسمى وضع المراقب أيضًا وضع النشر. يمكن فهم أن العميل يشترك في المراقب حول الهدف. عند تحديث الحالة المستهدفة ، ينشر الهدف هذه التحديثات إلى المشترك (يتم تمديد نمط التصميم هذا إلى بنية عامة ، تسمى بنية النشر-الاشتراك). يمكن تمثيل هذه المفاهيم من خلال الرسم البياني للصف التالي:
يستخدمه فيما يتعلق بتلقي تغييرات حالة التحديث وتمرير مرجع إلى exeretesubject إلى مُنشئه. يوفر هذا إشارة إلى موضوع معين لمراقب معين ، يمكن من خلاله الحصول على التحديثات عندما تتغير الحالة. ببساطة ، سيتم إخبار المراقب المحدد بتحديث الموضوع ، وفي الوقت نفسه ، استخدم المراجع في مُنشئه للحصول على حالة الموضوع المحدد ، وأخيراً تخزين كائنات حالة البحث هذه ضمن خاصية ObserverState للمراقب المحدد. تظهر هذه العملية في مخطط التسلسل التالي:
تخصص النماذج الكلاسيكية <BR /> على الرغم من أن نموذج المراقب عالمي ، فهناك أيضًا العديد من النماذج المتخصصة ، والأكثر شيوعًا هي الاثنان التاليين:
1. توفير معلمة لكائن الحالة وقم بتمريرها إلى طريقة التحديث التي يطلق عليها المراقب. في الوضع الكلاسيكي ، عندما يتم إخطار المراقب بأن حالة الموضوع قد تغيرت ، سيتم الحصول على حالتها المحدثة مباشرة من الموضوع. هذا يتطلب من المراقب حفظ مرجع كائن إلى الحالة التي تم استردادها. هذا يشكل مرجعًا دائريًا ، يشير مرجع concretesubject إلى قائمة المراقبين الخاصة به ، ويشير مرجع ConcreteObserver إلى concretesubject التي يمكن أن تحصل على حالة الموضوع. بالإضافة إلى الحصول على الحالة المحدثة ، لا يوجد أي صلة بين المراقب والموضوع الذي يسجله للاستماع إليه. يهتم المراقب بكائن الدولة ، وليس الموضوع نفسه. وهذا يعني ، في كثير من الحالات ، ترتبط ConcreteObserver و concretesubject معًا بالقوة. على العكس من ذلك ، عندما يستدعي concretesubject وظيفة التحديث ، يتم تمرير كائن الحالة إلى ConcreteObserver ، ولا يحتاج الاثنان إلى ربطهما. إن العلاقة بين ConcreteObserver و STATE COSTER تقلل من درجة الاعتماد بين المراقب والدولة (انظر مقالة Martin Fowler لمزيد من الاختلافات في الارتباط والاعتماد).
2. دمج الفئة المجردة للموضوع و concretesubject في فئة الفردي. في معظم الحالات ، لا يؤدي استخدام الفئات المجردة في الموضوع إلى تحسين مرونة البرنامج وقابليته للتوسع ، لذا فإن الجمع بين هذه الفئة المجردة والفئة الملموسة يبسط التصميم.
بعد الجمع بين هذين النموذجين المتخصصين ، يكون مخطط الفئة المبسط كما يلي:
في هذه النماذج المتخصصة ، يتم تبسيط بنية الطبقة الثابتة بشكل كبير ويتم تبسيط التفاعلات بين الفئات. مخطط التسلسل في هذا الوقت هو كما يلي:
ميزة أخرى في وضع التخصص هي إزالة المتغير الأعضاء من ConcreteObserver. في بعض الأحيان ، لا يحتاج المراقب المحدد إلى حفظ أحدث حالة من الموضوع ، ولكن يحتاج فقط إلى مراقبة حالة الموضوع عند تحديث الحالة. على سبيل المثال ، إذا قام المراقب بتحديث قيمة متغير العضو إلى الإخراج القياسي ، فيمكنه حذف ObserVerState ، الذي يزيل الارتباط بين ConcreteObserver وفئة الحالة.
تستخدم قواعد التسمية الأكثر شيوعًا <br /> الأوضاع الكلاسيكية وحتى الوضع المهني المذكور أعلاه مصطلحات مثل الإرفاق والفصل والمراقب ، في حين أن العديد من تطبيقات Java تستخدم قواميس مختلفة ، بما في ذلك التسجيل ، غير التسجيل ، المستمع ، إلخ. تجدر الإشارة إلى أن الحالة هي مصطلح عام لجميع الكائنات التي يحتاجها المستمع إلى مراقبة التغييرات. يعتمد الاسم المحدد لكائن الحالة على السيناريو المستخدم في وضع المراقب. على سبيل المثال ، في وضع المراقب في المشهد حيث يستمع المستمع إلى حدوث الحدث ، سيتلقى المستمع المسجل إشعارًا عند حدوث الحدث. كائن الحالة في هذا الوقت هو الحدث ، أي ما إذا كان الحدث قد حدث.
في التطبيقات الفعلية ، نادراً ما يتضمن تسمية الأهداف موضوعًا. على سبيل المثال ، قم بإنشاء تطبيق حول حديقة الحيوان ، وتسجيل مستمعين متعددين لمراقبة فئة حديقة الحيوان ، وتلقي الإخطارات عندما تدخل الحيوانات الجديدة إلى حديقة الحيوان. الهدف في هذه الحالة هو فئة حديقة الحيوان. من أجل الحفاظ على المصطلحات متسقة مع مجال المشكلة المحدد ، لن يتم استخدام مصطلح "الموضوع" ، مما يعني أنه لن يتم تسمية فئة حديقة الحيوان.
يتبع تسمية المستمع عمومًا لاحقة المستمع. على سبيل المثال ، سيتم تسمية المستمع المذكور أعلاه لمراقبة الحيوانات الجديدة. وبالمثل ، فإن تسمية الوظائف مثل التسجيل ، وعدم التسجيل والإخطار غالباً ما يتم احتسابها بأسماء المستمعين المقابلة. على سبيل المثال ، سيتم تسمية السجل ، غير المسجلة ، وإخطار وظائف animaladdedlistener. تجدر الإشارة إلى أنه يتم استخدام اسم وظيفة الإخطار ، لأن دالة الإخطار تتعامل مع مستمعين متعددين بدلاً من مستمع واحد.
ستظهر طريقة التسمية هذه طويلة ، وعادة ما يسجل الموضوع أنواعًا متعددة من المستمعين. على سبيل المثال ، في مثال حديقة الحيوان المذكورة أعلاه ، في حديقة الحيوان ، بالإضافة إلى تسجيل مستمعين جدد لمراقبة الحيوانات ، يحتاج أيضًا إلى تسجيل مستمع للحيوانات لتقليل المستمعين. في هذا الوقت ، ستكون هناك وظيفتان للتسجيل: (RecordAnimalAddedListener و RecordAnimalRemovedListener. وبهذه الطريقة ، يتم استخدام نوع المستمع كأحد المؤهلات للإشارة إلى نوع المراقب. حل آخر هو إنشاء وظيفة قائمة تسجيل ، ثم تحمله الزائد ، ولكن الحل 1 يمكن أن يعرف بشكل أكثر سهولة الاستماع.
هناك بناء جملة Idiomatic آخر هو استخدام البادئة بدلاً من التحديث ، على سبيل المثال ، تتم تسمية وظيفة التحديث onanimaladded بدلاً من updateanimaladded. يكون هذا الموقف أكثر شيوعًا عندما يحصل المستمع على إشعارات للتسلسل ، مثل إضافة حيوان إلى القائمة ، ولكن نادراً ما يتم استخدامه لتحديث بيانات منفصلة ، مثل اسم الحيوان.
بعد ذلك ، ستستخدم هذه المقالة قواعد Java الرمزية. على الرغم من أن القواعد الرمزية لن تغير التصميم الحقيقي وتنفيذ النظام ، إلا أنه من المهم مبدأ تطوير مهم لاستخدام المصطلحات التي يعرفها المطورين الآخرون ، لذلك يجب أن تكون على دراية بقواعد نمط المراقب في Java الموصوفة أعلاه. سيتم شرح المفهوم أعلاه أدناه باستخدام مثال بسيط في بيئة Java 8.
مثال بسيط
إنه أيضًا مثال حديقة الحيوان المذكورة أعلاه. باستخدام واجهة API من JAVA8 لتنفيذ نظام بسيط ، يشرح المبادئ الأساسية لنمط المراقب. تم وصف المشكلة على النحو التالي:
قم بإنشاء حديقة حيوان نظام ، مما يتيح للمستخدمين الاستماع والتراجع عن حالة إضافة كائن جديد للحيوان ، وإنشاء مستمع معين ، مسؤول عن إخراج اسم الحيوان الجديد.
وفقًا للتعلم السابق لنمط المراقب ، نعلم أنه لتنفيذ مثل هذا التطبيق ، نحتاج إلى إنشاء 4 فئات ، على وجه التحديد:
أولاً ، نقوم بإنشاء فئة حيوانية ، وهو كائن Java بسيط يحتوي على متغيرات العضو ، والمصممون ، و Getters وطرق Setter. الرمز كما يلي:
الفئة العامة Animal {اسم السلسلة الخاصة ؛ الحيوانات العامة (اسم السلسلة) {this.name = name ؛ } السلسلة العامة getName () {return this.name ؛ } public void setName (اسم السلسلة) {this.name = name ؛ }}استخدم هذه الفئة لتمثيل الأشياء الحيوانية ، وبعد ذلك يمكنك إنشاء واجهة AnimalAddedListener:
الواجهة العامة animaladdedlistener {public void onanimaladded (Animal Animal) ؛}الفئتين الأولين بسيطان للغاية ، لذلك لن أقدمهما بالتفصيل. بعد ذلك ، قم بإنشاء فئة حديقة الحيوان:
Zoo Class Public {Private List <Inmal> leuties = new ArrayList <> () ؛ قائمة خاصة <AntamentDedListener> المستمعين = جديد ArrayList <> () ؛ public void addanimal (Animal Animal) {// أضف الحيوان إلى قائمة الحيوانات this.animals.add (Animal) ؛ // إخطار قائمة المستمعين المسجلين this.notifyanimaladdedlisteners (Animal) ؛ } public void registerAnimalAddedListener (AnimalAddedListener Beasherer) {// أضف المستمع إلى قائمة المستمعين المسجلين this.listeners.add (المستمع) ؛ } public void unregisteranimaladdedlistener (AnimalAddedListener beasherer) {// قم بإزالة المستمع من قائمة المستمعين المسجلين this.listeners.remove (المستمع) ؛ } void void notifyanimaladdedlisteners (Animal Animal) {// إخطار كل من المستمعين في قائمة مستمعي المستمعين المسجلين this.listeners.foreach (المستمع -> المستمع. }}هذا القياس معقد من السابقين. يحتوي على قائمتين ، يتم استخدام إحداها لتخزين جميع الحيوانات في حديقة الحيوان والآخر يستخدم لتخزين جميع المستمعين. بالنظر إلى أن الكائنات المخزنة في الحيوانات ومجموعات المستمع بسيطة ، فقد اختارت هذه المقالة ArrayList للتخزين. يعتمد بنية البيانات المحددة للمستمع المخزن على المشكلة. على سبيل المثال ، بالنسبة لمشكلة حديقة الحيوان هنا ، إذا كان للمستمع الأولوية ، فيجب عليك اختيار بنية بيانات أخرى ، أو إعادة كتابة خوارزمية سجل المستمع.
يعد تنفيذ التسجيل والإزالة على حد سواء طريقة مندوب بسيط: تتم إضافة أو إزالة كل مستمع من قائمة الاستماع للمستمع كمعلمة. يتم تنفيذ وظيفة الإخطار قليلاً عن التنسيق القياسي لنمط المراقب. ويشمل معلمة الإدخال: الحيوان الذي تمت إضافته حديثًا ، بحيث يمكن لدالة الإخطار أن تمرر مرجع الحيوانات المضافة حديثًا إلى المستمع. استخدم وظيفة foreach من واجهة برمجة تطبيقات التدفقات لاجتياز المستمعين وتنفيذ وظيفة theonanimaladded على كل مستمع.
في وظيفة Addanimal ، تتم إضافة الكائن الحيواني والمستمع المضافة حديثًا إلى القائمة المقابلة. إذا لم يتم أخذ تعقيد عملية الإخطار في الاعتبار ، فيجب تضمين هذا المنطق في طريقة مكالمة مريحة. ما عليك سوى المرور في إشارة إلى الكائن الحيوانية المضافة حديثًا. هذا هو السبب في أن التنفيذ المنطقي لمستمع الإخطار مغلف في دالة الإخطارات.
بالإضافة إلى القضايا المنطقية المتمثلة في الإخطار ، من الضروري التأكيد على القضية المثيرة للجدل حول رؤية وظائف الإخطار. في نموذج المراقب الكلاسيكي ، كما قال GOF في الصفحة 301 من أنماط تصميم الكتب ، فإن وظيفة الإخطار عامة ، ولكن على الرغم من استخدامها في النمط الكلاسيكي ، فإن هذا لا يعني أنه يجب أن يكون عامًا. يجب أن يعتمد اختيار الرؤية على التطبيق. على سبيل المثال ، في مثال حديقة حيوان هذه المقالة ، تكون وظيفة الإخطار من النوع المحمي ولا تتطلب كل كائن لبدء إشعار مراقب مسجل. يحتاج فقط إلى التأكد من أن الكائن يمكن أن يرث الوظيفة من الفئة الأصل. بالطبع ، هذا ليس هو الحال بالضبط. من الضروري معرفة الفئات التي يمكنها تنشيط وظيفة الإخطار ، ثم تحديد رؤية الوظيفة.
بعد ذلك ، تحتاج إلى تنفيذ فئة printnameanimaladdedlistener. تستخدم هذه الفئة طريقة system.out.println لإخراج اسم الحيوان الجديد. الرمز المحدد كما يلي:
الطبقة العامة printnameanimaladdedlistener تنفذ animaladdedistener {Override public void updateanimaladded (Animal Animal) {// print اسم النظام الحيواني المضافة حديثًا. }}أخيرًا ، نحتاج إلى تنفيذ الوظيفة الرئيسية التي تدفع التطبيق:
الفئة العامة الرئيسية {public static void main (string [] args) {// إنشاء حديقة الحيوان لتخزين Zoo Zoo = new Zoo () ؛ . // إضافة حيوان يخطر Zoo.Addanimal (حيوان جديد ") ؛ }}تقوم الوظيفة الرئيسية ببساطة بإنشاء كائن حديقة للحيوانات ، ويسجل مستمعًا يخرج اسم الحيوان ، ويقوم بإنشاء كائن حيوان جديد لدفع المستمع المسجل. الإخراج النهائي هو:
أضاف حيوان جديد يحمل اسم "النمر"
أضاف المستمع
يتم عرض مزايا وضع المراقب بالكامل عند إعادة تأسيس المستمع وإضافته إلى الموضوع. على سبيل المثال ، إذا كنت ترغب في إضافة مستمع يحسب العدد الإجمالي للحيوانات في حديقة الحيوان ، فأنت بحاجة فقط إلى إنشاء فئة مستمع معينة وتسجيله في فئة حديقة الحيوان دون أي تعديل لفئة حديقة الحيوان. إن إضافة رمز CountingAnimalAddedListener هو على النحو التالي:
الطبقة العامة CountingAnimalAddedListener تنفذ animaladdedistener {private static int eleventsaddedcount = 0 ؛ Override public void updateanimaladded (Animal Animal) {// زيادة عدد الحيوانات animalsaddedcount ++ ؛ // اطبع عدد الحيوانات system.out.println ("Total Animals المضافة:" + eleustsaddedcount) ؛ }}الوظيفة الرئيسية المعدلة هي كما يلي:
الفئة العامة الرئيسية {public static void main (string [] args) {// إنشاء حديقة الحيوان لتخزين Zoo Zoo = new Zoo () ؛ . Zoo.RegisterAnimalAddedListener (New CountingAnimalAddedListener ()) ؛ // إضافة حيوان يخطر Zoo.Addanimal (حيوان جديد ") ؛ Zoo.Addanimal (حيوان جديد ("الأسد")) ؛ Zoo.Addanimal (حيوان جديد ("بير")) ؛ }}نتيجة الإخراج هي:
تمت إضافة حيوان جديد يحمل اسم "حيوانات Tiger'total": 1 ضحى حيوانًا جديدًا يحمل اسم "حيوانات أسد لا تضاف: 2 مضافة حيوانًا جديدًا مع اسم" حيوانات بيرتال ".
يمكن للمستخدم إنشاء أي مستمع إذا قام فقط بتعديل رمز تسجيل المستمع. هذا قابلية التوسع ترتبط بشكل أساسي بموضوع المراقب ، بدلاً من المرتبطة مباشرة بـ ConcreteObserver. طالما لم يتم تعديل الواجهة ، فليس هناك حاجة لتعديل موضوع الواجهة.
فصول داخلية مجهولة ، وظائف Lambda وتسجيل المستمع
أحد التحسنات الرئيسية في Java 8 هو إضافة الميزات الوظيفية ، مثل إضافة وظائف Lambda. قبل تقديم وظيفة Lambda ، قدمت Java وظائف مماثلة من خلال فئات داخلية مجهولة المصدر ، والتي لا تزال تستخدم في العديد من التطبيقات الموجودة. في وضع Observer ، يمكن إنشاء مستمع جديد في أي وقت دون إنشاء فصل مراقب معين. على سبيل المثال ، يمكن تنفيذ فئة printnameanimaladdedlistener في الوظيفة الرئيسية مع فئة داخلية مجهولة المصدر. رمز التنفيذ المحدد كما يلي:
الفئة العامة الرئيسية {public static void main (string [] args) {// إنشاء حديقة الحيوان لتخزين Zoo Zoo = new Zoo () ؛ . // إضافة حيوان يخطر Zoo.Addanimal (حيوان جديد ") ؛ }}وبالمثل ، يمكن أيضًا استخدام وظائف Lambda لإكمال مثل هذه المهام:
الفئة العامة الرئيسية {public static void main (string [] args) {// إنشاء حديقة الحيوان لتخزين Zoo Zoo = new Zoo () ؛ . // إضافة حيوان يخطر Zoo.Addanimal (حيوان جديد ") ؛ }}تجدر الإشارة إلى أن وظيفة Lambda مناسبة فقط للمواقف التي توجد فيها وظيفة واحدة فقط في واجهة المستمع. على الرغم من أن هذا المطلب يبدو صارمًا ، إلا أن العديد من المستمعين هم في الواقع وظائف واحدة ، مثل animaladdedlistener في المثال. إذا كانت الواجهة تحتوي على وظائف متعددة ، فيمكنك اختيار استخدام فئات داخلية مجهولة.
هناك مشكلة في التسجيل الضمني للمستمع الذي تم إنشاؤه: نظرًا لأن الكائن يتم إنشاؤه في نطاق مكالمة التسجيل ، فمن المستحيل تخزين إشارة إلى مستمع معين. هذا يعني أنه لا يمكن إلغاء المستمعين المسجلين من خلال وظائف Lambda أو الفئات الداخلية المجهولة لأن وظائف الإلغاء تتطلب إشارة إلى المستمع المسجل. تتمثل إحدى الطرق السهلة لحل هذه المشكلة في إرجاع إشارة إلى المستمع المسجل في وظيفة RecordAnimalAddedListener. وبهذه الطريقة ، يمكنك إلغاء تسجيل المستمع الذي تم إنشاؤه باستخدام وظائف Lambda أو فئات داخلية مجهولة. رمز الطريقة المحسّن هو كما يلي:
PublicAdadDedListener RecordanimaladDedListener (AnimalAddedListener Beasherer) {// إضافة المستمع إلى قائمة المستمعين المسجلين this.listeners.add (المستمع) ؛ مستمع إرجاع ؛}رمز العميل لتفاعل الوظيفة المعاد تصميمه هو كما يلي:
الفئة العامة الرئيسية {public static void main (string [] args) {// إنشاء حديقة الحيوان لتخزين Zoo Zoo = new Zoo () ؛ // تسجيل المستمعين ليتم إخطارهم عند إضافة حيوان animaladdedlistener المستمع = zoo.registeranimaladdedlistener ((Animal) -> system.out.println ("أضاف حيوانًا جديدًا باسم" + Animal.getName () + "''") ؛ // إضافة حيوان يخطر Zoo.Addanimal (حيوان جديد ") ؛ // unregister the Zoo.UnregisterAnimalAddedListener (المستمع) ؛ // إضافة حيوان آخر ، لن يطبع الاسم ، لأن المستمع // لم يكن مسجلًا من قبل Zoo.Addanimal (حيوان جديد ("Lion")) ؛ }}تتم إضافة ناتج النتيجة في هذا الوقت فقط حيوان جديد يحمل اسم "Tiger" ، لأنه تم إلغاء المستمع قبل إضافة الحيوان الثاني:
أضاف حيوان جديد يحمل اسم "النمر"
إذا تم اعتماد حل أكثر تعقيدًا ، فيمكن أن تقوم وظيفة التسجيل أيضًا بإرجاع فئة المتلقي بحيث يتم استدعاء مستمع UNGISTER ، على سبيل المثال:
الطبقة العامة AnimalAddedListenerReceipt {Private FinalAddedListener Beasherer ؛ PublicAdadDedListenerReceipt (AnimalAddedListener Beasher) {this.listener = beather ؛ } public animalAddedListener getListener () {return this.listener ؛ }}سيتم استخدام الاستلام كقيمة الإرجاع لوظيفة التسجيل ويتم إلغاء معلمات الإدخال لوظيفة التسجيل. في هذا الوقت ، يكون تطبيق حديقة الحيوان كما يلي:
الطبقة العامة ZooUsingReceipt {// ... السمات الحالية والمُنشئ ... PublicAdedDedListenerCeipt registantanimaladdedListener (AnimalAddedListener Beasherer) {// إضافة المستمع إلى قائمة المستمعين المسجلين this.listeners.add (المستمع) ؛ إرجاع New AnimalAddedListenerReceipt (مستمع) ؛ } public void unregisteranimaladdedlistener (animaladdedListenerreceipt Office) {// قم بإزالة المستمع من قائمة المستمعين المسجلين this.listeners.remove (recipt.getListener ()) ؛ } // ... طريقة الإخطار الحالية ...}تتيح آلية التنفيذ المستقبلة الموضحة أعلاه تخزين المعلومات للاتصال بالمستمع عند الإلغاء ، أي إذا كانت خوارزمية تسجيل الإلغاء تعتمد على حالة المستمع عندما يسجل الموضوع المستمع ، فسيتم حفظ هذه الحالة. إذا كان تسجيل الإلغاء يتطلب فقط إشارة إلى المستمع المسجل السابق ، فستظهر تقنية الاستقبال مزعجة ولا ينصح بها.
بالإضافة إلى المستمعين المعقدين بشكل خاص ، فإن الطريقة الأكثر شيوعًا لتسجيل المستمعين هي من خلال وظائف Lambda أو من خلال فصول داخلية مجهولة المصدر. بالطبع ، هناك استثناءات ، أي الفئة التي تحتوي على موضوع ينفذ واجهة المراقب ويسجل مستمعًا يدعو إلى الهدف المرجعي. الحالة كما هو موضح في الكود التالي:
الطبقة العامة Zoocontainer تنفذ animaladdedlistener {private zoo zoo = new zoo () ؛ public zooocontainer () {// قم بتسجيل هذا الكائن كمستمع this.zoo.registerAnimalAddedListener (this) ؛ } public zoo getzoo () {return this.zoo ؛ } Override public void updateanimaladded (Animal Animal) {system.out.println ("Andival Animal with name '" + Animal.getName () + "'") ؛ } public static void main (string [] args) {// إنشاء حاوية حديقة الحيوان zooocontainer zooocontainer = new zoocontainer () ؛ // إضافة حيوان يخطر المستمع الذي تم إخطاره الداخلي zooocontainer.getzoo (). addanimal (حيوان جديد ("Tiger")) ؛ }}هذا النهج مناسب فقط للحالات البسيطة ولا يبدو الرمز محترفًا بدرجة كافية ، ولا يزال يحظى بشعبية كبيرة بين مطوري Java الحديثين ، لذلك من الضروري فهم كيفية عمل هذا المثال. نظرًا لأن Zoocontainer ينفذ واجهة AnimalAddedListener ، يمكن تسجيل مثيل (أو كائن) من Zoocontainer كأحد الأطراف على animaladdedlistener. في فئة zooocontainer ، يمثل هذا المرجع مثيلًا للكائن الحالي ، أي zooocontainer ، ويمكن استخدامه كمحرك animaladdedlistener.
بشكل عام ، لا يلزم جميع فئات الحاويات لتنفيذ هذه الوظائف ، وفئة الحاوية التي تنفذ واجهة المستمع يمكنها فقط استدعاء وظيفة تسجيل الموضوع ، ولكن ببساطة تمرير المرجع إلى وظيفة السجل ككائن المستمع. في الفصول التالية ، سيتم تقديم الأسئلة الشائعة والحلول للبيئات المتعددة.
تنفيذ سلامة الخيط <BR /> يقدم الفصل السابق تنفيذ نمط المراقب في بيئة Java الحديثة. على الرغم من أنها بسيطة ولكنها كاملة ، فإن هذا التنفيذ يتجاهل مشكلة رئيسية: سلامة مؤشر الترابط. معظم تطبيقات Java مفتوحة متعددة الخيوط ، ويستخدم وضع المراقب في الغالب في أنظمة متعددة الخيوط أو غير متزامنة. على سبيل المثال ، إذا قامت خدمة خارجية بتحديث قاعدة البيانات الخاصة بها ، فسيتلقى التطبيق أيضًا رسالة غير متزامنة ثم إخطار المكون الداخلي بالتحديث في وضع المراقب ، بدلاً من التسجيل المباشر والاستماع إلى الخدمة الخارجية.
تركز سلامة الخيط في وضع المراقب بشكل أساسي على جسم الوضع ، لأن تعارضات مؤشرات الترابط من المحتمل أن تحدث عند تعديل مجموعة المستمع المسجل. على سبيل المثال ، يحاول مؤشر ترابط واحد إضافة مستمع جديد ، بينما يحاول مؤشر الترابط الآخر إضافة كائن حيوان جديد ، والذي يؤدي إلى الإشعارات إلى جميع المستمعين المسجلين. بالنظر إلى ترتيب التسلسل ، قد يكون أو لا يكون مؤشر الترابط الأول قد أكمل تسجيل المستمع الجديد قبل أن يتلقى المستمع المسجل إشعارًا للحيوان المضافة. هذه حالة كلاسيكية من مسابقة موارد الخيوط ، وهذه الظاهرة هي التي تخبر المطورين أنهم يحتاجون إلى آلية لضمان سلامة الخيوط.
أسهل حل لهذه المشكلة هو: يجب أن تتبع جميع العمليات التي تصل أو تعديل قائمة مستمع التسجيل آلية مزامنة Java ، مثل:
public animaladdededlistener registeranimaladdededlistener (AnimalAddedListener Beasher) {/*..وبهذه الطريقة ، في الوقت نفسه ، يمكن لخيط واحد فقط تعديل أو الوصول إلى قائمة المستمع المسجلة ، والتي يمكن أن تجنب بنجاح مشكلات المنافسة في الموارد ، ولكن تنشأ مشكلات جديدة ، وتكون هذه القيود صارمة للغاية (لمزيد من المعلومات حول الكلمات الرئيسية المتزامنة ونماذج التزامن Java ، يرجى الرجوع إلى صفحة الويب الرسمية). من خلال مزامنة الطريقة ، يمكن ملاحظة الوصول المتزامن إلى قائمة المستمع في جميع الأوقات. إن تسجيل وإلغاء المستمع هو عملية كتابة لقائمة المستمع ، مع إخطار المستمع للوصول إلى قائمة المستمع هو عملية للقراءة فقط. نظرًا لأن الوصول من خلال الإخطار هو عملية قراءة ، يمكن تنفيذ عمليات الإخطار المتعددة في وقت واحد.
لذلك ، طالما لا يوجد تسجيل أو إلغاء المستمع ، طالما لم يتم تسجيل التسجيل ، طالما أنه يمكن تنفيذ أي عدد من الإخطارات المتزامنة في وقت واحد دون تشغيل مسابقة للموارد على قائمة المستمع المسجلة. بالطبع ، كانت المنافسة في الموارد في المواقف الأخرى موجودة لفترة طويلة. من أجل حل هذه المشكلة ، تم تصميم قفل الموارد لـ ReadWritelock لإدارة عمليات القراءة والكتابة بشكل منفصل. رمز تنفيذ مؤشرات ترابط مؤشرات الترابط المأمّن من فئة حديقة الحيوان على النحو التالي:
الفئة العامة threadsafezoo {private final readwritelock readWritelock = new reentrantreadwritelock () ؛ محمي القفل النهائي readlock = readWritelock.readlock () ؛ محمي القفل النهائي writelock = readWritelock.Writelock () ؛ قائمة خاصة <imannat> الحيوانات = ArrayList جديدة <> () ؛ قائمة خاصة <AntamentDedListener> المستمعين = جديد ArrayList <> () ؛ public void addanimal (Animal Animal) {// أضف الحيوان إلى قائمة الحيوانات this.animals.add (Animal) ؛ // إخطار قائمة المستمعين المسجلين this.notifyanimaladdedlisteners (Animal) ؛ } public animaladdedlistener registeranimaladdedListener (AnimalAddedListener beasherer) {// قفل قائمة المستمعين لكتابة this.writelock.lock () ؛ حاول {// إضافة المستمع إلى قائمة المستمعين المسجلين this.listeners.add (المستمع) ؛ } أخيرًا {// فتح الكاتب قفل this.writelock.unlock () ؛ } مستمع إرجاع ؛ } public void unregisteranimaladdedlistener (AnimalAddedListener beasherer) {// قفل قائمة المستمعين لكتابة this.writelock.lock () ؛ حاول {// إزالة المستمع من قائمة المستمعين المسجلين this.listeners.remove (المستمع) ؛ } أخيرًا {// فتح الكاتب قفل this.writelock.unlock () ؛ }} public void notifyanimaladdedlisteners (Animal Animal) {// قفل قائمة المستمعين لقراءة this.readlock.lock () ؛ جرب {// إخطار كل من المستمعين في قائمة المستمعين المسجلين this.listeners.foreach (المستمع -> المستمع. updateanimaladded (Animal)) ؛ } أخيرًا {// فتح قفل القارئ this.readlock.unlock () ؛ }}}من خلال مثل هذا النشر ، يمكن أن يضمن تنفيذ الموضوع سلامة مؤشرات الترابط ويمكن أن يصدر مؤشرات الترابط المتعددة الإخطارات في نفس الوقت. ولكن على الرغم من ذلك ، لا تزال هناك مشكلتان في مسابقة الموارد لا يمكن تجاهلهما:
الوصول المتزامن إلى كل مستمع. يمكن لخيوط متعددة إخطار المستمع بأن هناك حاجة إلى حيوانات جديدة ، مما يعني أنه قد يتم استدعاء المستمع بواسطة مؤشرات ترابط متعددة في نفس الوقت.
الوصول المتزامن إلى قائمة الحيوانات. قد تضيف مؤشرات ترابط متعددة كائنات إلى قائمة الحيوانات في نفس الوقت. إذا كان لترتيب الإشعارات تأثير ، فقد يؤدي ذلك إلى منافسة الموارد ، مما يتطلب آلية معالجة التشغيل المتزامنة لتجنب هذه المشكلة. إذا تلقيت قائمة المستمع المسجل إشعارًا لإضافة Animal2 ثم تتلقى إشعارًا لإضافة Animal1 ، فسيحدث مسابقة الموارد. ومع ذلك ، إذا تم تنفيذ إضافة Animal1 و Animal2 بواسطة خيوط مختلفة ، فمن الممكن أيضًا إكمال إضافة Animal1 قبل Animal2. على وجه التحديد ، يضيف الموضوع 1 Animal1 قبل إخطار المستمع ويغلق الوحدة النمطية ، ويضيف الموضوع 2 Animal2 ويبلغ المستمع ، ثم يخطر الموضوع 1 المستمع الذي تمت إضافته على Animal1. على الرغم من أنه يمكن تجاهل منافسة الموارد عندما لا يتم النظر في ترتيب التسلسل ، إلا أن المشكلة حقيقية.
对监听器的并发访问
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要在监听器类内实现线程安全。
监听器的有序通知当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); }}}这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类<br />把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); }}正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); }}Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型ZooListener,代码如下所示:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);}这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal) animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); }}将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为Adapter即可),代码如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {}}乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); }}有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的ZooListener接口代码如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}}通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}}这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或进行异步调用,不能立即给出返回值。
Complex & Blocking监听器关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前,并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于Java中的如下操作:
public class
如何使用Java8 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!