Det er ofte behov for å sikre at et objekt er konsistent med et annet, f.eks. at det som vises i et brukergrensesnittelement stemmer overens med tilstanden til de underliggende objektene i systemet. En god måte å gjøre det på er å bruke observatør-observert-teknikken.

Et observerbart objekt er et objekt som

  • har intern tilstand som kan lese og endres vha. tilgangsmetoder, f.eks. getter- og setter-metoder
  • lar andre objekter registrere seg som interessert i (endringer i) tilstanden og
  • sier fra til de registrerte objektene om endringer

Det første punktet er typisk for data- eller tilstandsorienterte objekter som er riktig innkapslet. De to andre punktene gjør det mulig for andre objekter å reagere på endringer og holde sin egen tilstand konsistent, som er formålet med observatør-observert-teknikken. Som navnet indikerer, så dreier denne teknikken seg om en kobling mellom to objekter: det ene objektet, observatøren, observerer den andre, den observerte. En sier gjerne at observatør og observert er to roller i et objekt-samspill. For at samspillet skal fungere, så må hvert objekt oppføre seg iht. reglene for samspillet.

Observert-rollenObservatør-rollen

Det observerte objektet må være observerbart, dvs. ha metoder for å

  • registrere observatører
  • endre tilstanden sin
  • si fra til observatørene om endringene

I tillegg kan det ofte være lurt å ha en metode for å

  • fjerne observatører

Observatøren må ha metoder for å

  • ta imot beskjed om tilstandsendringene i objektene det observerer

Akkurat når og hvordan observatøren blir registrert hos det observerte objektet er ikke gitt, det vesentlige er hva som skjer etterpå: at observatøren får beskjed om tilstandsendringer. 

Eksempel: Vintersportssted og pudderalarm

For å gjøre teknikken konkret, så tenker vi oss at et vintersportssted, f.eks. Oppdal, tilbyr seg å sende ut varsel om større mengder nysnø, såkalt pudderalarm, slik at skientusiastiske studenter (og faglærere) vet når de skal prioritere løssnøkjøring fremfor forelesninger. Her fungerer vintersportsstedet som observert og skientusiasten som observatør. For å implementere dette definerer vi derfor klassene Vintersportssted og Person. I tillegg, og dette er viktig, så defineres grensesnittet Pudderalarmlytter, som fanger opp det å være interessert i pudderalarm. Dette gjør koblingen mellom Vintersportssted og Person litt løsere og åpner for at andre klasser, f.eks. Rutebilselskap også kan vise sin interesse for pudderalarm og sette opp ekstrabusser til det aktuelle vintersportsstedet.

Pudderalarmlyttervoid pudderalarm(Vintersportssted)Vintersportsstedvoid addPudderalarmlytter(Pudderalarmlytter)void removePudderalarmlytter(Pudderalarmlytter)void firePudderalarm()PersonRutebilselskappudderalarmlyttere*

Diagrammet til venstre viser hvordan klassene henger sammen og hvilke metoder de implementerer.

Pudderalarmlytter-grensesnittet definerer én metode, som kalles når pudderalarmen går. Den kalles av Vintersportssted og implementeres av klassene som ønsker å reagere på pudder-tilstanden.

Vintersportssted har to metoder for å håndtere lytterne: én for å legge til og én for å fjerne Pudderalarmlytter-e. I tillegg har den en intern metode for å si fra til lytterne, som må kalles når pudder-tilstanden inntreffer. En slik metode er ikke påkrevd, men gir ryddigere kode når det er flere metoder som endrer tilstanden det skal varsles om.

Person og Rutebilselskap er klasser som ønsker å reagere på pudderalarmen, og derfor må de implementere Pudderalarmlytter-grensesnittet. Dette krever at de implementerer pudderalarm-metoden, men akkurat hva de gjør er opp til dem. Rutebilselskap kan f.eks. endre på rute- og bemanningsplanen for å gi rom for ekstrabusser og Person kan f.eks. kanselere alle avtaler og bestille bussbillett. Merk at pudderalarm-metoden får det aktuelle Vintersportssted-et som argument, slik observatørene (lytterne) kan reagere riktig. Rutebilselskap-et må f.eks. sette opp ekstrabusser til riktig sted og Person må bestille riktig bussbillett.

Koden nedenfor er en standard implementasjon av tilfellet som er beskrevet her.

En mer abstrakt illustrasjon av teknikken er gitt som eksempel på arv i klassediagrammer.

 
class Vintersportssted {

	...
	// felt og metoder for å håndtere vær- og føre-tilstanden
	// endringsmetoder må kalle firePudderalarm() når pudder-tilstanden inntreffer
	...

	private Collection<Pudderalarmlytter> pudderalarmlyttere = new ArrayList<Pudderalarmlytter>();

	public void addPudderalarmlytter(Pudderalarmlytter pudderalarmlytter) {
		pudderalarmlyttere.add(pudderalarmlytter);
	}

	public void removePudderalarmlytter(Pudderalarmlytter pudderalarmlytter) {
		pudderalarmlyttere.remove(pudderalarmlytter);
	}

	private void firePudderalarm() {
		for (Pudderalarmlytter pudderalarmlytter: pudderalarmlyttere) {
			pudderalarmlytter.pudderalarm(this);
		}
	}
}
interface Pudderalarmlytter {
	public void pudderalarm(Vintersportssted);
}

class Person implements Pudderalarmlytter {
	...
	@Override
	public void pudderalarm(Vintersportssted vintersportssted) {
		// kanselere avtaler og bestille buss til vintersportssted
	}
}

class Rutebilselskap implements Pudderalarmlytter {
	...
	@Override
	public void pudderalarm(Vintersportssted vintersportssted) {
		// sette opp ekstrabuss til vintersportssted
	}
}

Variasjoner i teknikken

Dette mønsteret er nyttig i mange sammenhenger og gjentar seg med noen variasjoner.

For observatøren er variasjonene knyttet til hvor kompleks tilstanden det varsles om er. I tilfellet over er tilstanden enten-eller, altså tilsvarende en logisk verdi, og dette er det enkleste tilfellet som kan forekomme. Men med mer kompleks tilstand, så vil også teknikken endres:

  • Metoden som er definert i observatør/lytter-grensesnittet kan ha flere og mer komplekse parametre. For tilstand som er lagret som i felt, er det ikke uvanlig å ha før- og etter-tilstanden som egne parametre, i tillegg til objektet som ble endret. Denne varianten finnes f.eks. i JavaFX sitt ObservableValue-grensesnitt. En annen variant er å ha ett parameter, men dette er et såkalt hendelsesobjekt, som (inne)holder data om selve endringen, inkludert objektet som ble endret og detaljer om tilstanden. Dette er typisk for GUI-komponenter, som putter informasjon om hva brukeren har gjort i objekter som arver fra java sin EventObject-klasse.
  • Observatør/lytter-grensesnittet kan ha flere metoder, for å si fra om ulike tilfeller/tilstander. Dette kan gjøre koden i klassene som implementerer grensesnittet ryddigere, fordi ulike tilfeller håndteres av ulike metoder. Denne varianten finner en f.eks. i Java Swing sitt MouseListener-grensesnitt, som har separate metoder for å varsle om at brukeren trykker (mousePressed) og slipper (mouseReleased) musknappene.
  • Observatør-rollen er noen ganger fordelt på flere grensesnitt. Dette gjøres når tilstanden det varsles om er kompleks, og en regner med at ulike klasser er interessert i ulike tilfeller og derfor vil finne det greiere å implementere et spesifikt grensesnitt med færre metoder. Denne varianten finner en f.eks. i Java Swing som har ulike grensenitt for musknapper (MouseListener) og musbevegelser (MouseMotionListener).

For den observerte er det færre variasjoner, men de som for observatøren også knyttet til kompleksiteten til tilstanden:

  • Når det skal varsle til flere lytter-metoder, så har en gjerne flere fire-metoder.
  • Når en skal varsle til flere lytter-grensesnitt, så har en gjerne flere lytterlister og add/remove-metoder. Alternativet er å ha et grensesnitt som de spesifikke grensesnittene arver fra, men dette gjør klassehierarkiet mer komplekst og bør unngås.

 

SidetypeFerdigDekningsgradOmfang
teori757575