الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

Observer Pattern in .Net

بدأه AliBazzi في 19 سبتمبر 2010 · 2 رد · 2,544 مشاهدة · في قسم تكنولوجيا Microsoft .NET العام
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

Observer Pattern هو أحد أهم أنماط التصميم التي يجب أن يحترفها أي مبرمج , لأنها تستعمل بكثافة في حياتنا اليومية عن طريق Paradigm إسمه Event Based Programming , فكرته واضحة ,سهلة ومباشرة .

متطلبات المقال :

فهم جيد للـ #C

Delegates Concept

Generic Concept

هناك لاعبين أساسيين هما :

Observable والذي يستطيع إعلام الاّخرين بأحداث و يكون هو مصدر معلومات , (يمكن تسميته أيضا Notifier , المنبه ) .

Observer من يقوم بالإستجابة للأحداث و إستخلاص منها ما هو مفيد له , ( أو Notifiable , أي قابل للتنبيه ) .

وتكمن الخلطة في أن أي Object ممكن أن يكون Observable عن طريق تقديم حدث (أو عدة أحداث ) تمكن أي Observer Object اّخر في الإشتراك في حدث (أو عدة ) متاح من قبل الـ Observable ,ويستطيع لاحقا الـ Observer فك الإشتراك في حدث (أو عدة أحداث ) قام بالربط معها مسبقا , موفَرة من قبل الـ Observable Object .

مثال : الـ Button (أو الزر ) هو Observable , هيث يقوم بتقديم عدة أحداث (كـ Click , Double Click , Key Down ... ) تعطي معلومات لمشتركيها المهتمين عند حدوثها , ويقوم مشتركيها في دورهم (الـ Observers Object ) بالتصرف بالشكل المناسب لكل حدث ...

يمكن وضع العلاقة هنا في سياق الـ Consumer , Producer حيث يمثل الـ Observable Object المنتِج (Producer) الذي يقوم بإعلام المشترك (أو المشتركين) بالحدث عند وقوعه , وتزويدهم بالمعلومات اللازمة إن لزم الأمر , ويمثل أي Observer Object الـ Consumer الذي يقوم بإستهلاك تلك المعلومات (التي هي ما إلا حدث , وربما رشَّة من المعلومات المتعلقة به ...) بالطريقة التي يراها مناسبة .

وبالتالي الـ Observable Object هو Producer ينتج أحداث (في وقت لا يمكن تحديده من قبل الـ Consumer ) تحمل معلومات لا يدري كيف يعالجها , والـ Observer Object هو Consumer يعرف جيدا كيف يستهلك و يعالج الحدث (ومعلوماته الإضافية ربما ) بالطريقة الصحيحة المناسبة عند حدوثه , لكنه لا يعلم وقت حدوثه ... فالعلاقة تحوي مستفيدين , كل منهما يتكامل دوره مع الاّخر .

سنبدأ بمثال بسيط نطرح من خلاله Pseudo Button (زر وهمي) يقدم حدث وحيد هو Click , و مستهلك وحيد يقوم بالتصرف بالطريقة المناسبة عند ضغط الزر ...

ClassDiagram1.png

البروتوكول المتداول في هذا المثال هو الـ IClickEventObserver Interface التي يجب على أي Observer تطبيقها ليتمكن من الربط فيما بعد مع الحدث في الـ Observable Object المتمثل في مثالنا بالصنف PseudoButton ,حاليا مثالنا يحتوي Two Observers هما ClickObserver1 و ClickObserver2 كلاهما يقدمان سلوكهما الخاص في الإستجابة للحدث , عن طريق الـ OnClick(Point):void Method التي يتم تمرير معلومات إضافية من خلالها عن طريق Point Object من قبل منتِج الحدث , ألا و هو PseudoButton ...

والاّن لنأخذ نظرة على الكود :

public  interface IClickEventObserver
    {
        void OnClick(Point  position);
    }
    public class Point
    {
        public  int X { get; private set; }
        public int Y { get; private set;  }
        internal Point(int x, int y)
        {
     		this.X = x;
            this.Y = y;
        }
    }

وكود الـ Two Observer هو :

public  class ClickObserver1 : IClickEventObserver
    {
        public  void OnClick(Point position)
        {
     		Console.WriteLine("Button Clicked ! ({0},{1})", position.X, position.Y);
 		}
    }
    public class ClickObserver2 :  IClickEventObserver
    {
        public void OnClick(Point  position)
        {
            Console.ForegroundColor =  ConsoleColor.Red;
            Console.WriteLine("Wow ! ({0},{1})",  position.X, position.Y);
            Console.ForegroundColor =  ConsoleColor.Gray;
        }
    }

كلاهما يستجيبان بكتابة معلومات الحدث إلى الـ Console , كل منهما بنكهته ...

والمهم هنا كود الـ Producer أو الـ Observable او الـ Notifier , سمه ما شئت :

public class PseudoButton
    {
 		public void StartSimulatingClicks() // Just ignore this code , to Avoid  Disturbance ...
        {
            new Action(() =>
     		{
                Random rand = new Random();
         		while (true)
                {
             		this.InvokeClickEvent(new Point(rand.Next(0, 300), rand.Next(0, 300)));
             		System.Threading.Thread.Sleep(1500);
         		}
            }).BeginInvoke(null, null);
        }

 		#region Click Event
        private void InvokeClickEvent(Point  position)
        {
            foreach (var observer in  this.clickObservers)
                observer.OnClick(position);
 		}

        private List<IClickEventObserver>  clickObservers = new List<IClickEventObserver>();

 		public void AddClickObserver(IClickEventObserver observer)
        {
     		this.clickObservers.Add(observer);
        }

 		public void DeleteClickObserver(IClickEventObserver observer)
 		{
            this.clickObservers.Remove(observer);
        }
 		#endregion
    }

يحوي هذا الزر على حدث وحيد إسمه click يتم الإشترك أو فك الإشتراك به عن طريق Add or Delete Click Observer Methods اللتان تقبلان أي Object قام بتطبيق IClickEventObserver , إضافة لذلك يحوي StartSimulatingClicks الذي يقوم بإطلاق الأحداث الوهمية في حدوث الـ Clicks كل 1500 ميل ثانية ...

المهم هو الكود الذي سيربط هذه المكونات كلها مع بعض , جربه بنفسك :

PseudoButton button = new PseudoButton(); // creates the pseudo button  (Observable)
            ClickObserver1 observer1 = new  ClickObserver1(); // the First Observer
     		button.AddClickObserver(new ClickObserver2()); // create The Second one  and Attatch it Directly
     		button.AddClickObserver(observer1); // Subscribe the first Observer to  the Click Event
            button.StartSimulatingClicks(); 
     		Console.ReadLine();
     		button.DeleteClickObserver(observer1); // Deattach the First Observer
     		Console.ReadLine();

ما تم سابقا هو إشراك الـ Observer 1 و Observer 2 في حدث الـ Click ومن ثم تشغيل الـ Simulator الذي يقوم بتوليد أحداث وهمية و عند نقر زر Enter سيتم إلغاء إشتراك من قبل Observer 2 وبالتالي سيظل الـ Observer 1 هو المستجيب الوحيد لحدث الـ Click ...

لو لاحظنا عدد الأسطر المطلوبة لعمل Event كـ Click في الـ Button السابق , لوجدنا أن هناك الكثير من الجهد في ذلك , لكن لحسن الحظ , هناك بُنى جاهزة تمكننا من عمل Event بسرعة و يسر متوفرة في إطار العمل Net. ,متوفرة عن طريق الكلمة المفتاحية event , فليكن المثال التالي الذي سيحُل المثال السابق , لكن بطريقة أسهل و "أنظف" :

ClassDiagram2.png

ما وضح من الشكل السابق هو أن الكثير من الأشياء "الرسمية" التي لا داعي لها ذهبت بالفعل و خف الضجيج المحيط بفكرة حدث وحيد إسمه Click وتحديدا :

نلاحظ أن الـ Click Event والظاهر على أنه Member في الـ Pseudo Button حل محل الـ Add ,Delete ClickEventObserver والذين كانا متواجدين في الحل السابق إضافة إلى إختفاء الـ List التي كانت تحمل الـ Clicks Observers تحت إسم observers , كل هذا بسبب أن event EventHandler<ClickEventArgs> Click قد حلت كل هذه المشاكل من : إضافة , حذف و تخزين الـ Observers المهتمين في حدث Click المَهيب icon_e_wink.gif

ونلاحظ أن الـ Observers قد تبخرا و تحولا إلى شيء أكثر منطقية هما Click_Button , Click_Button2 Methods الموجودتان في Program Class ...

لكن ظهر Type جديد هو ClickEventArgs وهو عبارة عن شيء لابد منه , لكي نتبع Design Guidelines المتبع في Microsoft لتصميم الـ Event و الذي ينص على أن أي حدث يجب أن يوفر المعلومتان التاليتين :

Sender وهو من نوع Object يقوم مٌطلق الحدث بالإشارة إلى نفسه , لكي يحصل عليه المتلقي (الـ Observer ) في حال حاجته لذلك , والأمثلة كثيرة في ذلك ...

e ما هو إلا بـ Object من نوع EventArgs (الذي لا يحوي أي شيء مفيد ) أو أحد مشتقاته التي تحمل معلومات فعلية (كما حال الـ ClickEventArgs في الأعلى ... ) فالـ EventHandler ما هو إلا Generic Delegate يقبل النوع EventArgs أو أحد مشتقاته حصرا ...

فلنستعرض الكود :

public class Point
    {
        public int X {  get; private set; }
        public int Y { get; private set; }
 		internal Point(int x, int y)
        {
            this.X =  x;
            this.Y = y;
        }
    }
    public class  ClickEventArgs : EventArgs
    {
        public Point Position {  get; private set; }
        internal ClickEventArgs(Point position)
 		{
            this.Position = position;
        }
    }
 	public class PseudoButton
    {
        public void  StartSimulatingClicks() // Just ignore this code , to Avoid Disturbance  ...
        {
            new Action(() =>
            {
         		Random rand = new Random();
                while  (true)
                {
             		this.InvokeClickEvent(new Point(rand.Next(0, 300), rand.Next(0, 300)));
             		System.Threading.Thread.Sleep(1500);
         		}
            }).BeginInvoke(null, null);
        }

 		#region Click Event
        private void InvokeClickEvent(Point  position)
        {
            if (this.Click != null)
         		this.Click.Invoke(this, new ClickEventArgs(position));
 		}

        public event EventHandler<ClickEventArgs>  Click;
        #endregion
    }

والإختبار الحاصل بالأعلى للـ Click في كونه Null أم عكس ذلك فقط للتأكد من وجود مشترك وحيد على الأقل قبل المحاولة في إعلام كل المشتركين(لأنك ستواجه NullRefrenceExcpetion في حالة محاولتك لعمل ذلك على حدث خالي من المشتركين ) , نعم كل المشتركين , لأنك بمجرد قيامك بطلب Invoke على أي Event فإنك تُعلِم كل الـ Observers مهما كان عددهم (بترتيب إشتراكهم) , واحدا كان أم أكثر ...

أما الربط بين هذه العناصر يكون كالتالي:

class Program
    {
                static  void Main(string[] args)
        {
            PseudoButton  button = new PseudoButton();
            button.Click += new  EventHandler<ClickEventArgs>(button_Click);//Attach Button_Click  method as an Observer
            button.Click += new  EventHandler<ClickEventArgs>(button_Click2);//Attach Button_Click2  method as an Observer
            button.StartSimulatingClicks();
     		Console.ReadLine();
            button.Click -= new  EventHandler<ClickEventArgs>(button_Click);//Deattach Button_Click  method as an Observer
            Console.ReadLine();
        }

 		static void button_Click(object sender, ClickEventArgs e)
 		{
            Console.WriteLine("Button Clicked ! ({0},{1})",  e.Position.X, e.Position.Y);
        }
        static void  button_Click2(object sender, ClickEventArgs e)
        {
     		Console.ForegroundColor = ConsoleColor.Red;
     		Console.WriteLine("Wow ! ({0},{1})", e.Position.X, e.Position.Y);
     		Console.ForegroundColor = ConsoleColor.Gray;
        }
 	}

نلاحظ أن هناك Two Operators في عملية الإشتراك وفك الإشتراك في أي حدث هما =+ , =- على الترتيب ,وسلوك مهم للغاية ينبغي ملاحظته في أنك لن تستطيع تنفيذ سطر كالتالي :

button.Click  = new EventHandler<ClickEventArgs>(button_Click);

وهو الإسناد المباشر لحدث , فأنت تستطيع إشتراك أو إلغاء إشتراكك , لكنك لن تستطيع تغطية , (إلغاء إشتراكات) كل المشتركين الموجودون من قبل , وبالتالي عملية الإسناد باطلة ولا تجوز منطقيا , ستحصل على Compiler Error عند محاولة تنفيذ ذلك ...

أخيرا , أجد من المتعة فهم الأدوات التي نملكها بعد تأمل الماضي , والماضي هنا هو الحل المتبع في المثال الأول الذي يعج بالـ Types التي لا تفيد بأي شيء , سوا إنها تعقد الأمور , ولكن وجودها ضرورة , لكن مع وجود كلمة Event كـ Keyword فالموضوع أصبح مدعوما من أداة موجودة في صلب اللغة ...

في النهاية يجب الإشارة إلى أن الـ events يجب إفترضاها على أنها مصدر معلومات , لكن يعمل بطريقة مختلفة عن الـ Enumerable

فهناك أبحاث نظرية في الـ Functional Programming تتحدث عن أن الـ Enumerable هو المُعاكس المنطقي للـ Observable فالأول يقوم بإنتاج العناصر (أو المعلومة بالشكل الأعم )عند الطلب , فهنا نقول بأننا نسحب المعلومة (Pulled By the Consumer) , واحدة تلو الأخرى ...

بينما في الـ Observable نقوم بتلقي المعلومة , ونستطيع أن نقول أنها تُرمى في وجهنا ! لأننا لا نعرف وقت صدورها , إن الحدث غير محدد الوقت , فهنا نقول بأن المعلومة تٌقذف لنا (Pushed to the Observers by the Observable) ...

وكملاحظة , هناك إطار عمل جديد أٌعده تحفة فنية في غاية الإتقان المنطقي و الفكري إسمه Reactive Extension مُشرفة عليه Microsoft Research , تقوم فكرته بشكل أساسي على معاملة الأحداث كمصادر معلومات , كالـ Enumerable لكن الفرق الوحيد , أنها مصادر معلومات غير معروفة التوقيت في الصدور ... ربما أكتب في المستقبل عن الـ Rx إن شاء الله ...

مستعد للأسئلة icon_mrgreen.gif

تم تعديل هذه المشاركة بواسطة AliBazzi في 19 سبتمبر 2010 في 15:30

#2

جهد تشكر عليه

لا حبذا لو كان المثال على ال DP مطبق على Business Objects مستخدماً المصطلحات الشائعة Register , UnRegister and Notify حتى تسهل المتابعة لمن يتطرق لل DP للمرة الاولى

نقطى اخرى الا و هى ذكر الفروق بين النمط التصميمى و بين المشابه له ففى حالتنا هذه كان بإمكانك المقارنة بين ال COR و ال Observer Pattern

تم تعديل هذه المشاركة بواسطة طارق إبراهيم في 19 سبتمبر 2010 في 12:28

Technical Lead Developer

My LinkedIn Profile

اللهم قنى شر الجهل و الجهلاء

( اقْتَرَبَ لِلنَّاسِ حِسَابُهُمْ وَهُمْ فِي غَفْلَةٍ مَّعْرِضُونَ ) {الأنبياء:1}

#3
طارق إبراهيم كتب:

جهد تشكر عليه

لا حبذا لو كان المثال على ال DP مطبق على Business Objects مستخدماً المصطلحات الشائعة Register , UnRegister and Notify حتى تسهل المتابعة لمن يتطرق لل DP للمرة الاولى

نقطى اخرى الا و هى ذكر الفروق بين النمط التصميمى و بين المشابه له ففى حالتنا هذه كان بإمكانك المقارنة بين ال COR و ال Observer Pattern

شكرا لك أخي طارق , لكن بصراحة لم أدخل إلى الاّن في مجال الـ Business Objects , إضافة إلى رغبتي في تقديم النمط في شكله التقليدي المعروف كما يقدم عادة في كتب الـ Design Pattern ...

شكرا لك مرة أخرى ...

مواضيع مشابهة