Az observer minta lehetÅ‘vé teszi, hogy egy objektum az állapot változásairól értesÃtsen egy vagy több másik objektumot. Ez alapján hasonlÃt a mediator mintához, de ott egy központi objektum felé irányul az értesÃtés, ami dönt és elintézi annak a továbbÃtását a megfelelÅ‘ objektumok felé. Az observer esetén azonban direkt kommunikáció lehetséges Ãgy.
Arról már volt szó a mediator esetén, hogy ez a fajta kommunikáció káros, ha kiterjedten van alkalmazva az alkalmazásunkban, de ennek a mintának is meg van a helye. Például grafikus alkalmazásfejlesztés során.
Egy ablak létrehoz további gyerek vezérlÅ‘elemeket, például gombokat. Ha a felhasználó a gombra kattint, akkor értesÃtenie kellene az ablakot, hogy kattintás történt rá, hogy az ablakban elhelyezett kód reagálni tudjon arra.
A C# nyelvi megoldása erre az események. Az események és az eseménykezelők a .NET és a C# observer implementációja.
Az események implementálásáról itt külön nem ejtenék szót, mert erről a könyv korábbi részeiben volt szó. Helyette nézzük meg, hogy hogyan is épül fel az observer, ha nem eseményeket alkalmazunk:
public class Observable : IObservable<string>
{
private readonly List<IObserver<string>> _observers;
public Observable()
{
_observers = new List<IObserver<string>>();
}
public IDisposable Subscribe(IObserver<string> observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
return new Unsubscriber(_observers, observer);
}
public void SendNotification()
{
foreach (var observer in _observers)
{
observer.OnNext("Notification");
}
}
private class Unsubscriber : IDisposable
{
private readonly List<IObserver<string>> _observers;
private readonly IObserver<string> _observer;
public Unsubscriber(List<IObserver<string>> observers, IObserver<string> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observers.Contains(_observer))
{
_observer.OnCompleted();
_observers.Remove(_observer);
}
}
}
}
public class Observer : IObserver<string>
{
public void OnCompleted()
{
//ha több esemény nem jön jelzés
}
public void OnError(Exception error)
{
//Ha hiba van
}
public void OnNext(string value)
{
Console.WriteLine(value);
}
}
A megvalósÃtás az IObservable<T> és IObserver<T> interfészekre épül, amelyek beépÃtett elemei a .NET-nek és a System névtérben találhatóak meg. Az IObservable<T> egy megfigyelhetÅ‘ objektum viselkedést definiál. A Subscribe metódusával fogad egy IObserver<T> implementációt, amit egy listában letárolunk. Ennek a metódusnak a visszatérési értékének egy IDisposable implementációnak kell lennie. Erre azért van szükség, mert az értesÃtéseket publikáló IObservable<T> és az értesÃtéseket fogadó IObserver<T> implementációk közösen referálnak egymásra, vagyis amÃg ez a referencia létezik, addig a GC nem tudja Å‘ket felszabadÃtani.
Lényegében a visszatérési érték szerepe itt, hogy a feliratkozott objektum le tudjon iratkozni az értesÃtésekrÅ‘l, ha szeretne. Ezt a megvalósÃtást az Unsubscriber osztály végzi, amit az Observable részeként definiáltam, mert máshol nincs rá szükség.
A megfigyelÅ‘ az Observer osztály. Ennek három fajta értesÃtést is tudunk küldeni a definiált interfészen keresztül. Az OnNext metódus meghÃvódásakor tudhatjuk, hogy az értesÃtés kiküldése közben nem történt semmi probléma. A neve arra utal, hogy az értesÃtéseket előállÃtó objektum a következÅ‘ értesÃtendÅ‘ osztályt értesÃti. Ha az OnError metódus hÃvódik fel, akkor ezzel a megfigyelt jelezheti a megfigyelÅ‘knek, hogy az értesÃtések küldése közben vagy a belsÅ‘ logikájában valami kivétel történt. Az OnCompleted felhÃvásával jelzi a megfigyelt objektum, hogy több értesÃtést nem fog küldeni, a megfigyelÅ‘ nyugodtan leiratkozhat.
Előnyök
- Követi az Open/Closed elvet, mivel egy új feliratkozó bevezetése miatt nem kell módosÃtani a megfigyelt osztályt.
- Az osztályok közötti kapcsolatok futási időben változtathatóak.
Hátrányok
- A feliratkozók a feliratkozókat tároló kollekció sorrendjében értesülnek, ami problémákhoz vezethet.
Observer és az események
Felmerülhet a kérdés, hogy ha az események ugyan ezt valósÃtják meg nyelvi szinten, akkor mégis mi értelme lehet az Observer mintát implementálni C# esetén? A válasz a flexibilitásban rejlik. Az események feliratkozói egy listában tárolódnak és az esemény ellövésekor lényegében a listán végigmenve mindenki értesül. De mi van akkor, ha azt szeretnénk, hogy ne listában tárolódjanak a feliratkozók, hanem egy HashSet-ben, mert el szeretnénk kerülni a dupla feliratkozásokat? Ezt sajna eseményekkel nem tudjuk megvalósÃtani.
Egy másik elÅ‘ny még az úgynevezett weak event pattern, ami nem implementálható eseményekkel. Az observer és az események alap implementációjában a feliratkozó és az eseményeket szolgáltató objektum között 1:1 referencia van, ami azt jelenti, hogy amÃg ez inem szűnk meg, egyik osztályt sem tudja a GC felszabadÃtani, ami végsÅ‘ soron memória szivárgás hibákat eredményezhet az alkalmazásunkban.
A weak event a leiratkozás elmulasztásából akadó hibákat küszöböli ki úgy, hogy a feliratkozókat becsomagolja egy WeakReference tÃpusú osztályba. Ez szintén a System névtéreben található. Ez egy speciális osztály, aminek a Target tulajdonsága mutat annak a konstruktorban kapott osztály referenciájára, amit a GC fel tud szabadÃtani.