17.1 Proxy 274
17.1.1 Simpelt eksempel: En stak, der logger kaldene 274
17.1.2 Variationer af designmønstret Proxy 275
17.1.3 Eksempel: Gøre data uforanderlige vha. Proxy 276
17.1.4 Doven Initialisering/Virtuel Proxy 277
17.1.5 Eksempel på Virtuel Proxy: En stak der først oprettes, når den skal bruges 277
17.2 Adapter 278
17.2.1 Simpelt eksempel 278
17.2.2 Anonyme klasser som adaptere 279
17.2.3 Anonyme adaptere til at lytte efter hændelser 280
17.2.4 Eksempel: Få data til at passe ind i en JTable 281
17.2.5 Ikke-eksempel: Adapter-klasserne 282
17.3 Iterator 283
17.3.1 Iteratorer i Collections-klasserne 283
17.3.2 Definere sin egen form for iterator 284
17.3.3 Iteratorer i JDBC 284
17.3.4 Iterator til at gennemløbe geometriske figurer 285
17.4 Facade 286
17.4.1 Eksempel: URL 286
17.4.2 Eksempel: Socket og ServerSocket 286
17.5 Observatør/Lytter 287
17.5.1 Eksempel: Hændelser 287
17.5.2 Eksempel: Kalender 288
17.6 Dynamisk Binding 289
17.6.1 JDBC og Dynamisk Binding 290
17.6.2 Eksempel: Fortolkning af matematikfunktioner 292
17.7 Opgaver 293
17.8 Løsninger 294
17.8.1 Dataforbindelseslogger 294
17.8.2 Designmønstre i JDBC 294
Det er en god idé at have kigget i afsnit 1.1, Lister og mængder, kapitel 8, Databaser (JDBC), afsnit 15.9, Introduktion til designmønstre og kapitel 16, Skabende designmønstre, før man læser dette kapitel.
I dette kapitel vil vi beskrive nogle af de designmønstre, der ofte ses anvendt i i standardbiblioteket og i lidt større programmer.
Den overordnede idé i mange af designmønstrene er, som diskuteret i afsnit 15.9, at give idéer til, hvordan man afkobler (dvs. mindsker graden af bindinger) mellem den del af programmet, som bruger nogle objekter (kaldet klienten), og den del af programmet (dvs. de klasser), der bruges.
Problem: Et objekt, der bliver brugt af klienten, skal nogen gange bruges lidt anderledes, men ikke altid, så det er uhensigtsmæssigt at ændre i klassen eller i klienten.
Løsning: Lav en Proxy-klasse, der lader som om, den er det rigtige objekt, og kalder videre i det rigtige objekt.
Få metodekald til et objekt til at gå igennem et Proxy-objekt (mellem-objekt), der modtager metodekald på vegne af (fungerer som en erstatning) for det rigtige objekt
Proxy kunne på dansk hedde "stråmand" eller "mellemmand". Ordet brugtes oprindeligt i banksektoren, men de fleste kender i dag kun ordet i forbindelse med internettet: Har man ikke direkte forbindelse til internettet kan det være nødvendigt at konfigurere proxy-indstillingerne i sin netlæser, sådan at den sender forespørgslerne til en proxy-server, der spørger videre ud på internettet.
Oftest ved klienten ikke at den bruger en proxy. Når proxyen bliver kaldt, vil den som regel delegere kaldet videre til det andet objekt, men den kan også vælge f.eks.:
at returnere med det samme og udføre kaldet i baggrunden
at afvise kaldet (f.eks. ved at kaste en undtagelse)
at udføre kaldet på en anden måde (f.eks. anderledes parametre)
I stak-eksemplet fra afsnit 15.3.2 kunne det måske være rart under programudviklingen at logge de metodekald, der bliver foretaget på stakken.
Vi kunne da lave en Proxy til stakken, der udskriver kaldene:
public class Staklogger implements Stak { private StakMedNedarving2 rigtigeStak; public Staklogger(StakMedNedarving2 s) { rigtigeStak = s; } public void lægPå(Object o) { System.out.print("Staklogger: lægPå("+o+")"); rigtigeStak.lægPå(o); } public Object tagAf() { Object o = rigtigeStak.tagAf(); System.out.print("Staklogger: tagAf() gav: "+o); return o; } }
Der, hvor vi opretter objektet, pakker vi det rigtige Stak-objekt ind i vores Staklogger:
Stak s = new Staklogger( new StakMedNedarving2() );
Staklogger vil nu blive brugt på vegne af (i stedet for) StakMedNedarving2, uden at klienten (resten af programmet) ved det.
Staklogger er en Stak, der delegerer videre til en anden Stak (en StakMedNedarving2)
Bemærk, at i dette simple eksempel refererer StakLogger til en StakMedNedarving2-klasse. Man kan gøre StakLogger mere generelt anvendeligt ved at referere til Stak-interfacet i stedet (jvf. afsnit 15.3).
Der findes nogle almindelige variationer af Proxy-designmønstret:
Fjernproxy - bruges, når man har brug for en lokal repræsentation af et objekt, der ligger på en anden maskine. Afsnit 16.8.2 Dataforbindelse over netværk er et eksempel på dette. RMI (Remote Method Invocation) beskrevet i afsnit 14.3 anvender også dette princip.
Cache - fungerer som proxy for et objekt med nogle omkostningsfulde metodekald. I de tilfælde hvor en tidligere cachet returværdi fra metodekaldet kan bruges, foretages kaldet ikke, men den cachede værdi returneres i stedet (se afsnit 16.8.3, Dataforbindelse, der cacher forespørgsler).
Adgangssproxy - bestemmer, hvad klienten kan gøre med det virkelige objekt. Et eksempel kan findes i næste afsnit.
Virtuel Proxy - udskyder oprettelsen af omkostningsfulde objekter, indtil der er brug for dem. Et eksempel kan findes i afsnit 17.1.5.
Dette eksempel viser, hvordan en samling af data (af type Collection) kan gøres uforanderlig (dvs. at data i objektet ikke kan ændres, efter at objektet er oprettet - se afsnit 18.1 for en nærmere diskussion) ved hjælp af en Proxy.
Klassen bruges ved at pakke den oprindelige samling ind i proxy-klassen (klassen UforanderligSamling vist herunder) og derefter kun huske referencen til proxyen:
Collection d = new ArrayList(); d.add("Hej"); d.add("med"); d.add("dig"); ... d = new UforanderligSamling(d); // herefter kan dataene ikke mere ændres gennem d
UforanderligSamling delegerer alle forespørgsler videre til den oprindelige samling, mens alle ændringer afvises ved at kaste undtagelsen UnsupportedOperationException.
import java.util.*; import java.io.*; public class UforanderligSamling implements Collection, Serializable { private Collection c; // til videredelegering UforanderligSamling(Collection c) { if (c==null) throw new NullPointerException(); this.c = c; } // videredelegering af kald, der ikke ændrer samlingen c public int size() { return c.size(); } public boolean isEmpty() { return c.isEmpty(); } public boolean contains(Object o) { return c.contains(o); } public boolean containsAll(Collection coll) { return c.containsAll(coll); } public Object[] toArray() { return c.toArray(); } public Object[] toArray(Object[] a) { return c.toArray(a); } public String toString() { return c.toString(); } private static void fejl() { throw new UnsupportedOperationException("Denne samling kan ikke ændres"); } // afvisning af kald, der ændrer samlingen public void clear() { fejl(); } public boolean add(Object o) { fejl(); return false; } public boolean remove(Object o) { fejl(); return false; } public boolean addAll(Collection c) { fejl(); return false; } public boolean removeAll(Collection c) { fejl(); return false; } public boolean retainAll(Collection c) { fejl(); return false; } // iteratorer skal afvise ændringer, men ellers fungere som c's iterator public Iterator iterator() { return new Iterator() { // anonym klasse, der implementerer Iterator Iterator i = c.iterator(); // til videredelegering til c's iterator public boolean hasNext() { return i.hasNext(); } public Object next() { return i.next(); } public void remove() { fejl(); } }; } }
I afsnit 1.6.6, Uforanderlige samlinger, vises, hvad der sker, når man prøver at ændre i en uforanderlig samling. Ovenstående svarer nemlig til det Proxy-objekt man får hvis man kalder Collections.unmodifiableCollection().
Bruges en Proxy, kan oprettelsen af det andet objekt egentlig godt udskydes, indtil første gang der er brug for det. Så kalder man proxyen en Virtuel Proxy. Første gang proxyen får brug for at kalde videre i det andet objekt, oprettes dette.
En Virtuel Proxy modtager metodekald på vegne af et andet objekt, som det først opretter, når der er brug for det
Omkostningen ved at programmere en Virtuel Proxy er, at hver gang objektet skal bruges, skal det først tjekkes, om objektet er blevet oprettet.
Der kan være flere grunde til at bruge en Virtuel Proxy:
Det rigtige objekt kan ikke oprettes endnu, f.eks. fordi det afhænger af andre objekter, der ikke er klar endnu på oprettelsestidspunktet.
At oprette det rigtige objekt er dyrt i hukommelses- eller CPU-forbrug, og det er måske slet ikke sikkert, at programmet kommer til at bruge objektet, så det er en fordel at udskyde oprettelsen.
Her er en Virtuel Proxy til stakken fra afsnit 15.3.2:
public class VirtuelStak implements Stak { private Stak rigtigeStak; public void lægPå(Object o) { if (rigtigeStak==null) rigtigeStak = new StakMedNedarving2(); rigtigeStak.lægPå(o); } public Object tagAf() { if (rigtigeStak==null) rigtigeStak = new StakMedNedarving2(); return rigtigeStak.tagAf(); } }
Der, hvor vi opretter objektet, bruger vi den virtuelle stak:
Stak s = new VirtuelStak();
Den virtuelle stak er nu oprettet, men ikke den rigtige stak.
Den oprettes første gang lægPå() kaldes:
... s.lægPå("Hej"); // først her oprettes den rigtige stak s.lægPå("med"); s.lægPå("dig");
Der er egentlig ikke nogen specielt god grund til at give en stak en Virtuel Proxy for noget så simpelt som en stak - eksemplet er valgt, fordi det er simpelt og illustrativt.
Problem: Et system forventer et objekt af en bestemt type (der implementerer et bestemt interface eller arver fra en bestemt klasse), men det objekt, man ønsker at give til systemet, har ikke denne type.
Løsning: Definér et Adapter-objekt af den type, som systemet forventer, og lad Adapter-objektet delegere kaldene videre til det rigtige objekt.
Få et objekt til at passe ind i et system ved at bruge et Adapter-objekt, der passer ind i systemet, og som kalder videre i det rigtige objekt
En Adapter fungerer som omformer mellem nogle klasser
I almindeligt sprogbrug er en adapter en lille omformer, der gør det muligt at forbinde et stik og en fatning, der ellers ikke ville passe sammen. Som designmønster er en Adapter er en klasse, der fungerer som 'lim' mellem nogle klasser og får dem til at fungere sammen, selvom de ikke umiddelbart er beregnet til at spille sammen.
Man kunne f.eks. i en virksomhed stå i den situation, at man har lavet en del af et program selv, men så ønsker at udbygge programmet med nogle klasser, der er lavet af en ekstern udvikler, som ikke kender noget til resten af programmet. Virksomhedens egne klasser implementerer et givet interface, men den del af programmet, der er lavet af den eksterne udvikler, implementerer ikke dette interface. Hvad gør man så?
En mulighed er at omskrive den del af programmet, der er lavet eksternt, sådan at klasserne kommer til at implementere virksomhedens eget interface. Det kan være en besværlig og tidskrævende proces.
En anden mulighed er at lave en adapterklasse, som implementerer virksomhedens interface, og som så sørger for at kalde videre i de klasser, der er lavet eksternt.
Typisk implementerer en Adapter altså et interface, der er kendt af klienten, og formidler adgang til en klasse, der ikke er kendt af klienten.
Lad os sige, at vi har nogle opgaver, der skal udføres, og at vi har defineret klassen Opgave, der tager sig af disse opgaver:
public class Opgave { public void udfør() { // noget kode her til at udføre opgaven // ... System.out.println("Opgave udført."); } }
Nu viser det sig senere, at vi får brug for at køre opgaven i en separat tråd. For at køre noget i en separat tråd skal interfacet Runnable (der specificerer run()-metoden) implementeres.
Nu kunne vi selvfølgelig ændre klassen Opgave, så den implementerede Runnable, men vi kunne også vælge at lave en Adapter, der får Opgave til at passe ind i Runnable:
public class OpgaveRunnableAdapter implements Runnable { Opgave opg; OpgaveRunnableAdapter(Opgave o) { opg = o; } public void run() { opg.udfør(); // Oversæt kald af run() til kald af udfør() } }
Derefter kan vi passe et Opgave-objekt ind i en tråd:
public class BenytOpgaveRunnableAdapter { public static void main(String[] args) { Opgave opgave = new Opgave(); Runnable r = new OpgaveRunnableAdapter(opgave); Thread t = new Thread(r); t.start(); } }
Opgave udført.
Vi
ønsker Opgave kørt af en tråd, men Thread kræver,
at det
implementerer Runnable, så vi laver en adapter til
Opgave.
En anonym klasse er en klasse uden navn, som der oprettes et objekt ud fra der, hvor den defineres. F.eks.:
public class KlasseMedAnonymKlasse { public void metode() { // ... programkode for metode X objektAfAnonymKlasse = new X() { void metodeIAnonymKlasse() { // programkode } // flere metoder og variabler i anonym klasse }; // mere programkode for metode } }
Lige efter new angives det, hvad den anonyme klasse arver fra, eller et interface, der implementeres (i dette tilfælde X). Man kan ikke definere en konstruktør til en anonym klasse (den har altid standardkonstruktøren). Angiver man nogle parametre ved new X(), er det parametre til superklassens konstruktør.
Fordelen ved anonyme klasser er, at det tillades på en nem måde at definere et specialiseret objekt præcis, hvor det er nødvendigt - det kan være meget arbejdsbesparende.
Da adapterklasser er så små og ofte kun skal bruges et enkelt sted, definerer man dem ofte som anonyme klasser.
Det følgende eksempel gør det samme, men i stedet for at bruge OpgaveRunnableAdapter anvendes en anonym klasse, der implementerer Runnable og kalder videre i Opgave.
public class BenytAnonymAdapter { public static void main(String[] args) { final Opgave opgave = new Opgave(); Runnable r = new Runnable() { public void run() // kræves af Runnable { opgave.udfør(); } }; Thread t = new Thread(r); t.start(); } }
Vi har brugt masser af en bestemt slags adapter-klasser i vores programmering: Til at lytte efter hændelser og udføre noget bestemt, når hændelsen skete. F.eks.:
private TextArea t1, t2; private Button kopierKnap; ... kopierKnap.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { String s = t1.getText(); t2.setText(s); } });
Her er den anonyme klasse en Adapter, der implementerer interfacet ActionListener, der er kendt af klienten (kopierKnap), og formidler adgang til klasser, der ikke er kendt af klienten (TextArea t1 og t2).
Eksempelvis kunne det være, at vi havde en liste af Kunde-objekter (defineret i afsnit 16.8), som vi ønskede at vise på skærmen i en JTable (beskrevet i afsnit 6.3.2).
JTable ved selvfølgelig ikke, hvordan den skal vise Kunde-objekter - de "passer" ikke umiddelbart i en JTable. Nu kunne vi naturligvis lave programmet om, sådan at det var bedre indrettet til JTable, eller vi kunne kopiere indholdet af Kunde-objekterne over i en datastruktur som JTable kunne genkende.
En smartere løsning ville være at lave en adapterklasse, der "passer" ind i JTable (implementerer TableModel eller arver fra AbstractTableModel eller DefaultTableModel), og som bruger den eksisterende Kunde-liste.
import java.util.*; import javax.swing.table.*; public class KundelisteTableModelAdapter extends AbstractTableModel { private List liste; public KundelisteTableModelAdapter(List liste1) { liste = liste1; } public int getRowCount() { return liste.size(); } public int getColumnCount() { return 2; } // navn og kredit public String getColumnName(int kol) { return kol==0 ? "Navn" : "Kredit"; } public Object getValueAt(int række, int kol) { Kunde k = (Kunde) liste.get(række); return kol==0 ? k.navn : ""+k.kredit; } }
KundelisteTableModelAdapter er en Adapter, der implementerer interfacet TableModel (gennem klassen AbstractTableModel), der er kendt af klienten (JTable), og formidler adgang til klasser, der ikke er kendt af klienten (listen af Kunde-objekter).
Her er et eksempel på brug af KundelisteTableModelAdapter:
import java.util.*; import javax.swing.*; public class BenytKundelisteTableModelAdapter { public static void main(String arg[]) { // Opret liste List liste = new ArrayList(); liste.add( new Kunde( "Jacob", -1899) ); liste.add( new Kunde( "Søren", 600) ); // Opret vindue med tabel JFrame vindue = new JFrame(); JTable tabel = new JTable(); vindue.getContentPane().add(tabel); // Lad tabel vise liste v.hj.a. adapteren tabel.setModel( new KundelisteTableModelAdapter( liste )); // vis vindue vindue.setSize(200,100); vindue.setVisible(true); } }
Uheldigvis er der i Java også nogle klasser, der hedder adapter-klasserne. Ingen af disse klasser er eksempler på designmønstret Adapter!
Disse klasser tjener i stedet til at lette programmørens arbejde, når han skal implementere et hændelseslytter-interface, der har mere end en metode. I stedet for at implementere interfacet direkte arver man fra en såkaldt adapter-klasse, der har tomme implementationer for alle metoderne.
Eksempelvis, i stedet for at implementere MouseListener:
import java.awt.event.*; public class Linjelytter implements MouseListener { public void mousePressed(MouseEvent hændelse) // kræves af MouseListener { System.out.println("Der blev trykkket med musen!"); } //-------------------------------------------------------------------- // Ubrugte hændelser (skal defineres for at implementere MouseListener) //-------------------------------------------------------------------- public void mouseReleased(MouseEvent hændelse){} // kræves af MouseListener public void mouseClicked(MouseEvent event) {} // kræves af MouseListener public void mouseEntered (MouseEvent event) {} // kræves af MouseListener public void mouseExited (MouseEvent event) {} // kræves af MouseListener }
kan man arve fra MouseAdapter (der implementerer MouseListener med tomme metoder):
import java.awt.event.*; public class Linjelytter2 extends MouseAdapter { public void mousePressed(MouseEvent hændelse) { System.out.println("Der blev trykkket med musen!"); } }
Tilsvarende med de andre klasser i java.awt.event, der ender på Adapter (ComponentAdapter, FocusAdapter, KeyAdapter, MouseAdapter, MouseMotionAdapter og WindowAdapter): Ingen af dem er en Adapter i designmønster-henseende.
Problem: Du er i gang med at lave et system, som andre (klienter) skal anvende, hvor de skal kunne gennemløbe dine data. Du ønsker ikke, at de skal kende noget til, hvordan data er repræsenteret i dit system (f.eks. antal elementer eller deres placering i forhold til hinanden).
Løsning: Definér et hjælpeobjekt (en Iterator), som klienten kan bruge til at gennemløbe data i dit system.
En Iterator er et hjælpeobjekt beregnet til at gennemløbe data
En Iterator har som minimum:
en metode til at spørge, om der er flere elementer, og
en metode til at hente næste element
En Iterator bruges i stedet for en tællevariabel. Fordelen ved at definere en Iterator er, at klienten ikke behøver at vide noget om strukturen af de data, der gennemløbes.
Iteratorer er flittigt brugt i Collections-klasserne, idet alle datastrukturerne ligefrem har metoden iterator(), der giver et objekt, der itererer gennem elementerne.
Interfacet Iterator ser således ud:
package java.util; public interface Iterator { boolean hasNext(); // om der er flere elementer Object next(); // hent næste element void remove(); }
Man kan f.eks. gennemløbe en Vector (eller en anden af Collections-klasserne) med
Vector samlingAfData; // indsæt nogle strenge i v ... Iterator i = samlingAfData.iterator(); while (i.hasNext()) { String s = (String) i.next(); ... }
eller med en for-løkke:
for (Iterator i = v.iterator(); i.hasNext(); ) { String s = (String) i.next(); ... }
Fordi vi bruger en Iterator, er vi afskærmet fra strukturen af de data, der gennemløbes. Data behøver f.eks. ikke have et bestemt indeks eller rækkefølge som i en Vector. Ovenstående eksempler virker lige så godt med en List eller Set (en mængde).
Bemærk, at klassen Iterator, som den er defineret i Collections-klasserne, har noget, man normalt ikke forbinder med en iterator, nemlig metoden remove(), der fjerner det aktuelle element fra den underliggende samling af data.
Selvom forskellige former for iteratorer bruges flittigt i standardbiblioteket, vil man nok sjældent komme ud i at definere sin egen form for Iterator, da interfacet java.util.Iterator dækker langt de flestes behov.
Man vil meget sjældent selv definere en ny form for Iterator, med mindre man er i gang med at programmere et programbibliotek
Når vi behandler svaret på en forespørgsel til en database, sker det ved at iterere gennem svar-tabellen række for række.
Det gøres med et ResultSet-objekt. Interfacet ResultSet ser således ud:
package java.sql; public interface ResultSet { boolean next() throws SQLException; boolean previous() throws SQLException; boolean isFirst() throws SQLException; boolean isLast() throws SQLException; boolean first() throws SQLException; boolean last() throws SQLException; int getRow() throws SQLException; boolean absolute( int row ) throws SQLException; boolean relative( int rows ) throws SQLException; ... mange andre metoder til bl.a. at aflæse/opdatere rækker }
Eksempel på brug:
// forespørgsler ResultSet rs = stmt.executeQuery("SELECT navn, kredit FROM kunder"); while (rs.next()) { String navn = rs.getString("navn"); double kredit = rs.getDouble("kredit"); System.out.println(navn+" "+kredit); }
Her er metoderne til at spørge, om der er flere elementer, og til at hente næste element slået sammen til én metode, nemlig next().
Vi (klienten) behøver ikke at vide noget om, hvordan data er repræsenteret, og heller ikke om alle data hentes på én gang eller lidt efter lidt efterhånden som vi kalder next().
De funktioner, Java har til at manipulere med todimensionale geometriske figurer (Java2D - beskrevet i kapitel 5) i pakken java.awt.geom, baserer sig kraftigt på iteratorer.
Der findes disse grundlæggende geometriske figurer:
Rektangler - Rectangle2D og RoundRectangle2D
Linjer - Line2D (en ret linje), CubicCurve2D (en linje, der er buet efter et ankerpunkt) og QuadCurve2D (en linje, der er buet efter to ankerpunkter)
Ellipse2D og Arc2D (buestykke)
Fælles for dem alle er, at de består af (buede eller rette) linjestykker (en firkant består f.eks. af fire rette linjestykker), og alle figurerne kan, selvom de er ret forskelligt repræsenteret indeni, returnere en Iterator til at gennemløbe linjestykkerne i figuren.
Denne iterator ser således ud:
package java.awt.geom; public interface PathIterator { // om iterationen er nået gennem alle linjestykkerne public boolean isDone(); // gå til næste linjesegment public void next(); // lægger data får det aktuelle linjestykke ind i variablen segment public int currentSegment(double[] segment); ... flere metoder }
Når Java2D skal kombinere flere geometriske figurer til en ny figur, sker det ved, at den gennemløber figurernes linjesegmenter v.hj.a. iteratoren og derpå opbygger det kombinerede geometriske objekt.
Det sker f.eks. i klassen GeneralPath, der repræsenterer en vilkårlig geometrisk figur, der f.eks. kan bygges op ved at kombinere andre geometriske figurer:
GeneralPath figur = new GeneralPath(); figur.append( new Line2D.Float(0, 0, 100, 100), false ); figur.append( new CubicCurve2D.Float(0, 0, 80, 15, 10, 90, 100, 100), false ); figur.append( new Arc2D.Float(-30, 0, 100, 100, 60, -120, Arc2D.PIE), false );
Hver gang append() bliver kaldt med en figur, findes først en PathIterator på figuren, denne gennemløbes derefter, og linjestykkerne føjes til GeneralPath-objektet.
Problem: Et sæt af beslægtede objekter er indviklede at bruge, og der er brug for en simpel grænseflade til dem.
Løsning: Definér et hjælpeobjekt, en Facade, der gør objekterne lettere at bruge.
En Facade giver en simplificeret grænseflade til en gruppe delsystemer eller til et indviklet system
En Facade er altså et objekt, der giver en "brugergrænseflade" til nogle andre objekter og dermed forenkler brugen af disse objekter.
I pakken java.net er klassen URL en Facade for en række andre klasser, der kan håndtere en lang række protokoller, bl.a. HTTP, FTP, e-post og lokale filer (se eksempler i afsnit 18.4.3).
Men for klienten er URL-klassen ekstrem nem at bruge, f.eks. kan klienten hente en hjemmeside ned i en datastrøm med:
URL u = new URL("http://java.sun.com/"); InputStream is = u.openStream(); ...
Facaden skjuler hele processen for os og letter os dermed fra byrden med at forstå, hvordan klasserne og kommunikationen fungerer inde bagved: Internt bruger URL et InetAddress-objekt til at repræsentere værtsmaskinens IP-adresse, og den bruger et URLStreamHandler-objekt til at håndtere protokollen (i dette tilfælde HTTP-protokollen). Dette URLStreamHandler-objekt fabrikerer en URLConnection, og URL kalder videre i dette URLConnection-objekt, når vi kalder openStream().
Se afsnit 12.7 for en beskrivelse af netværksklasserne i standardbiblioteket.
Læser man dokumentationen til klasserne Socket (der repræsenterer en forbindelse til en bestemt maskine over netværket på en bestemt port og som klienter bruger til at forbinde sig til værtsmaskiner med) og ServerSocket (der repræsenterer en port på en værtsmaskine der er åben for indkommende forbindelser), finder man ud af, at de begge faktisk er facader til klassen SocketImpl.
Denne konstruktion skyldes, at det på styresystemets niveau er næsten de samme kald, der skal ske for en Socket og en ServerSocket, og de varetages derfor af den samme klasse (SocketImpl). Socket og en ServerSocket bruger altså begge SocketImpl til at varetage den egentlige netværkskommunikation.
Designerne til Javas standardbibliotek har anvendt designmønstret Facade for at gøre det simplere at lave netværkskommunikation: De har delt funktionaliteten i SocketImpl op i to letforståelige klasser (Socket og en ServerSocket) til de to måder, den kan bruges på.
Problem: Et objekt skal kunne underrette nogle andre objekter om en eller anden ændring eller hændelse, men det er ikke hensigtsmæssigt, at objektet kender direkte til de andre objekter.
Løsning: Lad lytterne (observatørerne) implementere et fælles interface (eller arve fra en fælles superklasse) og registrere sig hos det observable (observerbare) objekt. Det observable objekt kan herefter underrette lytterne gennem interfacet, når der er brug for det.
Designmønstret Observatør (eng.: Observer) kaldes også Abonnent (eng.: Publisher-Subscriber) eller lytter (eng.: Listener).
Det mest kendte eksempel på Observatør-designmønstret er hændelseshåndteringen i grafiske brugergrænseflader.
Når man vil lytte efter musehændelser, opretter man en klasse, der implementerer MouseListener-interfacet (observatøren):
import java.awt.*; import java.awt.event.*; public class Muselytter implements MouseListener { public void mousePressed(MouseEvent hændelse) // kræves af MouseListener { Point trykpunkt = hændelse.getPoint(); System.out.println("Mus trykket ned i "+trykpunkt); } public void mouseReleased(MouseEvent hændelse) // kræves af MouseListener { Point slippunkt = hændelse.getPoint(); System.out.println("Mus sluppet i "+slippunkt); } public void mouseClicked(MouseEvent hændelse) // kræves af MouseListener { System.out.println("Mus klikket i "+hændelse.getPoint()); } //-------------------------------------------------------------------- // Ubrugte hændelser (skal defineres for at implementere MouseListener) //-------------------------------------------------------------------- public void mouseEntered (MouseEvent event) {} // kræves af MouseListener public void mouseExited (MouseEvent event) {} // kræves af MouseListener }
Man skal registrere lytteren (observatøren) ved at kalde metoden addMouseListener(lytter) på den grafiske komponent, der sender hændelserne (den observable):
import java.applet.*; public class LytTilMusen extends Applet { public LytTilMusen() { Muselytter lytter = new Muselytter(); this.addMouseListener(lytter); // tilføj lytteren til er appletten selv } }
Når man senere klikker med musen, bliver metoder i lytteren kaldt.
Forestil dig et program, der skal fungere som en fælles kalender for en forening, der udbyder forskellige foredrag.
Kalenderen indeholder en liste over foredragene, og medlemmerne af foreningen kan tilmelde sig de forskellige foredrag efter ønske.
Hvis der sker ændringer i planen, skal kun de interesserede medlemmer have besked, dvs. de, som har tilmeldt sig et givent foredrag.
Det kan implementeres ved at lave et foredragsobjekt, som indeholder en liste af registrerede lyttere. Objektet har metoder til at tilføje og fjerne lyttere og til at løbe listen igennem og sende beskeder, når det er nødvendigt.
Her er en skitse til et klassediagram:
Problem: Programmet skal senere kunne udvides til at bruge nogle flere klasser, uden at programmet skal skrives om.
Løsning: Definér et fælles interface (eller superklasse) for klasserne, og søg efter egnede klasser på køretidspunktet, indlæs dem og brug dem.
Indlæs klasser dynamisk under kørslen
Dynamisk Binding (eller Dynamisk Lænkning - eng.: Dynamic Linkage) går ud på at indlæse klasser dynamisk, efter at programmet er startet. Det er en meget kraftfuld mekanisme der tillader "plug-ins", dvs. at programdele kan føjes til programmet, efterhånden som der er behov for dem, og som måske er produceret af nogle helt andre end dem, der oprindeligt skrev programmet.
I Java indlæses en klasse dynamisk med et kald til Class.forName(), der tager en streng med et klassenavn som parameter.
F.eks. indlæser man Vector-klassen og opretter et objekt med:
Class klassen = Class.forName("java.util.Vector");
Object objektet = klassen.newInstance();
Dette forudsætter, at klasserne findes der, hvor systemet plejer at lede (ellers kan man definere sin egen ClassLoader som beskrevet i afsnit 11.4.3, Indlæse klasser fra filsystemet).
Herunder er beskrevet to konkrete eksempler på Dynamisk Binding. Et tredje eksempel (URL-klassen) diskuteres i afsnit 18.4.5.
JDBC anvender Dynamisk Binding til at håndtere drivere for de forskellige databaser. Det tillader enhver databaseleverandør at levere drivere, og de vil umiddelbart passe ind i JDBC.
Kigger man i pakken java.sql (se javadokumentationen), kan man få en idé om, hvordan det gøres. Herunder er en forsimplet udgave af implementationen af JDBC, der illustrerer princippet (eksemplet kan ikke køres, det skal mere ses som en illustration af JDBC's brug af Dynamisk Binding).
DriverManager har en intern liste af drivere. Når DriverManager.getConnection() kaldes, kalder den en metode i hver af sine indlæste drivere for at finde en, der passer. Drivere, der ikke passer til den URL, der beskriver forbindelsen, signalerer dette ved at returnere null.
Driverne har på forhånd kaldt DriverManager.registerDriver() og registreret sig selv.
import java.sql.*; import java.util.*; public class DriverManager { private static Vector drivere = new Vector(); public static Connection getConnection(String url) throws SQLException { // Gå gennem alle de indlæste drivere og forsøg at lave en forbindelse for (int i = 0; i < drivere.size(); i++) { Driver d = (Driver) drivere.elementAt(i); Connection result = d.connect(url); if (result != null) { // Success! return result; } } // kommer hertil var der ingen drivere der kunne klare forbindelsen throw new SQLException("No suitable driver"); } /** * Drivere kalder denne metode når de bliver indlæst, for at registrere sig selv */ public static void registerDriver(Driver driver) { drivere.addElement(driver); } // ... flere metoder }
Alle drivere skal implementere interfacet Driver.
import java.sql.*; public interface Driver { /** * Når DriverManager.getConnection() kaldes, kalder den denne metode i hver * af sine indlæste drivere, for at finde en driver der passer. * @return Forbindelse til databasen, eller null hvis denne driver ikke passer * @param url Adressen på databasen, f.eks. "jdbc:odbc:datakilde1" * eller "jdbc:oracle:thin:@ora.javabog.dk:1521:student" */ Connection connect(String url) throws SQLException; // ... flere metoder }
Driver-klasserne skal også sørge for, når de bliver indlæst, at registrere sig hos DriverManager. Lad os forestille os, at vi vil skrive en driver til databasen Xyz, som skal genkende URL'er på formen "jdbc:xyz:...". Hvis den får en passende URL, returnerer den et XyzConnection-objekt, ellers null.
import java.sql.*; public class XyzDriver implements Driver { // Klasseinitialiseringsblok - køres én gang når klassen indlæses static { Driver drv = new XyzDriver(); DriverManager.registerDriver( drv ); } public Connection connect(String url) throws SQLException { // passer URLen til min driver? if (url.startsWith("jdbc:xyz:")) { // kode til at oprette et passende Connection-objekt return new XyzConnection(url); } // hvis URL'en ikke startede med "jdbc:xyz:" så returner null // og så vil en anden driver blive forsøgt. return null; } // ... flere metoder }
Vi forestiller os, at et program til at tegne kurver dynamisk skal kunne udvides med flere funktioner. Funktioner, implementerer alle interfacet Funktion:
public interface Funktion { public double beregn(double x); }
Alle klasserne har en navngivning, der tillader dem at blive dynamisk indlæst: De hedder alle Funktion_ og så navnet, f.eks. repræsenterer klassen Funktion_sin funktionen sinus:
public class Funktion_sin implements Funktion { public double beregn(double x) { return Math.sin(x); } }
Funktions-fortolkeren indlæser klasser, som implementerer Funktion-interfacet ud fra funktionens navn dynamisk:
public class FunktionsfortolkerDynBind { public Funktion findFunktion(String navn) { try { // Prøv at indlæse en klasse der hedder f.eks. Funktion_sin Class klasse = Class.forName("Funktion_"+navn); // Opret et objekt fra klassen Funktion f = (Funktion) klasse.newInstance(); return f; } catch (Exception ex) { ex.printStackTrace(); throw new IllegalArgumentException("ukendt funktion: "+navn); } } public Funktion fortolk(String udtryk) { // endnu ikke implementeret - returner bare noget. return findFunktion("sin"); } }
Klientprogrammer kalder analyser() med en streng og får en tilsvarende Funktion ud. Programmet kan senere udvides med f.eks. Funktion_cos, Funktion_tan o.s.v1.
Her er et eksempel på brug:
public class BenytFunktionsfortolkerDynBind { public static void main(String arg[]) { FunktionsfortolkerDynBind analysator = new FunktionsfortolkerDynBind(); Funktion f = analysator.fortolk("sin(5*cos(x))"); System.out.println("f(1)=" + f.beregn(1) ); } }
Læs afsnit 16.8.3, Dataforbindelse, der cacher forespørgsler, og lav med, udgangspunkt i eksemplet, proxy-klassen Dataforbindelseslogger, der skriver ud til skærmen hver gang der kaldes en metode på dataforbindelsen.
Metoden findFunktion() i
FunktionsfortolkerDynBind kan gøres mere effektiv ved at
huske de allerede indlæste Funktion-klasser, sådan at en
funktion kun bliver indlæst én gang.
Udvid
fortolkeren med en afbildning (en HashMap), der afbilder allerede
indlæste funktionsnavne (strenge) over i de tilsvarende
klasser.
Udvid Funktionsfortolker fra afsnit 4.7.2 til at bruge dynamisk binding.
Kig i afsnit 8.1, Basisfunktioner i JDBC. Hvilke designmønstre kan du se, der er anvendt i JDBC-biblioteket, ud fra beskrivelsen?
Nævn 2 fabrikeringsmetoder.
Nævn mindst 2 andre designmønstre anvendt i JDBC, og beskriv dem.
Svarene findes i næste afsnit.
1Dette eksempel kan dog ikke tage højde for sammensatte funktioner som f.eks. sin(5*x+1), da Funktion-objekter ikke kan kombineres. Se afsnit 4.7.2 for et eksempel, der tager højde for sammensatte funktioner.