Liigu peamise sisu juurde

Observer

Sissejuhatus

Enamus süsteemides on mõned objektid loodud oleku jälgimiseks ja edastamiseks, samas nendele vastavad ka objektid, mis reageerivad antud olekule. Naiivne lähenemine sellist süsteemi luua oleks panna allikas sõltuma otse enda jägijast, mis tekitab koheselt tugeva seose nende klasside vahel. Antud lähenemise puhul peab allikas teadma kõiki oma terbijaid, kutsuma igaüht eraldi ja muutuma igakord, kui mõni tarbija lisatakse või eemaldatakse.

Vaatleja (observer) muster katkestab selle sõltuvuse. Antud muster koosneb reeglina kahest peamisest komponendist - subjektist ja vaatlejast. Allikas - mida nimetatakse subjektiks - hoiab endas nimekirja osapooltest - vaatlejatest. Subjekt teavitab vaatlejaid, vaatlejad reageerivad muudatustele. Selle mustri peamine eelis on, et subjekt ei pea teadma, kes vaatlejad on või mida nad saadud teatega peale hakkavad.

Probleemist lähemalt

Antud probleem on eriti hästi nähtav reaalajasüsteemides. Võtame näiteks ilmajaama, mis mõõdab temperatuuri ja õhuniiskust. Nendest andmetest sõltuvad mitmed erinevad süsteemid, nagu fuajee ekraanid, mobiilirakendused ja hoiatusteenused. Kõik need reageerivad saadud andmetele erineval viisil.

Ilma vaatlejamustrita peab ilmajaam kõigist neist teadlik olema:

public class WeatherStation {
private double temperature;
private double humidity;

private LobbyDisplay lobbyDisplay;
private MobileApp mobileApp;
private AlertService alertService;

public void setReadings(double temperature, double humidity) {
this.temperature = temperature;
this.humidity = humidity;

lobbyDisplay.update(temperature, humidity);
mobileApp.update(temperature, humidity);
alertService.checkConditions(temperature, humidity);
}
}

Sellise lahendusega kaasneb mitu probleemi:

  • WeatherStation on otseselt seotud iga vaatlejaga. Uue vaatleja lisamine - näiteks andmelogimine - nõuab WeatherStation-i muutmist.
  • Samuti tähendab vaatleja eemaldamine eeldab WeatherStation-i muutmist.
  • WeatherStation on teadlik sellest, mida iga vaatleja teeb ja kuidas neid välja peab kutsuma.

Vaatlejamuster

Nagu eelnevalt mainitud koosneb antud muster kahest komponendist:

  • Subjekt (antud näites WeatherStation), mis hoiab endas olekut ning nimekirja vaatlejatest.
  • Vaatleja, mis võtab subjektist tulevaid teavitusi vastu ja reageerib nendele endale sobival viisil.

Mustri rakendamiseks peab esmalt looma liidese, mida kõik vaatlejad teostavad:

public interface WeatherObserver {
void onReadingsChanged(double temperature, double humidity);
}

Subjekt hoiab endas nimekirja vaatlejatest. Teavituse välja saatmiseks kasutab liidesest tulenevat meetodit:

public class WeatherStation {
private double temperature;
private double humidity;

private final List<WeatherObserver> observers = new ArrayList<>();

public void addObserver(WeatherObserver observer) {
observers.add(observer);
}

public void removeObserver(WeatherObserver observer) {
observers.remove(observer);
}

public void setReadings(double temperature, double humidity) {
this.temperature = temperature;
this.humidity = humidity;
notifyObservers();
}

private void notifyObservers() {
for (WeatherObserver observer : observers) {
observer.onReadingsChanged(temperature, humidity);
}
}
}

Iga vaatleja teostab liidest endale sobival viisil:

public class LobbyDisplay implements WeatherObserver {
@Override
public void onReadingsChanged(double temperature, double humidity) {
System.out.printf("Lobby display: %.1f°C, %.0f%% humidity%n",
temperature, humidity);
}
}
public class AlertService implements WeatherObserver {
private static final double HEAT_INDEX_THRESHOLD = 35.0;

@Override
public void onReadingsChanged(double temperature, double humidity) {
double heatIndex = calculateHeatIndex(temperature, humidity);
if (heatIndex > HEAT_INDEX_THRESHOLD) {
System.out.println("ALERT: Dangerous heat index detected!");
}
}

private double calculateHeatIndex(double temp, double humidity) {
// simplified heat index calculation
return temp + (humidity / 100.0) * 5;
}
}

Kõikide osade kokkupanek ja kasutamine käiks järgnevalt:

WeatherStation station = new WeatherStation();

station.addObserver(new LobbyDisplay());
station.addObserver(new MobileApp());
station.addObserver(new AlertService());

station.setReadings(32.0, 80.0); // all three observers are notified automatically

Teavituste väljasaatmisvoog - subjekt saadab sama teavituse välja kõikidele vaatlejatele:

Pane tähele mis muutus:

  • WeatherStation ei ole enam teadlik konkreetsestest vaatlejatest. See on ainult teadlik, et mingid vaatlejad eksisteerivad ning need teostavad WeatherObserver liidest.
  • Uue vaatleja lisamiseks tuleb luua uus klass, mis teostab vastavad liidest, ning siis tuleb välja kutsuda addObserver(...) meetod.
  • Igat vaatlejat saab iseseisvalt testida, ilma et peaks looma konkreetset WeatherStation objekti.

Push vs Pull

Eelnevalt toodud näites edastab subjekt andmed vaatlejatele, lisades väärtused otse teavitusse (push). Alternatiivina võib edastada ainult viite subjektile endale, lastes igal vaatlejal vajaliku info ise välja valida subjektist (pull).

Näiteks:

public interface WeatherObserver {
void onReadingsChanged(WeatherStation station);
}
public class LobbyDisplay implements WeatherObserver {
@Override
public void onReadingsChanged(WeatherStation station) {
System.out.printf("%.1f°C%n", station.getTemperature());
// only reads temperature, ignores humidity
}
}

Push-lähenemine on lihtsam ja sobib hästi olukorras, kus kõik vaatlejad vajavad samu andmeid. Pull-lähenemine on paindlikum olukordades, kus erinevad vaatlejad on huvitatud subjekti oleku eri osadest. Samuti väldib selline lähenemine liidese muutmist iga kord, kui subjektile lisandub uus väli.

Kasutusjuhud

Vaatlejamuster sobib, kui

  • Ühe objekti oleku muutus nõuab teadmata või muutliku arvu teiste objektide teavitamist.
  • Soovid lisada ja eemaldada vaatlejaid käigu pealt ilma allikat muutmata.
  • Vaatlejad peaksid saama reageerida iseseisvalt ega pea üksteisest teadma.
  • Subjekt peaks jääma sõltumatuks konkreetsetest klassidest, mis temast sõltuvad.
nõuanne

Vaatlejamuster on üks lihtsamaid näiteid sündmuspõhisest (event-driven) arhitektuurist. Sama põhiidee - sündmuse tekkimine ja sellele reageerimine - esineb ka laiemates süsteemides: kasutajaliideste sündmuste käsitlemisel ning sõnumivahendajates nagu RabbitMQ või Kafka, kus üks tootja (publisher) saadab sõnumeid ja mitu tarbijat (subscriber) reageerivad neile iseseisvalt. Keerukamates lahendustes lisanduvad asünkroonsus, hajutatus ja töökindlus, kuid aluspõhimõte jääb samaks.