4. Programozási technológiák – Tervezési minták
4.4. Viselkedési tervezési minták
4.4.2. Megfigyelő – Observer
Lehetővé teszi, hogy egy objektum megváltozása esetén értesíteni tudjon tetszőleges más objektumokat anélkül, hogy bármit is tudna róluk. Részei:
1. Alany: Tárolja a beregisztrált megfigyelőket, interfészt kínál a megfigyelők be- és kiregisztrálására valamint értesítésére.
2. Megfigyelő: Interfészt definiál azon objektumok számára, amelyek értesülni szeretnének az alanyban bekövetkezett változásról. Erre a frissít (update) metódus szolgál.
Két fajta megfigyelő megvalósítást ismerünk:
1. „Pull-os” megfigyelő: Ebben az esetben a megfigyelő lehúzza a változásokat az alanytól.
2. „Push-os” megfigyelő: Ebben az esetben az alany odanyomja a változásokat a megfigyelőnek.
A kettő között ott van a különbség, hogy a frissít metódus milyen paramétert kap. Ha az alany átadja önmagát (egy frissít(this) hívás segítségével) a megfigyelőnek, akkor ezen a referencián keresztül a megfigyelő képes lekérdezni a változásokat. Azaz ez a „pull-os” megoldás.
Ha a frissít metódusnak az alany azokat a mezőit adja át, amik megváltoztak és amiket a megfigyelő figyel, akkor „push-os” megoldásról beszélünk. A következő példában épp egy ilyen megvalósítást láthatunk.
4.4.2.1. Forráskód using System;
using System.Collections.Generic;
namespace Observer_Pattern_Weather_Station {
public interface ISubject {
// observer regisztrálásra
void registerObserver(IObserver o);
// observer törlésre
void removeObserver(IObserver o);
// meghívódik, hogy értesítse az megfigyelőket // amikor a Subject állapota megváltozik void notifyObservers();
}
public interface IObserver {
// értékék amiket megkapnak az observerek a Subjecttől, push-os megoldás void update(float temp, float humidity, float pressure);
}
public interface IDisplayElement {
// meghívjuk amikor meg szeretnénk jeleníteni a display elementet void display();
}
// implementáljuk a Subject interfészt public class WeatherData : ISubject {
// hozzáadunk egy listát amiben observereket tárolunk private List<IObserver> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
// létrehozzuk az observereket tároló listát observers = new List<IObserver>();
}
public void registerObserver(IObserver o) {
// amikor egy observer regisztrál, egyszerűen hozzáadjuk a listához observers.Add(o);
}
public void removeObserver(IObserver o) {
// amikor egy observer le akar regisztrálni egyszerűen töröljük a listából int i = observers.IndexOf(o);
if (i >= 0) {
observers.Remove(o);
}
}
// itt szólunk az observereknek az állapotról
// mivel mind observerek, van update() metódusuk, így tudjuk őket értesíteni public void notifyObservers()
{
for (int i = 0; i < observers.Count; i++) {
IObserver observer = (IObserver)observers.ElementAt(i);
observer.update(temperature, humidity, pressure); // ez push-os // observer.update(this); // ez pull-os
} }
// amikor a Weather Station-től megkapjuk a frissített értékeket, //értesítjük az observereket
public void measurementsChanged() {
notifyObservers();
}
// értékek beállítása hogy tesztelhessük a display elementeket
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// egyéb metódusok }
// a display implementálja az Observert,
//így fogadhat változásokat a WeatherData objektumtól // továbbá implementálja a DisplayElement-et, mivel
//minden display element-nek implementálnia kell ezt az interfészt
public class CurrentConditionsDisplay : IObserver, IDisplayElement {
private float temperature;
private float humidity;
private ISubject weatherData;
// a konstruktor megkapja a weatherData objektumot // (a Subject) és arra használjuk, hogy
// a display-t observerként regisztráljuk
public CurrentConditionsDisplay(ISubject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
// amikor az update() meghívódik, mentjük a temperature-t és a humidity-t // majd meghívjuk a display()-t
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
// Megjelenítjük a legújabb eredményeket public void display()
{
Console.WriteLine("Current conditions: " + temperature + "F degrees and " + humidity + "%
humidity");
} }
public class WeatherStation {
static void Main(string[] args) {
// létrehozzuk a weatherData objektumot
WeatherData weatherData = new WeatherData();
// létrehozzuk a displayt és odaajuk neki a weatherData-t
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
// új időjárási mérésértékek szimulálása weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
Console.ReadKey();
} } }
4.4.2.2. UML-ábra
23. ábra
4.4.2.3. Gyakorló feladat
Az alábbi leírás szerint készítsünk forráskódot. A megoldáshoz használjuk a megfigyelő tervezési mintát!
Feladat
A fenti példakódot alakítsuk át „pull-os” megfigyelővé.
Feladat
Cégünk azt a megtisztelő feladatot kapta, hogy kalózhajót kellett programozni, ami több heti kódírás után egész szépen úszott a vízen, egy picike probléma volt csak vele. Mégpedig, hogy a főárboc tetején lévő kosárban ülő őrszemnek nem volt rádiója, hiszen még nem találták fel, ezért minden alkalommal, ha valaki alatta ment el az felkiabált, hogy látja-e már a gazdag zsákmányt, vagy az ellent. Szegény emberünknek úgy kiszáradt a torka, hogy egy hordó rumot kellett délig meginnia. A probléma orvoslására és a rumkészlet megmentésére azt találtuk ki, hogy aki szeretne értesülni a hírekről (observer) az köt egy kötelet a csuklójához, majd a másik végét feldobja (registerObserver()) az őrszemnek (subject). Amikor az őrszem említésre méltót lát, akkor megrángatja a kötelek végét (notifyObservers()), és az összegyűlt „megfigyelőknek” lekiabálja a hírt (update()). Aki nem sablonfüggvény mintát gyakran hasonlítják össze a stratégia mintával az alábbi mondattal:
1. Stratégia: Ugyanazt csináljuk, de másképp;
2. Sablonfüggvény: Ugyanúgy csináljuk, de mást.
A receptben háromféle lépés lehet:
1. kötelező és közös: bármit készítünk a recepttel ez a lépés mindig ugyanaz,
2. kötelező, de nem közös: bármit készítünk a recepttel ez a lépés szükséges, de minden esetben mást és mást kell konkrétan csinálni,
3. opcionális: ez a lépés nem minden esetben szükséges.
Ezeket programozás technikailag így valósíthatjuk meg:
1. A kötelező és közös lépések olyan metódusok, amelyek már az ősben konkrétak és azokat általában nem is szabad felülírni. Ilyen a forró ital főzésénél a vízforraló metódus.
2. A kötelező, de nem közös lépések az ősben absztrakt metódusok, amit a gyermek osztályok fejtenek ki. Ilyen a forró ital főzésénél az édesítés.
3. Az opcionális lépések az ősben hook metódusok, azaz van törzsük, de az üres.
Mivel a gyermek osztálynak implementálnia kell minden absztrakt metódust, ezért az ilyenek kötelezőek. Igaz, hogy akár az implementáció üres is lehet. Mivel a hook metódusoknak van implementációjuk, de üres az üres, ezért nem muszáj őket felülírni, de lehet az OCP elv megszegése nélkül. Ezért ezek az opcionális lépések. A hook metódusokat C# nyelven virtuálisnak kell deklarálni.
Maga a recept a sablonfüggvény. Gyakran csak ez az egy metódus publikus, minden más metódus, azaz a recept lépései privát vagy védett metódusok (a szerint, hogy a gyermeknek felül írhatja-e vagy sem). Erre azért van szükség, hogy az egyes lépéseket ne lehessen össze-vissza sorrendben hívni, csak a recept által rögzített sorrendben.
Elméletben a sablonfüggvény egy algoritmus, amelyben a lépések nem változnak, de a lépések tartalma igen. Ha esetleg mégis bejön egy új lépés, azt érdemes hook metódusnak felvenni.
Érdekes megfigyelni, hogy az absztrakt ős és a gyermek osztályai IOC (inversion of control) viszonyban állnak hasonlóan, mint a gyártófüggvény esetén. Ugyanúgy itt is nem a gyermek hívja az ős metódusait, hanem az ős a gyermekét. Ezt úgy érjük el, hogy a sablonfüggvény absztrakt, illetve virtuális metódusokat hív. Amikor a gyermek osztály példányán keresztül hívjuk majd a sablonfüggvényt, akkor a késői kötés miatt ezen metódusok helyett az őket felülíró gyermek béli metódusok fognak lefutni.