{ "year": "1999", "title": "Observer Pattern", "exercisesLink": "opgaver/opgaver.htm", "quote": { "text": "\"I just sit back and observe. You learn more that way\"", "source": "Anonymous" } }
Motivation
Når man opbygger objektsystemer, og distribuerer opgaver og data ud på en række objekter, kan det øge behovet for at opretholde konsistens i systemet. Behovet for konsistens skaber en kobling mellem objekterne. For at begrænse denne koblings skadelige virkning på designet, ønsker vi en løsning der er dynamisk og abstrakt.
Problem
Et eller flere objekters tilstand skal bevares konsistent med et andet objekts tilstand.
Løsning
Observer Pattern kaldes også Publisher-Subscriber Pattern. Den sidste af de to betegnelser har det fået, fordi princippet kan illustreres med at abonnere på en avis.
Der trykkes mange eksemplarer af den samme avis som sendes ud til alle som abonnerer. Avisen indeholder nyheder, der holder læseren up-to-date med det samfund, der er omkring ham.
Udgiveren af avisen har et abstrakt forhold til abonnenterne. Han kender i princippet kun navn og adresse. Abonnenterne modtager avisen fordi de har henvendt sig til udgiveren og bedt om et abonnement. I alt væsentlighed har abonnenten gjort det samme uanset om han har valgt at abonnere på en avis, et tidsskrift eller et nyhedsbrev (f.eks. via email).
I vores designløsning kan vi opnå den samme dynamiske og abstrakte forbindelse mellem publisher (udgiver) og subscriber (abonnent).
I termer af Observer Pattern kalder vi abonnenten Observer og udgiveren Subject. Observer ønsker at holde øje med Subject og abonnere på (modtager besked om) tilstandsændringer i Subject.
Vi kan opdele interaktionen mellem Observer og Subject i tre faser:
1. Observer tilmelder sig hos Subject og beder om at få besked om alle fremtidige tilstandsændringer i Subject.
2. Subject underretter Observer om tilstandsændringer hver gang disse forekommer.
3. Observer framelder sig hos Subject og modtager ikke længere beskeder fra Subject vedrørende tilstandsændringer.
Pkt. 1 og 3 sker på Observers initiativ, mens pkt. 2 sker på Subjects foranledning.
De tre faser anvender hver sin request og det abstrakte forhold mellem Observer og Subject opnåes ved, at de kun kender den del af hinandens interface, der udgøres af disse metoder.
1. Fase
I denne fase kalder Observer metoden addObserverSubject med en reference til sig selv som parameter. På den måde ved Subject hvilket objekt, der ønsker at få besked om fremtidige tilstandsændringer. Subject husker referencen i en collection; hvor den opbevarer referencer til alle dens Observere.
At addObserver tager Observeren som parameter åbner mulighed for at andre kan tilmelde Observeren. Dette er ikke tanken i Observer Pattern, men er naturligvis en mulighed afhængig af situationen.
2. Fase
Her kalder Subject metoden updateObserver med en reference til sig selv. Referencen bruger Observer til at skelne flere Subjects fra hinanden. Hvis Observer har tilmeldt sig hos flere forskellige Subjects har den behov for at kunne skelne dem fra hinanden. Det er nu op til Observer hvad der skal ske.
Der er generelt to teknikker til at opnå viden om den konkrete tilstandsændring: Push og Pull:
Push
Hvis man anvender Push, sender Subject ikke alene en reference til sig selv med som parameter — den sender yderligere en parameter! Den ekstra parameter indeholder oplysninger om tilstandsændringen og er tilstrækkelig til at Observer ikke behøver yderligere informationer. Betegnelsen "Push" kommer af, at Subject skubber (push'er) tilstandsændringen ud til Observer.
Pull
Ved Pull er det Observers egen opgave at hente de ønskede informationer om tilstandsændringen fra Subject. Det sker ved at Observer kalder en eller flere get-metoder der returnerer Subjects tilstand (Vi vil generelt lade en metode kaldet getState repræsentere disse i vores eksempler). Ved at inspicere det returnerede kan Observer fastslå tilstandsændringens karakter. Betegnelsen "Pull" kommer af, at Observer trækker (pull'er) tilstandsændringen fra Subject.
I praksis vil Push og Pull ikke nødvendigvis arbejde med hele Subjects tilstand, men kun den del, der er relevant for at opretholde konsistens i objektsystemet.
Det vil normalt være bekvemt for Subject at have en service-metode fire, der itererer gennem collection'en og kalder update på alle Observerne. Alle steder i Subjects metoder; hvor der sker tilstandsændringer kan man placere et kald af denne fire-metode (med flere på hinanden følgende tilstandsændringer, anvender man naturligvis kun ét kald af fire, efter disse er udført).
3. Fase
Observer framelder sig ved at kalde metoden deleteObserverSubject, med sig selv som parameter. Subject fjerner Observer fra sin collection og den vil ikke længere modtage beskeder om fremtidige tilstandsændringer.
Klassediagram
Vi ser her klassediagrammet for Pull, som er den mest anvendte teknik:
Figur 1:
Klasse-diagram for Pull
Konkret Man bemærker her, at ConObserver typisk må have et konkret kendskab til ConSubject; hvis den selektivt skal kunne hente data fra den. ConObserver må nødvendigvis kende betydningen af de data den henter, mens ConSubject ikke behøver at vide noget om ConObserver (ud over at den er en Observer).
Ikke konkret I diagrammet ovenfor er det indikeret at Subject har referencer til Observer; hvilket rent teknisk ikke kan lade sig gøre, da Subject er et interface. Det er dog gjort alligevel for at understrege, at et Subject ikke behøver et konkret kendskab til Observer'ne.
Flere realiseringer Der vil ofte være flere realiseringer af henholdsvis Subject og Observer interfacene, men for at holde eksemplet simpelt, har vi valgt kun én af hver.
Simpel konsistens I klassediagrammet har vi valgt at lade ConObserver opretholde en kopi af ConSubject's datakerne. Dette er en meget simpel form for konsistens mellem to objekter, og mange mere interessante anvendelser findes naturligvis. Vi har blot valgt denne for eksemplets skyld.
getState i Subject? Man bemærker i eksemplet på update-metodens implementation, at det bliver nødvendigt at caste Subject-referencen til en ConSubject-reference, da man ellers ikke kan kalde getState-metoden. Såfremt getState-metoden havde været erklæret i Subject-interfacet, havde det ikke været nødvendigt, men man plejer at erklære den i realiseringen. Det skyldes at forskellige getState-metoder i diverse realiseringer af Subject-interfacet ikke nødvendigvis returnerer det samme typemæssigt (i dette eksempel en integer).
Interaktion
Fase 1 og 3 er meget enkle og vi vil derfor kun se nærmere på fase 2 — her illustreret med et sekvensdiagram.
Figur 2:
Sekvens-diagram for Pull
Implementation
Observer Pattern er understøttet i Java, men først vil vi se hvordan man kan lave det "manuelt".
De to interfaces er ukompliserede:
 
          public interface Subject {
            public void addObserver(Observer obs);
            public void deleteObserver(Observer obs);
          }
        
          public interface Observer {
            public void update(Subject subject);
          }
        
ConSubject er derimod mere interessant. Specielt spørgsmålet om hvordan man opbevarer referencer til de tilmeldte Observer'e.
Pas på "hjælpsom" IDE Der findes i java.util en klasse der også hedder Observer. Hvis man kopierer indholdet af dette eksempel ind i en IDE, skal man muligvis være opmærksom på, at den ikke fejlagtigt "hjælper" én, ved at tilføje en import af java.util.Observer. Den kan nemlig tage fejl — det er vores egen Observer-klasse vi skal bruge i dette eksempel!
Vi vil her anvende en ArrayList (fra java.util) men man kunne i anvende en hvilket som helst collection der løser opgaven.
 
          import java.util.ArrayList;

          public class ConSubject implements Subject {
            private int state;
            private ArrayList<Observer> observers;

            public ConSubject(int value) {
              observers = new ArrayList<Observer>();
              
              changeState(value);
            }

            public void changeState(int value) {
              state = value;
              
              fire();
            }

            public int getState() {
              return state;
            }

            @Override
            public void addObserver(Observer obs) {
              observers.add(obs);
            }

            @Override
            public void deleteObserver(Observer obs) {
              observers.remove(obs);
            }

            private void fire() {
              for (Observer observer : observers)
                observer.update(this);
            }
          }
        
Bemærk at anvendelsen af changeState er "harmløs" i konstruktoren, da der endnu ikke kan være nogen tilmeldte Observer'e.
Man kunne måske undre sig over hvorfor getState ikke er en del af Subject-interfacet, men det skyldes at returtypen generelt kan variere fra Subject til Subject, og det derfor generelt ikke er hensigtsmæssigt at fastlægge den i det fælles interface.
ConObserver laves som følger:
 
          public class ConObserver implements Observer {
            private int state;
            private String name;

            public ConObserver(Subject subject, String name) {
              this.name = name;
              
              subject.addObserver(this);
            }

            @Override
            public void update(Subject sub) {
              if (sub instanceof ConSubject) {
                state = ((ConSubject) sub).getState();
                System.out.println(name + ": Subject changed to " + state);
              }
            }
          }
        
Man bemærker her, at vi lader ConObserver selv tilmelde sig som Observer hos Subject. Dette er ofte en bekvem løsning.
Vi har valgt at være forsigtige mht. det Subject som vi modtager i metoden update. Vi kontrollerer først om det er et ConSubject før vi anvender det. Dette er blot et eksempel på forsigtighed, som i realiteten er overflødigt, da det ikke kan gå galt, sådan som vores eksempel i øvrigt er opbygget.
Vi har her udvidet ConObserver's tilstand med en teksstreng til identifikation. Det har vi gjort afht. testanvendelsen, hvor det er bekvemt at kunne skelne flere instanser af ConObserver fra hinanden.
I testanvendelsen tilmelder vi tre Observer'e til ét Subject, og foretager en tilstandsændring:
 
          public class Main {

            public static void main(String[] args) {
              ConSubject subject = new ConSubject(3);

              new ConObserver(subject, "Observer A");
              new ConObserver(subject, "Observer B");
              new ConObserver(subject, "Observer C");

              subject.changeState(5);
            }
          }
        
          Observer A: Subject changed to 5
          Observer B: Subject changed to 5
          Observer C: Subject changed to 5
        
Understøttelse i Java
Depricated Java's understøttelse af Observer Pattern er depricated, og bør derfor ikke anvendes i produktionskode. Vi vælger dog at anvende den her, som et lærings-eksempel, da det udbygger vores forståelse af Observer Pattern.
Observer Pattern er i Java understøttet med interfacet Observer og klassen Observable. Disse befinder sig i java.util.
Når man anvender disse klasser får det generelle klassediagram følgende udseende:
Figur 3:
Klasse-diagram med Java's understøttelse
Der er følgende metoder i klassen Observable:
void addObserver( Observer )
Tilmelding af Observer.
void deleteObserver( Observer )
Framelding af Observer.
void deleteObservers()
Framelding af alle Observere.
void notifyObservers( Object )
Hvis tilstanden er ændret (dvs. hvis setChanged er kaldt siden sidst) kaldes update på alle Observer'ne. Object sendes med som den anden parameter til update. Ved hjælp af denne parameter har man mulighed for at lave Push. notifyObservers kalder hasChanged og clearChanged.
void notifyObservers()
Som ovenfor, dog sendes null med som anden parameter til update. Man anvender denne metode når man bruger Pull, og derfor ikke har behov for den ekstra parameter.
int countObservers()
returnerer antallet af Observere.
void setChanged()
Ved kald af denne metode, indikerer man overfor et senere kald af notifyObservers-metoderne, at tilstanden er ændret.
boolean hasChanged()
Returnerer boolsk om tilstanden er ændret og endnu ikke er meddelt Observer'ne. notifyObservers-metoderne kalder denne metode, for at se om de reelt skal kalde updateObserver'ne.
void clearChanged()
Modsat setChanged. notifyObservers-metoderne kalder denne metode for at indikere at Observer'ne er informeret om alle tilstandændringer.
Eksemplet fra vores "manuelle" implementationn af Observer Pattern vil med Java's understøttelse blive følgende:
 
          import java.util.Observable;

          class ConSubject extends Observable {
            private int state;

            public ConSubject(int value) {
              changeState(value);
            }

            public void changeState(int value) {
              state = value;
              
              setChanged();
              notifyObservers();
            }
            
            public int getState() {
              return state;
            }
          }
        
Bemærk, at vi skal kalde både setChanged og notifyObservers for at give besked til Observer'ne.
Samspillet mellem setChanged og notifyObservers gør det muligt at forsinke update-kald til Observer'ne. Hvis vi ofte foretager ændringer af datakernen, men kun ønsker update-kald i specielle situationer, kan vi anvende setChanged hver gang vi ændrer datakernen og nøjes med at kalde notifyObservers i de specielle situationer. Bemærk at de specielle situationer ikke behøver selv at ændre datakernen, da deres update-kald kan hvile på tidligere setChanged-kald.
Hvis man ikke har brug for at forsinke kald af update, kan det være nyttigt at lave en samlet fire-metode:
 
          private void fire() {
            setChanged();
            notifyObservers();
          }
        
ConObserver bliver som følger:
 
          import java.util.Observable;
          import java.util.Observer;

          class ConObserver implements Observer {
            private int state;
            private String name;

            public ConObserver(Observable subject, String name) {
              this.name = name;
              
              subject.addObserver(this);
            }

            public void update(Observable sub, Object obj) {
              if (sub instanceof ConSubject) {
                state = ((ConSubject) sub).getState();
                System.out.println(name + ": Subject changed to " + state);
              }
            }
          }
        
Testanvendelsen er nøjagtig den samme som før; hvilket er tiltalende, hvis man laver refactoring til en anvendelse af Java's understøttelse af Observer Pattern.
Forsinket update
Som det ses af setChanged- og notifyObservers-metoderne er det muligt at markere, at der er sket en ændring, med henblik på senere at gøre observerne opmærksomme på det.
F.eks.:
 
          if (...) {
            tilstandsændringer
            
            setChanged();
          }
            .
            .
          if (...) {
            tilstandsændringer
            
            setChanged();
          }
            .
            .
          notifyObservers(...);
        
Her sker der ændringer; hvis en eller flere af if-sætningerne bliver udført. Ved kald af setChanged-metoden markeres det om der er sket ændringer, og det afsluttende kald af notifyObservers vil være virkningsløst; hvis der ikke er sket ændringer. På den måde bliver eventuelle ændringer samlet, så der ikke sker et kald af notifyObservers; hver gang der måtte ske en ændring.
Dette medvirker til at gøre anvendelsen af Observer Pattern mere effektiv, i det der ikke foretages unødvendig mange update-kald til observerne.