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 , و مستهلك وحيد يقوم بالتصرف بالطريقة المناسبة عند ضغط الزر ...

البروتوكول المتداول في هذا المثال هو الـ 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 , فليكن المثال التالي الذي سيحُل المثال السابق , لكن بطريقة أسهل و "أنظف" :

ما وضح من الشكل السابق هو أن الكثير من الأشياء "الرسمية" التي لا داعي لها ذهبت بالفعل و خف الضجيج المحيط بفكرة حدث وحيد إسمه Click وتحديدا :
نلاحظ أن الـ Click Event والظاهر على أنه Member في الـ Pseudo Button حل محل الـ Add ,Delete ClickEventObserver والذين كانا متواجدين في الحل السابق إضافة إلى إختفاء الـ List التي كانت تحمل الـ Clicks Observers تحت إسم observers , كل هذا بسبب أن event EventHandler<ClickEventArgs> Click قد حلت كل هذه المشاكل من : إضافة , حذف و تخزين الـ Observers المهتمين في حدث Click المَهيب ![]()
ونلاحظ أن الـ 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 إن شاء الله ...
مستعد للأسئلة ![]()