Eventbuss


Tradisjonell kommunikasjonsflyt mellom objekter

Dersom en hendelse (et metodekall, endring i variabel etc.) i et objekt a skal utløse en hendelse i et annet objekt b, er den «enkleste» løsningen at a har b som en instansvariabel, og kaller en metode på b.

Illustrasjon av at a kaller en metode på b

public class Main {
  public static void main(String[] args) {
    B b = new B();
    A a = new A(b);
    a.somethingHappens("Foo", 42);
  }
}

class A {
  // a har b som en instansvariabel
  private B b;

  public A(B b) {
    this.b = b;
  }

  public void somethingHappens(String msg, int num) {
    System.out.println(this + " method called with args: " + msg + ", " + num);
    // a kaller en metode på b
    this.b.doReaction(msg, num);
  }
}

class B {
  private void doReaction(String msg, int num) {
    System.out.println(this + " reacts to args: " + msg + ", " + num);
  }
}

Ulempen med dette er at a da må importere typen til b, og må vite hvilken metode i b som skal kalles. I tillegg må a vite om alle ulike objekter som skal reagere på hendelsen, og kalle på hver av dem dersom det er flere.

Vi kan øke modulariteten ved å ha en eventbuss som mellommann.

Kommunikasjonsflyt med eventbuss

Kommunikasjon via en eventbuss gjør at objektet a hvor hendelsen initielt skjer (kalt produsenten) ikke trenger å vite noen ting om hva konsumenten b er eller hvor mange det er av dem, men forholder seg til omverdenen kun i form av eventbussen. Dette gjør det lettere å skrive kode med høy modularitet; kode som kan utvikles, endres, feilsøkes, byttes ut og testes isolert fra andre deler av koden.

Kommunikasjonsflyten kan illustreres slik:

EventBus som mellommann for at en hendelse i a utløser en hendelse i b

Et event-objekt representerer på en måte argumentene til metodekallet i b, og kan være i noe så enkelt som en record-klasse.

Selv om dette ved første øyekast øker kompleksiten, er det ofte en liten pris å betale – fordelene ved økt modularitet og redusert kompleksiteten i resten av programmet kan være store.

Merk: man kan anse kommunikasjon med eventbuss som en slags måte å kalle metoder på andre objekter, men det er ikke helt det samme: med en eventbuss får du for eksempel ikke noen returverdi på samme måte vi kan få ved et metodekall.

Strukturen i en enkel eventbuss

De mest grunnleggende ingrediensene for å opprette en eventbuss er som følger:

UML-diagram som viser elementene i en forenklet eventbuss

I diagrammet over er klassen A en produsent av hendelser, mens klassen B er en konsument av hendelser.

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

public class Main {
  public static void main(String[] args) {
    MyEventBus eventBus = new MyEventBus();
    A a = new A(eventBus);
    B b = new B(eventBus);

    a.somethingHappens("Foo", 42);
  }
}

// En hendelse er en samling med verdier
record MyEvent(String msg, int num) {}

// Grensesnittet (en metode i) b må implementere 
@FunctionalInterface
interface MyEventHandler {
  void handle(MyEvent event);
}

// Selve EventBus -klassen
class MyEventBus {
  private List<MyEventHandler> eventHandlers = new ArrayList<>();

  public void register(MyEventHandler eventHandler) {
    this.eventHandlers.add(eventHandler);
  }

  public void post(MyEvent event) {
    for (MyEventHandler eventHandler : this.eventHandlers) {
      eventHandler.handle(event);
    }
  }
}

// Eksempel på klasse som produserer hendelser 
class A {
  private MyEventBus eventBus;

  public A(MyEventBus eventBus) {
    this.eventBus = eventBus;
  }

  public void somethingHappens(String msg, int num) {
    System.out.println(this + " method called with args: " + msg + ", " + num);
    this.eventBus.post(new MyEvent(msg, num));
  }
}

// Eksempel på klasse som konsumerer hendelser
class B {
  public B(MyEventBus eventBus) {
    eventBus.register(this::doReaction);
  }

  private void doReaction(MyEvent event) {
    String msg = event.msg();
    int num = event.num();
    System.out.println(this + " reacts to event w/info: " + msg + ", " + num);
  }
}

Generell eventbuss

Eventbussen i forrige avsnitt er laget spesielt for MyEvent-hendelser som inneholder akkurat én String og én int som informasjon; dette gjør at vi ikke kan benytte den med andre typer hendelser uten å måtte skrive en masse kode på nytt. Vi kan heldigvis enkelt modifisere koden slik at eventbussen kan håndtere alle slags ulike typer hendelser med ulik tilhørende informasjon.

Til dette formålet:

UML-diagram som viser elementene i en generell eventbuss

Forskjeller vs den enkle eventbussen i forrige avsnitt:

  • MyEvent er nå en undertype av Event
  • EventBus og EventHandler benytter typen Event i stedet for MyEvent
  • Metoden doReaction i konsumenten sjekker at event’en har den typen metoden ønsker å reagere på.
import java.util.List;
import java.util.ArrayList;

public class Main {
  public static void main(String[] args) {
    EventBus eventBus = new EventBus();
    A a = new A(eventBus);
    B b = new B(eventBus);

    a.somethingHappens("Foo", 42);
  }
}

// Event er et gresesnitt uten noen type, den eneste hensikten er lage et
// knutepunkt i typehierarkiet
interface Event {}

// MyEvent implementerer Event, og er derfor en undertype
record MyEvent(String msg, int num) implements Event {}

// Grensesnittet (en metode i) b må implementere 
@FunctionalInterface
interface EventHandler {
  void handle(Event event);
}

// Selve EventBus -klassen
class EventBus {
  private List<EventHandler> eventHandlers = new ArrayList<>();

  public void register(EventHandler eventHandler) {
    this.eventHandlers.add(eventHandler);
  }

  public void post(Event event) {
    for (EventHandler eventHandler : this.eventHandlers) {
      eventHandler.handle(event);
    }
  }
}

// Eksempel på klasse som produserer hendelser 
class A {
  private EventBus eventBus;

  public A(EventBus eventBus) {
    this.eventBus = eventBus;
  }

  public void somethingHappens(String msg, int num) {
    System.out.println(this + " called on with args: " + msg + ", " + num);
    this.eventBus.post(new MyEvent(msg, num));
  }
}

// Eksempel på klasse som konsumerer hendelser
class B {
  public B(EventBus eventBus) {
    eventBus.register(this::doReaction);
  }

  private void doReaction(Event event) {
    if (event instanceof MyEvent myEvent) {
      String msg = myEvent.msg();
      int num = myEvent.num();
      System.out.println(this + " got event with info: " + msg + ", " + num);
    }
  }
}