The Lone Programmer

Blog o programiranju i dizajniranju aplikacija sa .NET i C# tehnologijama …

Observer pattern vs. .NET event sistem

Posted by Denis Biondic on Juni 28, 2009

Observer pattern je sigurno jedan od najpoznatijih design patterna koji postoje, barem ja imam takav utisak. Prije nego što sam se posvetio materiji design pattern-a, Observer je bio jedini za koji sam mogao reći da poznajem, ili barem sam mislio tako :) Ono što jesam znao, jeste kako napraviti custom event u .NET-u, te kako to iskoristiti za nešto realno.

Ipak, poznato je da svaki developer počne gledati drugačije na polimorfizam i nasljeđivanje jednom kada počne proučavati design pattern-e. Taj slučaj je bio i sa mnom. Ono što sam mislio da je Observer, zapravo je bila veoma pojednostavljena implementacija istog. Pravi Observer pattern (iz GoF knjige, GoF – Gang of Four, poznati naziv za autore najpoznatije Design Pattern knjige – Design Patterns: Elements of Reusable Object-Oriented Software) upotrebljava abstraktne klase / interfejse za implementaciju.

Pitanje koje nakon ovoga nastaje, koja je implementacija bolja? Da li je bolji pristup nasljeđivanja abstraktnih klasa, ili klasične implementacije neodređenog broja event-a unutar klase? Odgovor slijedi …

Observer pattern služi za odvajanje zavisnosti među objektima koji inače trebaju da na neki način komuniciraju jedni sa drugim. Observer omogućava da broj “povezanih” objekata bude proizvoljan, te da se novi objekti mogu dodavati i za vrijeme izvođenja programa (run-time). Ta zavisnost bi na normalan način bila uspostavljena pozivanjem raznih metoda između objekata. Observer izbjegava ovaj pristup, i promoviše loose coupling. Primjeri upotrebe Observer pattern-a obično ispunjavaju uslov da se radi o jednom subjektu i više posmatrača (observera).

Tipični primjer Observer pattern-a, preuzet iz GoF knjige, je dat na slici:

Primjer Observer pattern-a

Primjer Observer pattern-a




Primjer koji ćemo isprogramirati je isto iz GoF knjige, tako da se direktno može vidjeti razlika u implementaciji .NET event verzije. Radi se o abstraktnom primjeru sata, kod kojeg je subjekat promatranja vrijeme, a observeri su razni oblici satova koji prikazuju to vrijeme. GoF implementacija se zasniva na dvije abstraktne klase: Subject i Observer. Ove dvije klase u sebi implementiraju sve potrebne mehanizme za funkcionisanje pattern-a, kao što su Attach() i Detach() metode, kolekcija registrovanih Observer objekata itd… Dijagram je dat na slici:

GoF Observer pattern primjer

GoF Observer pattern primjer sa satom


Za programiranje samog sata ćemo iskoristiti Timer iz System.Timers namespace-a. Započinjemo sa abstraktnim klasama Subject i Observer:


public abstract class Subject
{
    private List<Observer> observers = new List<Observer>();

    public void Attach(Observer o)
    {
        observers.Add(o);
    }

    public void Detach(Observer o)
    {
        if (!observers.Remove(o))
        {
            throw new ArgumentException("Observer not registered!");
        }
    }

    protected void Notify()
    {
        foreach(Observer o in observers)
        {
            o.Update();
        }
    }
}


public abstract class Observer
{
    public abstract void Update();
}

Ove dvije klase omogućavaju svu funkcionalnost Observer pattern-a. U primjeru sata, pojedini satovi su observeri, dok je vrijeme subject:


public class TimeCounter : Subject
{
    private DateTime currentTime;
    private Timer internalClock;

    public TimeCounter(DateTime startTime)
    {
        currentTime = startTime;
        internalClock = new Timer();
        internalClock.Elapsed += internalClock_Tick;
        internalClock.Interval = 500;
        internalClock.Start();
    }

    void internalClock_Tick(object sender, EventArgs e)
    {
        if (DateTime.Now.Second != currentTime.Second)
        {
            currentTime = DateTime.Now;
            Notify();
        }
    }

    public DateTime CurrentTime
    {
        get { return currentTime; }
        set { currentTime = value; }
    }
}


public class SimpleClock : Observer
{
    private TimeCounter time;

    public SimpleClock(TimeCounter time)
    {
        this.time = time;
        time.Attach(this);
    }

    public override void Update()
    {
        Console.WriteLine("Current time: " + time.CurrentTime.TimeOfDay);
    }
}

public class DigitalClock : Observer
{
    private TimeCounter time;

    public DigitalClock(TimeCounter time)
    {
        this.time = time;
        time.Attach(this);
    }

    public override void Update()
    {
        Console.WriteLine("DIGITAL TIME: " + time.CurrentTime.Hour + "." + time.CurrentTime.Minute + "." +
            time.CurrentTime.Second);
    }
}

Korištenje je isto vrlo jednostavno:


TimeCounter time = new TimeCounter(DateTime.Now);
SimpleClock simple = new SimpleClock(time);
DigitalClock digital = new DigitalClock(time);
Console.ReadLine();


Current time: 19:40:41.0228761
DIGITAL TIME: 19.40.41
Current time: 19:40:42.0368761
DIGITAL TIME: 19.40.42
Current time: 19:40:43.0508761
DIGITAL TIME: 19.40.43
Current time: 19:40:44.0658761
DIGITAL TIME: 19.40.44
Current time: 19:40:45.0798761
DIGITAL TIME: 19.40.45

Jedna stvar je veoma specifična za prethodni primjer: svaki observer mora interno znati za subjekat koji promatra. U prethodnom primjeru satovi su morali znati za klasu TimeCounter, jer je to bio jedini način da dobiju novo vrijeme. Ovo u nekim slučajevima može predstavljati problem, većinom zbog nemogućnosti pristupa private memberima konkretne subject implementacije (naravno, ako su isti potrebni u observeru). Ova se situacija može jednostavno rješiti uvođenjem novog objekta u igru, koji će imati sve potrebne informacije za observere, i slati se kroz argument Update() metode. Ovaj postupak koristi upravo .NET event sistem, tako što nalaže da potpis svake event handler metode sadrži EventArgs argument. Novi potrebni argumenti se mogu jednostavno dodati nasljeđivanjem ove klase, i dodavanjem željenih informacija.

Prije implementacije, par riječi o samom .NET event modelu. Pretpostavljam da svi znate šta su delegati, i u biti, šta su eventi. Ovdje samo izlažem neke detalje radi poistovjećivanja sa Observer pattern-om. Uglavnom, .NET event je ugrađeni dio framework-a i C# jezika, i obuhvaća sve prethodno pokazane mogućnosti Observer pattern-a. Interno, jedan event u sebi sadrži kolekciju registriranih delegata (observer-a), i omogućava jednostavno registovanje i de-registrovanje pomoću += i -= operatora. Koristeći orginalni GoF pattern, za ovo se morala praviti posebna klasa!

Implementacija je puno jednostavnija:


// za enkapsuliranje informacija potrebnih observer-ima,
// tako da se dodatno promovise loose-coupling
public class TimeEventArgs : EventArgs
{
    private DateTime currentTime;

    public TimeEventArgs(DateTime currentTime)
    {
        this.currentTime = currentTime;
    }

    public DateTime CurrentTime
    {
        get { return currentTime; }
    }
}


public class TimeCounter
{
    private DateTime currentTime;
    private Timer internalClock;

    public delegate void OnTimeChangeHandler(object o, TimeEventArgs e);
    public event OnTimeChangeHandler TimeChanged;

    public TimeCounter(DateTime startTime)
    {
        this.currentTime = startTime;
        internalClock = new Timer();
        internalClock.Interval = 500;
        internalClock.Elapsed += internalClock_Elapsed;
        internalClock.Start();
    }

    void internalClock_Elapsed(object sender, ElapsedEventArgs e)
    {
        if (DateTime.Now.Second != currentTime.Second)
        {
            if (TimeChanged != null)
                TimeChanged(null, new TimeEventArgs(DateTime.Now));
            currentTime = DateTime.Now;
        }
    }
}


class Clock
{
    public Clock(TimeCounter time)
    {
        time.TimeChanged += time_TimeChanged;
    }

    void time_TimeChanged(object o, TimeEventArgs e)
    {
        Console.WriteLine("Current time: " + e.CurrentTime);
    }
}

Na kraju – zaključak. Odgovor na pitanje s početka teksta je jednostavan: .NET event sistem je samo još jedna, pojednostavljena, implementacija GoF Observer pattern-a. Kreatori framework-a su ovaj pattern posebno uzeli u obzir, te u sam framework ugradili podršlu za isti. Gledajući razlike između implementacija, event primjer je manji, fleksibilniji i čitljiviji; te ujedno ima par veoma važnih prednosti:

  • Objekat se može registrovati samo na “događaje” koje želi posmatrati.
    Recimo da postoji objekat koji ima mnogo potencijalnih događaja na koje se moguće “registrovati”. Recimo da se radi o Button klasi, koja ima metode OnClick(), OnMouseOver() itd… Pošto .NET implementira Observer pattern pomoću event modela, registrovanje na samo neke određene događaje je vrlo jednostavno, i moguće. Ako bi koristili orginalnu GoF implementaciju sa interfejsima/abstraktnim klasama, u observer-ima bi morali implementirati svaku od ovih metoda. Jedini način da se ovo rješi bi bio uvođenje dodatnog “mediator” objekta.
  • Sintaksa event modela je ugrađena u sam framework, te je prema tome svima poznata
  • Lakše održavanje i reusability koda
  • Ako bi željeli dodati novi event, to možemo uraditi bez narušavanja bilo kojeg postojećeg koda. Ako bi to pokušali uraditi u GoF slučaju, morali bi u svakoj Observer klasi implementirati i tu dodanu metodu.

Sve u svemu: .NET events 1 : GoF Observer 0

Ostavi odgovor

XHTML: Možete koristiti sljedeće tagove: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>