https://visualhunt.com/photo/211711/

Wzorce projektowe – Obserwator

Wzorzec projektowy Obserwator należy do wzorców behawioralnych, czyli czynnościowych. Ta grupa obejmuje wzorce związane z zachowaniem obiektów oraz ich wzajemnymi odpowiedzialnościami.

W Obserwatorze istnieje obiekt, którego stan może się zmieniać (Subject) oraz grupa obiektów śledzących te modyfikacje (Observer). Z tego opisu wyłania się coś na kształt relacji jeden do wielu, przy czym zależność ta jest luźna. Oznacza to, że podmiot (obiekt obserwowany) nie wie zbyt dużo o swoich obserwatorach. W zasadzie jedyna informacja jaką posiada, to wiedza na temat typów obiektów śledzących zmiany. Nie są to obiekty dokładnie tej samej klasy, lecz mogą być dziedziczące ze wspólnej klasy bazowej lub implementujące ten sam interfejs. Takie podejście umożliwia zastosowanie jednakowego sposobu komunikacji pomiędzy podmiotem, a różnymi obserwatorami.

Ogólna idea jest taka, że obiekt obserwowany przechowuje listę zarejestrowanych obserwatorów, do których wysyła informacje o zmianach swojego stanu. Istnieją różne warianty wzorca. W jednym z nich (tak jak w moim przykładzie) podmiot informuje swoich obserwatorów o zmianach stanu, jednocześnie go przesyłając. Obiekt obserwowany może również udostępniać publiczne metody do pobierania danych co może znaleźć zastosowanie, gdy obserwator nie potrzebuje koniecznie całego pakietu danych, lecz jedynie niewielki fragment. Obiekty śledzące mogą przechowywać stan podmiotu (jak w moim przykładzie w prywatnych polach klasy), jak również mogą posiadać referencję do samego podmiotu, co umożliwi pobranie tylko wybranych danych wewnątrz metody update(). W takiej sytuacji źródło danych może wywołać metodę update() na rzecz obserwatorów bez żadnego argumentu (nie przekazując danych), jedynie informując o zmianach, po czym obserwator pobierze z podmiotu te dane, które go interesują

Po tym wstępie, czas na przykład, w którym wykorzystam część kodu przygotowaną w ramach poprzedniego wpisu, gdzie zaimplementowałem realizację odczytu temperatury z czujnika DS1820.

Podmiot, czyli obiekt obserwowany

Obiektem obserwowanym w moim przykładzie będzie klasa realizująca odczyt temperatury z czujnika (DS1820Reader). W ramach implementacji wzorca utworzyłem interfejs TemperatureProvider, który zapewnia istnienie metod do jego obsługi – dodawania i usuwania obserwatorów (addTemperatureObserver() i deleteTemperatureObserver()) oraz informowania o zmianach (notifyObservers()). Metoda read() inicjuje uruchomienie odczytu z sensora i nie wchodzi bezpośrednio w implementację wzorca. W zasadzie można pomyśleć o wydzielenie tej metody do oddzielnego interfejsu bądź klasy.

interface TemperatureProvider {
 
    public void addTemperatureObserver(TemperatureObserver temperatureObserver);
 
    public void deleteTemperatureObserver(TemperatureObserver temperatureObserver);
 
    public void notifyObservers();
 
    public List<Measurement> read();
 
}

Tak jak wspominałem wcześniej, korzystam z fragmentów kodu opisanych poprzednio, a zamieszczonych na GitHub. Poniżej zamieszczam jedynie nowe elementy związane bezpośrednio z zastosowaniem wzorca. Dane pomiarowe przechowywane są w prywatnym polu List<Measurement> measurements, natomiast zarejestrowani obserwatorzy w List<TemperatureObserver> temperatureObservers. Implementację metody addTemperatureObserver() można wyposażyć w dodatkową logikę, której zadaniem może być m. in. sprawdzenie poprawności przekazanego obiektu obserwatora. W notifyObservers() znajduje się najistotniejsza część wzorca, czyli poinformowanie odbiorców o modyfikacjach podmiotu.

@Data
public class DS1820Reader implements TemperatureProvider {
 
    private List<Measurement> measurements;
    private List<TemperatureObserver> temperatureObservers;
 
    public DS1820Reader() {
        this.temperatureObservers = new ArrayList<>();
        this.measurements = new ArrayList<>();
    }
 
    @Override
    public void addTemperatureObserver(TemperatureObserver temperatureObserver) {
        this.temperatureObservers.add(temperatureObserver);
    }
 
    @Override
    public void deleteTemperatureObserver(TemperatureObserver temperatureObserver) {
        int index = this.temperatureObservers.indexOf(temperatureObserver);
        if (index > 0) {
            this.temperatureObservers.remove(index);
        }
    }
 
    @Override
    public void notifyObservers() {
        this.temperatureObservers.forEach(observer -> observer.update(this.measurements));
    }
 
    @Override
    public List<Measurement> read() {
    // (...)
    notifyObservers();
    // (...)
    }
    // pozostałe metody prywatne bez zmian
}

Obiekty obserwujące

W dalszej kolejności należy stworzyć gapiów, którzy implementują interfejs TemperatureObserver. Najistotniejszą metodą i w zasadzie jedyną należącą do wzorca jest metoda update(), która wykorzystywana jest przez podmiot do informowania/aktualizowania obiektów śledzących. W przypadku tego interfejsu skorzystałem z możliwości jakie daje Java 8 w postaci domyślnych metod, co ograniczyło powtórzenia w kodzie, bo ich implementacje były w zasadzie identyczne dla wszystkich klas obserwatorów.

public interface TemperatureObserver {
 
    public void update(List<Measurement> measurements);
 
    default public List<Measurement> createLocalCopy(List<Measurement> measurements) {
        return measurements.stream().map(m -> new Measurement(m)).collect(Collectors.toList());
    }
 
    default public void changeUnit(List<Measurement> measurements, TemperatureUnitEnum destUnit) {
        measurements.forEach((m) -> {
            m.setUnit(destUnit);
            float celsiusValue = m.getValue();
            m.setValue(convert(celsiusValue));
        });
    }
 
    public float convert(float value);
}

Metoda createLocalCopy() realizuje głębokie kopiowanie wartości danych pomiarowych (listy obiektów typu Measurement), natomiast changeUnit() zmienia jednostkę temperatury w przekazanej kolekcji ze stopni Celsjusza na zgodną z parametrem destUnit. Obie metody nie wchodzą bezpośrednio w implementację wzorca, służą raczej do realizacji kontekstu.

Przygotowałem trzy klasy obserwatorów, których logika realizuje ewentualne przeliczanie i wyświetlanie odczytanej z czujnika temperatury, w różnych jednostkach ([°C], [°F] lub [K]), odpowiednio: CelsiusObserver, FahrenheitObserver oraz KelvinObserver.

public class KelvinObserver implements TemperatureObserver, TemperaturePrinter  {
 
    private List<Measurement> measurements = new ArrayList<>();
 
    @Override
    public void update(List<Measurement> measurements) {
        this.measurements = createLocalCopy(measurements);
        changeUnit(this.measurements, TemperatureUnitEnum.K);
        printValues(this.measurements);
    }
 
    @Override
    public float convert(float value) {
        float result = value + 273.15f;
        return (float) (Math.round(result * 100.0) / 100.0);
    }
}
 
public class CelsiusObserver implements TemperatureObserver, TemperaturePrinter  {
 
    private List<Measurement> measurements = new ArrayList<>();
 
    @Override
    public void update(List<Measurement> measurements) {
        this.measurements = measurements.stream().map(m -> new Measurement(m)).collect(Collectors.toList());
        this.printValues(this.measurements);
    }
 
    @Override
    public float convert(float value) {
        return (float) (Math.round(value * 100.0) / 100.0);
    }
}
 
public class FahrenheitObserver implements TemperatureObserver, TemperaturePrinter  {
 
    private List<Measurement> measurements = new ArrayList<>();
 
    @Override
    public void update(List<Measurement> measurements) {
        this.measurements = createLocalCopy(measurements);
        changeUnit(this.measurements, TemperatureUnitEnum.F);
        printValues(this.measurements);
    }
 
    @Override
    public float convert(float value) {
        float result = (value * 1.8f) + 32;
        return (float) (Math.round(result * 100.0) / 100.0);
    }
}

Podczas ostatniego przeglądu kodu przed opublikowaniem posta, wydzieliłem oddzielny interfejs TemperaturePrinter, zawierający domyślną metodę odpowiedzialną za wyświetlanie wyniku pomiarów.

public interface TemperaturePrinter {
 
    default public void printValues(List<Measurement> measurements) {
        measurements.stream().forEach(System.out::println);
    }
}

Działanie w akcji

Tak z grubsza wygląda wzorzec obserwatora, pozostało zweryfikować kod w działaniu.

Pozwolę dodać w tym miejscu przypomnienie dla siebie – jeśli nie można odczytać informacji z czujnika, sprawdź czy są nadane odpowiednie uprawnienia! 🙂

sudo chmod a+rw /dev/ttyUSB0

Funkcja main() uruchamiająca odczyt wygląda niezbyt skomplikowanie. W pierwszej kolejności tworzone są trzy obiekty obserwatorów (celsiusObserver, kelvinObserver oraz fahrenheitObserver) . Następnie kreowany jest podmiot (obiekt dostarczający dane z czujników), w którym rejestrowane są obiekty śledzące. Ich działanie widoczne jest dopiero po wywołaniu metody read() na obiekcie reader.

 public static void main(String args[]) {
        System.out.println("Wzorzec projektowy Obserwator");
        TemperatureObserver celsiusObserver = new CelsiusObserver();
        TemperatureObserver kelvinObserver = new KelvinObserver();
        TemperatureObserver fahrenheitObserver = new FahrenheitObserver();
 
        TemperatureProvider reader = new DS1820Reader();
        reader.addTemperatureObserver(celsiusObserver);
        reader.addTemperatureObserver(fahrenheitObserver);
        reader.addTemperatureObserver(kelvinObserver);
 
        List<Measurement> measurements = reader.read();
        System.out.println("\nOdczytana wartość:");
        measurements.forEach(System.out::println);
    }

Na wyjściu programu odczytujemy wartości temperatury w trzech różnych jednostkach, co fajnie obrazuje działanie wzorca.

Wzorzec projektowy Obserwator
2017-06-20 16:58:47 (1086F714010800BF): 27.81 °C
2017-06-20 16:58:47 (1086F714010800BF): 82.06 °F
2017-06-20 16:58:47 (1086F714010800BF): 300.96 K
 
Odczytana wartość:
2017-06-20 16:58:47 (1086F714010800BF): 27.81 °C

Literatura obowiązkowa:

Wzorce projektowe
Java SE 8 Date and Time
Default Methods
Wzorce projektowe. Rusz głową! – jak dla mnie książka na prawdę godna polecenia

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *