Eventbuss
- Tradisjonell kommunikasjonsflyt mellom objekter
- Kommunikasjonsflyt med eventbuss
- Strukturen i en enkel eventbuss
- Generell 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
.
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:
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:
- Et type
MyEvent
som inneholder informasjon om hendelsen - Et funksjonelt grensesnitt
MyEventHandler
som (en metode i)b
må implementere - En klasse
MyEventBus
som har metodene register(MyEventHandler) og post(MyEvent).
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:
- Vi benytter et grensesnitt
Event
som er uten noen metoder.- Produsent-koden lager fremdeles spesifikke eventtyper (f. eks. MyEvent), men alle slike typer skal være undertyper av Event.
- Vi lar
EventHandler
ogEventBus
forholde seg til typenEvent
i stedet for spesifikke eventtyper. - Konsumenten har ansvar for å sjekke om den mottatte eventtypen har samme type den er ment å reagere på.
Forskjeller vs den enkle eventbussen i forrige avsnitt:
MyEvent
er nå en undertype avEvent
- EventBus og EventHandler benytter typen
Event
i stedet forMyEvent
- 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);
}
}
}