Observer


Observer er et mønster man kan organisere koden sin i som er svært nyttig for å oppnå kode med høy modularitet. Man benytter dette mønsteret for å omslutte en variabel som tidvis blir endret, slik at ulike deler av kodebasen kan registrere seg for å få beskjed når variabelen endres uten at den som faktisk utfører endringen trenger å vite hvem det gjelder.

Om å observere verdier

Anta at vi skriver visnings-kode som tegner en grønn prikk på skjermen dersom en variabel i modellen er true, mens vi tegner en rød prikk dersom variabelen er false. Vi ønsker at tegningen vi viser på skjermen alltid er oppdatert basert på hvilken verdi denne variabelen har. Vi ønsker med andre ord å «observere» variabelen.

Vi har to muligheter for å holde tegningen vår oppdatert:

Det siste alternativet vil naturligvis være langt mindre ressurskrevende for prosessoren, og gir oss også mye raskere respons fremfor å måtte vente helt til neste gang vi sjekker verdien. Men hvordan kan vi forsikre oss om at vi får beskjed når variabelen endrer seg? For å løse dette, benytter vi «observer» -mønsteret.

Dersom du ser typer eller metoder som har begrepene observer eller listener som en del av navnet, er det sannsynligvis observer-mønsteret som er tatt i bruk i en eller annen form. I interaktiv grafikk har vi for eksempel sett KeyListener, MouseListener med flere som er varianter av dette.

Klientkode observerer

Observatørkode har til hensikt å observere en variabel og reagere på endringer i den. I dette eksempelet har klienkoden tilgang til en boolsk verdi som skal observeres gjennom et omsluttende ObservableBoolean -objekt.

Kontrollflyt for enkelt observer

I eksempelet under er det koden i klassen MyView som er observatørkoden, mens metoden onXChanged er selve lytteren (altså metoden som blir kalt når den observerte verdien endrer seg).

public class MyView extends JPanel {

  private final ObservableBoolean x;

  public MyView(MyModel model) {
    // ... bla bla bla ...
    this.x = model.getX();
    ValueChangedListener listener = this::onXChanged;
    this.x.addValueChangedListener(listener);
  }

  private void onXChanged() {
    this.repaint();
  }

  @Override
  public static void paintComponent(Graphics g) {
    // ... bla bla bla ...
    boolean actualXValue = this.x.getValue();
    // ... kode som velger farge basert på actualMyBoolValue her ...
    // ... kode som tegner prikk her ...
  }
}

Variabelen som skal observeres i eksempelet over er en boolean. For å få ut selve boolean-verdien, kalles getValue() på det obsluttende ObservableBoolean-objektet (se eksempel i paintComponent over).

Videre ser vi at onMyBoolValueChanged legges til som en lytter på den observerbare verdien i konstruktøren til MyView. Hensikten er at hver gang verdien som observeres endrer seg, skal metoden onMyBoolValueChanged kalles; som i sin tur kaller på repaint. Da vil komponenten tegnes på nytt hver gang verdien som observeres endrer seg.

Klientkode på den andre siden endrer verdien

Et helt annet sted er koden som endrer verdien. I stedet for å endre på variabelen direkte, benytter den set- og get-metoder på det omsluttende ObservableBoolean -objektet for å endre verdi. Legg merke til at variabelen er final, siden det omsluttende objektet i seg selv aldri skal endre seg, kun verdien inni.

public class MyModel {
  private final ObservableBoolean x = new ObservableBoolean(false);

  public ObservableBoolean getX() {
    return this.x;
  }

  public void toggleX() {
    this.x.set(!this.x.getValue());
  }
}

Observer-mønsteret: grunnleggende idé

For å gjennomføre eksempelet vist over, trenger vi to elementer:

@FunctionalInterface
public interface ValueChangedListener {
  void onValueChanged();
}
public class  ObservableBoolean {
  private final List<ValueChangedListener> listeners = new ArrayList<>();
  private boolean value;

  public ObservableBoolean(boolean initialValue) {
    this.value = initialValue;
  }

  public void addValueChangedListener(ValueChangedListener listener) {
    this.listeners.add(listener);
  }

  public boolean getValue() {
    return this.value;
  }

  public void setValue(boolean newValue) {
    this.value = newValue;
    this.notifyListeners();
  }

  private void notifyListeners() {
    for (ValueChangedListener listener : this.listeners) {
      listener.onValueChanged();
    }
  }
}

Legg merke til at eneste måte å endre verdien på, er ved å kalle setValue; og da vil alle de registrerte lytter-metodene bli kalt.

import java.util.List;
import java.util.ArrayList;

@FunctionalInterface
interface ValueChangedListener {
  void onValueChanged();
}

class  ObservableBoolean {
  private final List<ValueChangedListener> listeners = new ArrayList<>();
  private boolean value;

  public ObservableBoolean(boolean initialValue) {
    this.value = initialValue;
  }

  public void addValueChangedListener(ValueChangedListener listener) {
    this.listeners.add(listener);
  }

  public boolean getValue() {
    return this.value;
  }

  public void setValue(boolean newValue) {
    this.value = newValue;
    this.notifyListeners();
  }

  private void notifyListeners() {
    for (ValueChangedListener listener : this.listeners) {
      listener.onValueChanged();
    }
  }
}

public class Main {
  private static ObservableBoolean b = new ObservableBoolean(false);

  public static void main(String[] args) {
    ValueChangedListener listener = Main::ohIThinkTheValueChanged;
    Main.b.addValueChangedListener(listener);
    Main.b.setValue(true);
    Main.b.setValue(false);
    Main.b.setValue(true);
  }

  private static void ohIThinkTheValueChanged() {
    System.out.println("b changed. new value: " + Main.b.getValue());
  }
}

Illustrasjon av programmet under

Et komplett eksempel, men pakket inn i én fil. For å kjøre, kopier innholdet inn i en fil som heter Main.java og kjør kommandoen java Main.java i kommandolinjen (krever en relativt ny Java-versjon, fungerer med Java 17).

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.util.ArrayList;
import java.util.List;

public class Main {
  public static void main(String[] args) {
    MyModel model = new MyModel();
    MyView view = new MyView(model);
    new Timer(1000, (e) -> model.toggleX()).start();

    JFrame frame = new JFrame("INF101");
    frame.setContentPane(view);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }
}

@FunctionalInterface
interface ValueChangedListener {
  void onValueChanged();
}

class ObservableBoolean {
  private final List<ValueChangedListener> listeners = new ArrayList<>();
  private boolean value;

  public ObservableBoolean(boolean initialValue) {
    this.value = initialValue;
  }

  public void addValueChangedListener(ValueChangedListener listener) {
    this.listeners.add(listener);
  }

  public boolean getValue() {
    return this.value;
  }

  public void setValue(boolean newValue) {
    this.value = newValue;
    this.notifyListeners();
  }

  private void notifyListeners() {
    for (ValueChangedListener listener : this.listeners) {
      listener.onValueChanged();
    }
  }
}

class MyModel {
  private final ObservableBoolean x = new ObservableBoolean(false);

  public ObservableBoolean getX() {
    return this.x;
  }

  public void toggleX() {
    this.x.setValue(!this.x.getValue());
  }
}

class MyView extends JPanel {
  private final ObservableBoolean x;

  public MyView(MyModel model) {
    this.setPreferredSize(new Dimension(200, 200));
    this.x = model.getX();
    ValueChangedListener listener = this::onMyBoolValueChanged;
    this.x.addValueChangedListener(listener);
  }

  private void onMyBoolValueChanged() {
    this.repaint();
  }

  @Override
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;

    boolean actualX = this.x.getValue();
    Color color = actualX ? Color.GREEN : Color.RED.darker();
    g2.setColor(color);
    g2.fill(new Ellipse2D.Double(0, 0, this.getWidth(), this.getHeight()));
  }
}

Observer-mønsteret: forbedret utgave

Vår forbedrede utgave som kvittererer ut punktene over, består av tre deler:

Kontrollflyt for forbedret observer

@FunctionalInterface
public interface ValueChangedListener<E> {
  void onValueChanged(ObservableValue<E> source, E newValue, E oldValue);
}
public interface ObservableValue<E> {
  void addValueChangedListener(ValueChangedListener<E> listener);
  boolean removeValueChangedListener(ValueChangedListener<E> listener);
  E getValue();
}
public class ControlledObservableValue<E> implements ObservableValue<E> {
  private final List<ValueChangedListener<E>> listeners = new ArrayList<>();
  private E value;

  public ControlledObservableValue() {
    this(null);
  }

  public ControlledObservableValue(E initialValue) {
    this.value = initialValue;
  }

  @Override
  public void addValueChangedListener(ValueChangedListener<E> listener) {
    this.listeners.add(listener);
  }

  @Override
  public boolean removeValueChangedListener(ValueChangedListener<E> listener) {
    return this.listeners.remove(listener);
  }

  @Override
  public E getValue() {
    return this.value;
  }

  public void setValue(E newValue) {
    if (!Objects.equals(this.value, newValue)) {
      E oldValue = this.value;
      this.value = newValue;
      this.notifyListeners(newValue, oldValue);
    }
  }

  private void notifyListeners(E newValue, E oldValue) {
    for (ValueChangedListener<E> listener : this.listeners) {
      listener.onValueChanged(this, newValue, oldValue);
    }
  }
}