javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen

17 Hyppigt anvendte designmønstre


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.

17.1 Proxy

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.:

17.1.1 Simpelt eksempel: En stak, der logger kaldene

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).

17.1.2 Variationer af designmønstret Proxy

Der findes nogle almindelige variationer af Proxy-designmønstret:

  1. 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.

  2. 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).

  3. Adgangssproxy - bestemmer, hvad klienten kan gøre med det virkelige objekt. Et eksempel kan findes i næste afsnit.

  4. Virtuel Proxy - udskyder oprettelsen af omkostningsfulde objekter, indtil der er brug for dem. Et eksempel kan findes i afsnit 17.1.5.

17.1.3 Eksempel: Gøre data uforanderlige vha. Proxy

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().

17.1.4 Doven Initialisering/Virtuel Proxy

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:

17.1.5 Eksempel på Virtuel Proxy: En stak der først oprettes, når den skal bruges

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.

17.2 Adapter

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å?

Typisk implementerer en Adapter altså et interface, der er kendt af klienten, og formidler adgang til en klasse, der ikke er kendt af klienten.

17.2.1 Simpelt eksempel

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.

17.2.2 Anonyme klasser som adaptere

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.

Eksempel: Anonym RunnableAdapter

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();
  }
}

17.2.3 Anonyme adaptere til at lytte efter hændelser

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).

17.2.4 Eksempel: Få data til at passe ind i en JTable

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);
  }
}

17.2.5 Ikke-eksempel: Adapter-klasserne

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.

17.3 Iterator

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 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.

17.3.1 Iteratorer i Collections-klasserne

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.

17.3.2 Definere sin egen form for iterator

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

17.3.3 Iteratorer i JDBC

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().

17.3.4 Iterator til at gennemløbe geometriske figurer

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:

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.

17.4 Facade

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.

17.4.1 Eksempel: URL

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.

17.4.2 Eksempel: Socket og ServerSocket

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å.

17.5 Observatør/Lytter

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).

17.5.1 Eksempel: Hændelser

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.

17.5.2 Eksempel: Kalender

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:

17.6 Dynamisk Binding

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.

17.6.1 JDBC og Dynamisk Binding

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
}

17.6.2 Eksempel: Fortolkning af matematikfunktioner

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) );
  }
}

17.7 Opgaver

Proxy

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.

Dynamisk Binding

  1. 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.

  2. Udvid Funktionsfortolker fra afsnit 4.7.2 til at bruge dynamisk binding.

Designmønstre i JDBC

Kig i afsnit 8.1, Basisfunktioner i JDBC. Hvilke designmønstre kan du se, der er anvendt i JDBC-biblioteket, ud fra beskrivelsen?

  1. Nævn 2 fabrikeringsmetoder.

  2. Nævn mindst 2 andre designmønstre anvendt i JDBC, og beskriv dem.

Svarene findes i næste afsnit.

17.8 Løsninger

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

17.8.1 Dataforbindelseslogger

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

17.8.2 Designmønstre i JDBC

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

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.

javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen
http://javabog.dk/ - af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (71% af værket).

Ønsker du at se de sidste 29% af dette værk (362838 tegn) skal du købe bogen. Så får du pæne figurer og layout, stikordsregister og en trykt bog med i købet.