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

15 Videregående OOP


15.1 Arv 228

15.1.1 Holdninger til arv 228

15.1.2 Modstrid mellem holdningerne 228

15.1.3 Øvelse 231

15.1.4 Nedarvinger, hvor funktionaliteten ikke udvides, men begrænses 231

15.2 Delegering (i stedet for arv) 232

15.2.1 At skjule nogle metoder 232

15.2.2 Modellering af roller 234

15.2.3 Resumé og konklusion 236

15.3 Specificér funktionalitet i et interface 236

15.3.1 Repetition af interfaces 236

15.3.2 Eksempel: Stak 237

15.3.3 Eksempel: Collections-klasserne 238

15.4 Konstanterklæringer i et interface 239

15.5 Markeringsinterface 239

15.6 Ansvarsområder 240

15.6.1 Eksempel på tildeling af ansvarsområder 240

15.6.2 Ekspert 242

15.6.3 Skaber 242

15.7 Lav kobling og høj kohæsion 243

15.7.1 Lav kobling 243

15.7.2 Høj kohæsion 243

15.7.3 Indkapsling 244

15.7.4 Indkapsling og pakker 244

15.8 GRASP 245

15.9 Introduktion til designmønstre 245

15.9.1 Designmønstre berørt i de følgende kapitler 245

I dette kapitel vil vi diskutere nogle af de begreber og tankegange, der kan være inspirerende, når man har tilegnet sig en grundlæggende viden og erfaring inden for objektorienteret programmering. Kapitlet er struktureret i emner, som kan læses nogenlunde uafhængigt af hinanden. Emnerne er beregnet som inspiration til eftertanke snarere end faste anvisninger, der altid bør følges.

Hvor det er relevant, beskrives nogle teknikker, der er specifikke i Java, og hvad man i stedet ville gøre i C++.

15.1 Arv

15.1.1 Holdninger til arv

Ved nedarving fra en klasse til en anden vil nedarvingen overtage ("arve") alle variabler og metoder fra superklassen. Der er dog flere mulige holdninger til, hvordan arv skal bruges.

Den kodenære, "amerikanske"1: Arv bruges til at genbruge variabler og metoder. Hvis to klasser har fælles variabler eller metoder, bør man lave en fælles superklasse, der tager vare på de ting, der er fælles.

Den analytiske, "skandinaviske": Arv repræsenterer en er-en-relation, og nedarving bør ske mellem klasser, når de begreber, som de står for, har en 'er-en'-relation til hinanden.

Den kodenære holdning, at arv bliver betragtet som en måde at spare kode på, er en 'nedefra-og-op'-strategi, hvor der først og fremmest programmeres og hvor klasserelationer som arv senere dukker frem som 'konsekvenser' af programmeringen. I de mere udbredte programmeringssprog, som C og Pascal, var denne holdning dominerende i 1980'erne, lige da klasser og objekter var blevet indført i disse sprog.

Senere, da discipliner som objektorienteret analyse og design voksede frem, blev den analytiske 'oppefra-og-ned'-måde at anskue tingene på mere populær.

Ofte vil disse tankegange give de samme nedarvingsrelationer, så det kan være svært umiddelbart at forstå, hvilken forskel det skulle gøre.

15.1.2 Modstrid mellem holdningerne

For at tage et klassisk eksempel, hvor disse to tankegange kommer i indbyrdes modstrid, så betragt et tegneprogram med forskellige figurer: Punkter, linjer, rektangler, trekanter, firkanter, polygoner, cirkler, ovaler osv. I første omgang defineres følgende klasser:

Klasse

variabler

metoder

Punkt

int x, y

void tegn()
void flytTil(x,y)

Linje

int x, y, dx, dy

void tegn()
void flytTil(x,y)

Rektangel

int x, y, dx, dy
boolean udfyldt

void tegn()
void flytTil(x,y)
double areal()

Trekant

int x, y, dx, dy, dx2, dy2
boolean udfyldt

void tegn()
void flytTil(x,y)
double areal()

Det er oplagt, at disse klasser har meget til fælles, og at nedarving bør komme på tale. Men hvordan skal arvehierakiet være?

Arvehierakiet i den kodenære, "amerikanske" tankegang

I den kodenære tankegang er sagen klar:

  • Punkt er superklassen,

  • Linje arver fra Punkt, tilføjer to variabler og omdefinerer tegn(),

  • Rektangel arver fra Linje og omdefinerer tegn().

  • Trekant kan arve fra Linje og genbruge tegn() til at tegne den første streg (en anden mulighed var at arve fra Rektangel, så man får variablen 'udfyldt' med).

Arvehierakiet i den analytiske, "skandinaviske" tankegang

I den analytiske tankegang stiller sagen sig helt anderledes: Linje kan ikke arve fra Punkt, da linjer ikke er punkter!

Faktisk er der ingen af de ovenfor nævnte klasser, der kan arve fra hinanden. Man må opfinde nogle ekstra, mere abstrakte klasser som Figur og UdstraktFigur (en Figur med et areal):

Klasse

variabler

metoder

Figur

int x, y;

void tegn(); (evt. abstrakt)
void flytTil(x,y);

UdstraktFigur

int x, y;
boolean udfyldt;

void tegn(); (evt. abstrakt)
void flytTil(x,y);
double areal(); (evt. abstrakt)

Nu kan Figur være superklassen, Punkt er-en Figur og arver derfor fra Figur og definerer tegn(), UdstraktFigur er-en Figur med en ekstra variabel og metode, Rektangel er-en UdstraktFigur udvidet med variablerne dx og dy og ligeledes med Trekant, der er-en UdstraktFigur udvidet med variablerne dx, dy, dx2 og dy2.

15.1.3 Øvelse

  1. Kig på klassediagrammet for den kodenære tankegang. Hvor skulle cirkler placeres? ellipser? firkanter? polygoner? Tegn dem ind.

  2. Kig på klassediagrammet for den analytiske tankegang. Hvor skulle cirkler placeres? ellipser? firkanter? polygoner? Tegn dem ind.

  3. Hvordan adskiller diagrammerne sig? Hvilken tankegang kan du bedst lide? Hvorfor? Hvor er der mest kode? Hvilket klassediagram tror du er nemmest at udvide senere?

15.1.4 Nedarvinger, hvor funktionaliteten ikke udvides, men begrænses

Et dilemma i den analytiske tankegang er, hvad man skal stille op, når der konceptuelt er tale om en specialisering (som altså burde medføre arv), men hvor funktionaliteten begrænses i stedet for (som normalt) at blive udvidet.

Tænk f.eks. på cirkler og ellipser. Det er klart, at Ellipse skal have to variabler til at angive radius (højde og bredde), mens Cirkel kun har brug for én radius.

På den anden side er der ingen tvivl om, at en cirkel er en slags ellipse (en hel rund ellipse), så konceptuelt gælder relationen Cirkel 'er-en' Ellipse, selvom Ellipse-klassen har flere data end Cirkel.

15.2 Delegering (i stedet for arv)

Delegering: At et objekt har et andet objekt, som det 'uddelegerer' nogle opgaver til

Ofte har man brug for at udvide og genbruge en klasses funktionalitet. Den sædvanlige måde at gøre det på er, som bekendt, ved at lave en nedarving, men det kan være, at det (af den ene eller anden grund) er mere hensigtsmæssigt at bruge delegering i stedet.

Delegering går ud på at skrive klassen med ekstra funktionalitet, sådan at den bruger den oprindelige klasse i stedet for at arve fra den.

15.2.1 At skjule nogle metoder

Eksempelvis kan det være, man ønsker at 'arve' en klasses data, og nogle men ikke alle, dens metoder. Så vil delegering sandsynligvis være mere hensigtsmæssigt end nedarving.

Arv kan ikke skjule metoder, men det kan delegering

Man vil lave datastrukturen 'stak', som er en liste, hvor man kun kan tilføje og fjerne elementer fra den ene ende (lægge på og tage af stakken). Man kunne arve fra Vector:

public class StakMedNedarving extends java.util.Vector
{
  public void lægPå(Object o)
  {
    addElement(o);
  }

  public Object tagAf()
  {
    Object o = lastElement();
    setSize( size() - 1 );
    return o;
  }
}

... men hvordan kan man nu være sikker på, at de, der bruger StakMedNedarving, bruger de nye metoder?

De kunne godt kalde elementAt() eller nogen af de andre metoder som StakMedNedarving arver fra Vector, og der er ikke nogen elegant måde at forhindre det på.

Det er mere hensigtsmæssigt at delegere opgaven med at holde styr på listen til Vector i stedet for at arve fra Vector:

public class StakMedDelegering
{
  private java.util.Vector v = new java.util.Vector();

  public void lægPå(Object o)
  {
    v.addElement(o);
  }

  public Object tagAf()
  {
    Object o = v.lastElement();
    v.setSize( v.size() - 1 );
    return o;
  }
}

Nu er vektoren indkapslet i stakken, og det er umuligt at kalde andre metoder end dem, der er defineret i StakMedDelegering.

Det er sjældent en god idé at arve fra 'værktøjsklasser' såsom Vector: Man har nemlig ikke mulighed for at forhindre brugeren af den nye klasse i at kalde nogle metoder, det ikke var meningen, han skulle kalde.

StakMedNedarving er et eksempel på den 'kodenære' tankegang beskrevet i afsnit 15.1.1 og illustrerer et af problemerne med denne måde at tænke på.

I C++ kan problemet med at skjule superklassens metoder løses ved at bruge såkaldt "privat nedarving", hvor kun underklassen har adgang til de nedarvede metoder (så det virker, som om metoderne er erklæret private i den nedarvende klasse).

Forhindre metodekald ved at kaste undtagelser

En anden mulighed kunne være at tilsidesætte alle de nedarvede metoder, der var uønskede, med nogle nye, der konsekvent kaster en undtagelse (eng.: exception), hvis de bliver kaldt. Dette er naturligvis ikke specielt elegant, men fra tid til anden kan man blive tvunget ud i den løsning (et andet eksempel ses i afsnit 17.1.3, Gøre data uforanderlige vha. Proxy).

public class StakMedNedarvingKastUndtagelser extends java.util.Vector
{
  public void lægPå(Object o)
  {
    super.addElement(o);
  }

  public Object tagAf()
  {
    Object o = lastElement();
    setSize( size() - 1 );
    return o;
  }

  public void addElement(Object obj) {
    throw new UnsupportedOperationException("Ikke tilladt på en stak");
  }

  public void setSize(int newSize)   {
    throw new UnsupportedOperationException("Ikke tilladt på en stak");
  }
  
  public Object elementAt(int index) {
    throw new UnsupportedOperationException("Ikke tilladt på en stak");
  }

  public void setElementAt(Object obj, int index) {
    throw new UnsupportedOperationException("Ikke tilladt på en stak");
  }

  public void insertElementAt(Object obj, int index) {
    throw new UnsupportedOperationException("Ikke tilladt på en stak");
  }
  // osv...
}

En stor ulempe ved denne teknik er, at fejl først fanges på kørselstidspunktet i stedet for under oversættertidspunktet.

En anden ulempe er, at hver gang superklassen får defineret en ny metode, der ikke skal kunne kaldes i nedarvingen, skal man huske at opdatere nedarvingen tilsvarende. Vector er således blevet udvidet med et antal metoder fra JDK1.1 til JDK1.2 (f.eks. get(), add() og de andre metoder i interfacet List - se kapitel 1, Samlinger af data), og koden ovenfor skulle derfor udvides med disse metoder, da man skiftede til JDK1.2 (i øvrigt er de fleste af Vectors metoder erklæret final og kan derfor faktisk overhovedet ikke tilsidesættes i en nedarving).

15.2.2 Modellering af roller

Et andet tilfælde, hvor delegering er en bedre idé end arv, er, hvor objekterne kan have flere roller eller skifte rolle under udførelsen af programmet.

Arv er kun velegnet til "er-en"-relationer, der aldrig ændrer sig under kørslen

Lad os se på et eksempel inden for en virksomhed. Her er personer, der kan være ledere, ansatte og konsulenter (man kunne f.eks. forestille sig, at de har hver sin lønberegnings-metode, forskellige beføjelser, osv. ...).

Man kunne modellere rollerne med nedarving som vist i figuren herunder:

Rollerne modelleret med nedarving

Der er en fælles superklasse...

public class Person
{
  String fornavn;
  String efternavn;
  String cpr;

  public String hentFornavn() { return cpr; }

  // flere metoder og variabler...
}

... og nogle nedarvinger af Person, bl.a. lederen:

public class LederArv extends Person
{
  java.util.List ansatte;

  // opret en leder
  public LederArv() { }

  // opret en leder-rolle med en eksisteren person (ikke så elegant...)
  public LederArv(Person p)
  {
    this.fornavn   = p.fornavn;
    this.efternavn = p.efternavn;
    this.cpr       = p.cpr;
    // kopiér resten af Persons variabler her...
  }

  public java.util.List hentAnsatte() { return ansatte; }

  // flere metoder og variabler...
}

Problemerne med idéen om at bruge nedarving kan være at:

  1. Personer kan i virkeligheden udfylde flere roller samtidigt.

  2. Personer kan i virkeligheden skifte rolle.

Hvis objektet kan udfylde flere roller

Hvis kombinationer af rollerne kan opstå, sådan at f.eks. en person samtidig er konsulent (aflønningsmæssigt) og leder (m.h.t. Beføjelser), vil ingen af klasserne passe.

Skulle rollerne alligevel modelleres med roller, ville man blive nødsaget til at lave nye nedarvinger for kombinationsrollerne, f.eks. LederOgKonsulent og AnsatOgKonsulent.

Hvis objektet kan skifte rolle

Værre ser det ud, hvis objektet kan skifte rolle (f.eks. fra Ansat til Leder). Da man ikke kan lave om på et objekts type (klasse), når det først er oprettet, er der intet at gøre andet end at oprette et objekt med den nye rolle og kopiere alle fælles data (dem, der stammer fra Person-klassen) fra det oprindelige objekt til det nye.

Skal vi lave en Ansat om til en Leder, kan det altså kun ske ved at oprette en ny Leder og smide det gamle Ansat-objekt væk. Vi må så kode Leder's konstruktør til at kopiere de fælles data (og i øvrigt håbe på, at der ikke er en anden del af programmet, der ændrer i data, efter at vi har kopieret dem).

Situationen med delegering i stedet for arv

I stedet for arv kunne vi modellere rollerne med delegering som vist nedenfor. Nu har Leder, Ansat og Konsulent en reference til et Person-objekt, og alle metodekald, der har med personen at gøre, delegeres videre til Person-objektet.

Rollerne modelleret med delegering

public class LederDelegering
{
  private Person person;
  private java.util.List ansatte;

  // opret en leder
  public LederDelegering()
  {
    person = new Person();
  }

  // opret en leder-rolle med en eksisteren person (mere elegant)
  public LederDelegering(Person p)
  {
    person = p;
  }

  public String hentFornavn() { return person.hentFornavn(); } // delegér

  public java.util.List hentAnsatte() { return ansatte; }

  // flere metoder og variabler...
}

Det er let at lade en Person have flere roller samtidig: Så deles et Leder-objekt og et Konsulent-objekt om et fælles Person-objekt.

Det er også let at lade en Person skifte rolle: Skal vi lave en Ansat om til en Leder, kan det ske ved at oprette en ny Leder og lægge det eksisterende Person-objekt ind i det.

Bemærk, at Leder, Ansat og Konsulent nu ikke mere er af typen Person. Dette er ikke noget problem, hvis man (som eksemplet forudsætter) har separate lister af ledere, ansatte og konsulenter. Har man brug for en fælles superklasse, kunne man definere et interface (eller en abstrakt klasse) PersonIF, som de alle implementerede, og som havde de metoder der var fælles for ledere, ansatte og konsulenter.

15.2.3 Resumé og konklusion

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.

15.3 Specificér funktionalitet i et interface

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.

15.3.1 Repetition af interfaces

I generel sprogbrug er et interface (da.: snitflade) en form for grænseflade, som man gør noget gennem. F.eks. er en grafisk brugergrænseflade de vinduer med knapper, indtastningsfelter og kontroller, som brugeren har til interaktion med programmet.

Vi minder om, at en klasse er definitionen af en type objekter. Her kunne man opdele i

  1. Grænsefladen - hvordan objekterne kan bruges udefra.
    Dette udgøres af navnene3 på metoderne, der kan ses udefra.

  2. Implementationen - hvordan objekterne virker indeni.
    Dette udgøres af variabler og programkoden i metodekroppene.

Et 'interface' svarer til punkt 1): En definition af, hvordan objekter bruges udefra. Man kan sige, at et interface er en "halv" klasse.

Et interface er en samling navne på metoder (uden krop)

Et interface kan implementeres af en klasse - det vil sige, at klassen definerer alle interfacets metoder sammen med programkoden, der beskriver, hvad der skal ske, når metoderne kaldes.

15.3.2 Eksempel: Stak

Grænsefladen til stakken (beskrevet i afsnit 15.2) kunne være defineret i et interface:

public interface Stak
{
  public void lægPå(Object o);
  public Object tagAf();
}

Der kan være mange klasser, der implementerer Stak, f.eks. de to fra forrige afsnit:

public class StakMedNedarving2 extends java.util.Vector implements Stak
{
  public void lægPå(Object o)
  {
    addElement(o);
  }

  public Object tagAf()
  {
    Object o = lastElement();
    setSize( size() - 1 );
    return o;
  }
}

og:

public class StakMedDelegering2 implements Stak
{
  private java.util.Vector v = new java.util.Vector();

  public void lægPå(Object o)
  {
    v.addElement(o);
  }

  public Object tagAf()
  {
    Object o = v.lastElement();
    v.setSize( v.size() - 1 );
    return o;
  }
}

Et enkelt sted i programmet er det nødvendigt at beslutte, hvilken implementation af Stak der anvendes, nemlig når objektet oprettes:

  Stak s = new StakMedNedarving2();

Resten af programmet arbejder bare med objekter af typen Stak uden at vide noget om, hvordan de er implementeret, f.eks.:

  ...
  System.out.print( s.tagAf() );
  System.out.print( s.tagAf() );
  fyldStakkenOp(s);
  ...

Også metoder, f.eks. fyldStakkenOp(), kan arbejde på ethvert objekt, der implementerer Stak-interfacet:

  public void fyldStakkenOp(Stak s)
  {
    s.lægPå("Hej");
    s.lægPå("med");
    s.lægPå("dig");
  }

Her er et klassediagram, der illustrerer, hvordan en klient (en klasse eller et program) benytter noget funktionalitet beskrevet i Stak. Metoder er ikke vist i diagrammet.

Pilen fra Klient til Stak viser, at klienten har en instans af Stak (en variabel af type Stak, der jo kan pege på alle objekter, der implementerer Stak-interfacet).

Stakimpl1 og Stakimpl2 er to klasser, der implementerer Stak-interfacet4 (nedarvingspilen er tegnet med stiplet linje for at markere, at der er tale om implementation af interface, ikke nedarving), f.eks.: StakMedNedarving og StakMedDelegering.

15.3.3 Eksempel: Collections-klasserne

Funktionaliteten i datastrukturerne i Collections-klasserne er alle specificerede i interfaces, og brugeren opfordres til at henvise så lidt til implementationerne (som f.eks. ArrayList og LinkedList) som muligt og så meget til interfacene (som f.eks. List) som muligt:

  List l = new ArrayList();
  
  // ... resten af programmet arbejder bare med en List-variabel
  //     uden at kende til den konkrete implementation af listen

Det gør det lettere senere at udskifte implementationen af datastrukturen med en anden med samme funktionalitet, men anderledes indre struktur, f.eks. udskifte ArrayList med LinkedList:

  List l = new LinkedList();
  
  // ... resten af programmet arbejder bare med en List-variabel
  //     uden at kende til den konkrete implementation af listen

Se også afsnit 1.1.2, Interfacene til lister og mængder.

15.4 Konstanterklæringer i et interface

Det følgende er et lille fif i Java, der kan gøre programkoden mere læselig (i C++ har man mulighed for at erklære globale konstanter, så her er fiffet ikke relevant):

Ved at erklære konstanter, der skal bruges fra flere klasser i et interface, kan klasser, der har brug for disse konstanter, implementere interfacet og derefter bruge konstanterne uden at skulle skrive et klassenavn foran.

Eksempelvis kunne vi definere et interface med nogle tilstande:

public interface Tilstandsliste
{
  int IKKE_STARTET = 0;
  int STARTER = 1;
  int I_GANG = 2;
  int STOPPER = 3;
  int STOPPET = 4;
}

Bemærk, at variabler i et interface automatisk erklæres public static final (klassevariabel, der kan aflæses af alle, men ikke ændres).

Nu kunne vi selvfølgelig bruge konstanterne, som man plejer, ved at skrive klassen foran:

public class BenytTilstande1
{
  public static void main(String[] args)
  {
    int tilstand = Tilstandsliste.IKKE_STARTET;

    while (tilstand != Tilstandsliste.STOPPET) {
      System.out.println("tilstand = "+tilstand);
      tilstand++;
    }
  }
}

... men i klasser, der implementerer Tilstandsliste, kan det skrives kortere:

public class BenytTilstande2 implements Tilstandsliste
{
  public static void main(String[] args)
  {
    int tilstand = IKKE_STARTET;
    
    while (tilstand != STOPPET) {
      System.out.println("tilstand = "+tilstand);
      tilstand++;
    }
  }
}

15.5 Markeringsinterface

Det følgende bliver brugt en del i Javas standardbibliotek og er derfor værd at nævne (de defineres sjældent uden for standardbiblioteket).

Et markeringsinterface erklærer ingen metoder, men markerer et eller andet.
En klasse mærkes ved at implementere markeringsinterfacet.

Et markeringsinterface (eng.: marker interface) er et interface uden nogen metoder (eller variabler). Det tjener kun til at mærke klasser, sådan at deres objekter kan genkendes og behandles særskilt af et eller andet system.

Vigtigste eksempel fra standardbiblioteket på et markeringsinterface er Serializable, der tjener til at markere, om en klasse må serialiseres.

Et andet er Cloneable (der markerer, om det er lovligt at kopiere et objekt ved at kalde dets clone()-metode).

Et tredje er Remote i pakken java.rmi, der markerer, om et objekt er et 'fjernobjekt' i RMI (og dermed, om det skal forblive på serveren, og klienten skal have en fjernreference til det, eller det skal serialiseres, og indholdet transporteres over til klienten, som da får en kopi af objektet).

15.6 Ansvarsområder

En grundidé inden for objektorienteret design er, at hvert objekt skal have et eller flere ansvarsområder.

Ansvarsområder for et objekt kan være:

  • At oprette nye objekter eller udføre en beregning

  • At foretage en handling i andre objekter

  • At kontrollere og koordinere aktiviteter i andre objekter

  • At kende private data

  • At kende relaterede objekter

  • At kende ting, som objektet kan beregne

15.6.1 Eksempel på tildeling af ansvarsområder

Et eksempel på, hvordan man tildeler objekter ansvarsområder, er vist i neden for (fra bogen 'Objektorienteret programmering i Java', http://javabog.dk, kapitel 4):

public class Terning
{
  public int værdi;

  public Terning()
  {
    kast();
  }

  public void kast()
  {
    double tilfældigtTal = Math.random();
    værdi = (int) (tilfældigtTal * 6 + 1);
  }

  public String toString()
  {
    String svar = ""+værdi;
    return svar;
  }
}

Et terning-objekt har ansvaret for det, som vedrører den enkelte terning, dvs. ansvar for at vise den enkelte ternings værdi og at kaste den enkelte terning.

Et raflebæger-objekt har ansvar for alt det, som vedrører alle terningerne, dvs. oprettelsen af terningerne, kaste terningerne, kende summen af deres værdier og beskrive bægerets indhold.

Man kunne godt nøjes med én klasse og så have al koden i den, men en opdeling i forskellige klasser og dermed objekter med hvert sit ansvarsområde gør programmet lettere at overskue.

import java.util.*;

public class Raflebaeger
{
  public Vector terninger;            // Raflebaeger har en liste af terninger

  public Raflebaeger(int antalTerninger)
  {
    terninger = new Vector();
    for (int i=0;i<antalTerninger;i++)
    {
      Terning t;
      t = new Terning();
      tilføj(t);
    }
  }

  public void tilføj(Terning t)       // Læg en terning i bægeret
  {
    terninger.addElement(t);
  }

  public void ryst()                // Kast alle terningerne
  {
    for (int i=0;i<terninger.size();i++) 
    {
      Terning t;
      t = (Terning) terninger.elementAt(i);
      t.kast();
    }
  }

  public int sum()                      // Summen af alle terningers værdier
  {
    int resultat;
    resultat=0;
    for (int i=0;i<terninger.size();i++) 
    {
      Terning t;
      t = (Terning) terninger.elementAt(i);
      resultat = resultat + t.værdi;
    }
    return resultat;
  }
  
  public int antalDerViser(int værdi) // Antal terninger med en bestemt værdi
  {
    int resultat;
    resultat = 0;
    for (int i=0;i<terninger.size();i++) 
    {
      Terning t;
      t = (Terning) terninger.elementAt(i);
      if (t.værdi==værdi) 
      {
        resultat = resultat + 1;
      }
    }
    return resultat;
  }

  public String toString ()           // Beskriv bægerets indhold
  {
    // (vektorens toString() kalder toString() på hver terning)
    return terninger.toString();
  }
}

15.6.2 Ekspert

Hvilke ansvarsområder skal de enkelte klasser have?

Tildel den klasse, der har den nødvendige information til at udføre handlingen, ansvaret for handlingen.

I raflebæger-eksemplet har Terning-objekterne ansvar for de ting, der har med den enkelte terning at gøre, og Raflebægeret har ansvar for de ting, der vedrører alle terningerne, da bægeret kender terningerne.

15.6.3 Skaber

Hvem skal være ansvarlig for at oprette nye instanser (objekter) af en given klasse?

En klasse A skal være ansvarlig for at oprette nye instanser af en anden klasse B, hvis en eller flere af de følgende ting gælder:

  • A indeholder objekter af typen B

  • A består af objekter af typen B

  • A kender de data, som B skal initialiseres med

  • A bruger objekter af typen B meget

I raflebæger-eksemplet skal et objekt af typen Raflebaeger være ansvarlig for at oprette Terning-objekterne, da et raflebæger indeholder terninger, og raflebægeret bruger Terning-objekterne meget.

Kapitel 16, Skabende designmønstre, handler i øvrigt om oprettelse af objekter.

15.7 Lav kobling og høj kohæsion

Når man designer sit program, må man gøre sig klart, hvilke dele af programmet der vil være hyppige udvidelser og omstruktureringer i fremtiden.

Disse dele skal gerne være kendetegnet af lav kobling udadtil og høj kohæsion indadtil.

15.7.1 Lav kobling

Hvordan opnås det, at klasserne i et program bliver lette at udskifte, og eventuelt genbruge?

Koblingen (bindingen) mellem klasser er et mål for, hvor afhængige klasserne er af hinanden. Programmer med høj kobling er kendetegnet ved, at ændringer i en klasse har stor indflydelse på de øvrige dele af programmet, og at koden i den enkelte klasse er svær at forstå isoleret set.

Ved at sørge for, at klasserne har så lille afhængighed af hinanden som muligt, kan overskueligheden af programmet forøges, og det bliver lettere at udskifte dele af programmet.

Det er især vigtigt, hvis man overvejer at genbruge noget af programkoden i en anden sammenhæng, at man sørger for, at bindingerne mellem disse to dele er så lav som mulig.

Lav kobling giver ofte flere fordele ud over genanvendelighed:

  • Fleksibilitet - den anvendte del kan lettere ændres eller udskiftes med noget andet.

  • Overskuelighed - delene kan forstås uafhængigt af hinanden.

Måder at få lav kobling

  • Lad klasser have referencer til så få andre klasser som muligt.

  • Specificér vigtig funktionalitet i interfaces, og lad variabler (og parametre) være af interfacets type for at undgå at lægge dig fast på en bestemt implementation (dette er beskrevet i afsnit 15.3).

  • Tildel den enkelte klasse et let forståeligt og ensartet ansvarsområde (høj kohæsion - se afsnit 15.7.2).

Høj kobling

Det modsatte, høj kobling, betyder, at programdelene er afhængige af hinanden, så man har et fastlåst program, hvor delene ikke kan isoleres fra helheden og anvendes i en anden sammenhæng.

Høj kobling opstår typisk, hvis ansvarsområderne ikke bliver ordentligt klarlagt. Et kendetegn på udflydende ansvarsområder er, at der er mange klasser involveret i at varetage en bestemt opgave (spaghetti-programmering). Koden vil ofte være svært forståelig, fordi man skal sætte sig ind i alle de forskellige klasser, som tager del i udførelsen af opgaven.

Høj kobling kan også opstå ved meget genbrug (arv er meget høj kobling) eller hvis man har mange små metoder, der kalder hinanden meget.

15.7.2 Høj kohæsion

Høj kohæsion (sammenhæng - eng.: high cohesion) vil sige, at et objekt har et overskueligt og let forståeligt ansvarsområde, eller eventuelt flere ansvarsområder, der er tæt relaterede til hinanden. Jo flere forskellige ansvarsområder et objekt/en klasse har, jo sværere bliver det at genbruge objektet/klassen.

15.7.3 Indkapsling

Den nemmeste måde at sikre lav kobling er ved at gøre metoder og variabler, man ikke ønsker brugt udefra, utilgængelige udefra.

Vi minder om at:

  • Variabler og metoder erklæret public altid er tilgængelige inden for og uden for klassen.

  • Variabler og metoder erklæret protected er tilgængelige for alle klasser inden for samme pakke. Klasser i andre pakker kan kun få adgang, hvis de er nedarvinger.

  • Skriver man ingenting, er det kun klasser i samme pakke, der har adgang til variablen eller metoden.

  • Hvis en variabel eller metode er erklæret private, kan den kun benyttes inden for samme klasse (og kan derfor ikke tilsidesættes i en nedarving). Det er det mest restriktive.

Adgangen kan sættes på skemaform:

Adgang

public

protected

(ingenting)

private

i samme klasse

ja

ja

ja

ja

klasse i samme pakke

ja

ja

ja

nej

nedarving i en anden pakke

ja

ja

nej

nej

ej nedarving og i en anden pakke

ja

nej

nej

nej

Holder man sig inden for samme pakke, er der altså ingen forskel mellem public, protected og ingenting.

15.7.4 Indkapsling og pakker

Ud af de ovenstående regler kan man konkludere, at adgangskontrol ud over public/private først bliver interessant, når programmet spænder over flere pakker.

Så kan klasserne inden for samme pakke virke nært sammen (f.eks. ændre i hinandens interne variabler), mens adgangen fra klasserne i de andre pakker er begrænset.

For at indkapsle en gruppe klasser, sådan at de kan tilgå hinandens metoder, mens disse metoder ikke er synlige udefra, er man nødt til at lægge dem i en pakke for sig

Eksempel

Kigger man nærmere på Funktion-klassen i afsnit 4.7.2, Repræsentation af funktioner, ser man, at dens forskellige nedarvinger (X, Konst, Sin etc.) ikke er erklæret public, men ingenting. Disse klasser er derfor kun tilgængelige for klasser, der ligger i den samme pakke (pakken vp.funktion), såsom klassen Funktionsfortolker beskrevet i afsnit 4.7.3, Fortolkning af strenge til funktioner. Samtidig er konstruktøren til Funktion erklæret på pakkeniveau, hvilket gør, at kun klasser i samme pakke kan arve fra Funktion.

Alt i alt er al logik omkring funktioner indkapslet i pakken vp.funktion.

Klassen Kurvetegner (afsnit 4.7.1, En komponent til at tegne kurver) ligger i pakken vp og har derfor ikke adgang til nedarvingerne. Den kan derfor ikke selv kombinere Funktion-objekter, men må bruge Funktionsfortolker.

15.8 GRASP

GRASP er nogle grundlæggende tommelfingerregler for, hvordan man skal designe sine klasser. GRASP står for: General Responsibility Assignment Software Patterns (og selve navnet GRASP angiver samtidig, at det er noget, der er lige til at tage og bruge).

De forrige afsnit har beskrevet en del af disse tommelfingerregler, mens andre bliver beskrevet senere i kapitlerne omkring designmønstre.

De 4 af i alt 9 GRASP-regler, der er beskrevet i denne bog, er:

  • Ekspert (eng.: Information Expert) - beskrevet i afsnit 15.6.2.

  • Skaber (eng.: Creator) - beskrevet i afsnit 15.6.3.

  • Lav kobling - beskrevet i afsnit 15.7.1.

  • Høj kohæsion (sammenhæng) - beskrevet i afsnit 15.7.2.

15.9 Introduktion til designmønstre

Et designmønster (eng.: design pattern) er en navngiven beskrivelse af, hvordan et givent problem kan løses og konsekvenserne af denne måde at løse problemet på.

Idéen er, at man måske senere i et andet program kan genbruge det samme designmønster til at løse et lignende problem (selve begrebet mønster angiver jo, at det er noget, som gentages).

Designmønstre hjælper med:

  • at give idéer til godt design, sådan at når man står over for en problemstilling ikke skal "opfinde den dybe tallerken" igen og kan undgå de mest almindelige faldgruber.

  • at give programmører en klar, fælles begrebsramme, der gør det lettere at forklare/dokumentere hvad man har lavet.

Ofte vil designmønstre give idéer til, hvordan man kan formindske graden af bindinger (kobling) mellem forskellige dele af programmet. Programkoden deles ofte op i to dele:

  • Den del, der anvender en bestemt anden del. Denne del kaldes kaldes 'klienten'.

Den del, der anvendes. Denne del er måske generel anvendelig, måske endda en del af et standardbibliotek.

15.9.1 Designmønstre berørt i de følgende kapitler

I kapitel 16, Skabende designmønstre, behandles:

  • Fabrikeringsmetode - en metode, der fabrikerer objekter for klienten (i stedet for at klienten selv opretter dem med new)

  • Fabrik - et objekt med en fabrikeringsmetode

  • Singleton - sikring af, at der kun eksisterer ét objekt af en bestemt slags

  • Abstrakt Fabrik/Toolkit - er en Fabrik med nedarvinger, der sørger for objektoprettelsen. Hvilken nedarving der anvendes, bestemmes af en fabrikeringsmetode

  • Bygmester - simplificerer oprettelsen af nogle relaterede objekter ved at oprette og konfigurere objekterne (evt. trinvist) for klienten

  • Prototype - objekter oprettes ud fra eksisterende skabelon-objekter

  • Objektpulje - genbrug de samme objekter igen og igen ved at huske dem i en pulje

I kapitel 17, Hyppigt anvendte designmønstre, behandles:

  • Proxy - få metodekald til at gå gennem et mellem-objekt (proxyen), der modtager metodekald på vegne af (fungerer som en erstatning) for det rigtige objekt og kalder videre i det rigtige objekt

  • Herunder Virtuel Proxy/Doven Initialisering - udskyde oprettelsen af det rigtige objekt til første gang, der er brug for det

  • Adapter - et hjælpeobjekt, der får et objekt til at passe ind i et system ved at fungere som omformer mellem objektet og systemet

  • Iterator - hjælper med at gennemløbe nogle data

  • Facade - forenkler brugen af et sæt objekter ved at give en simplificeret grænseflade til dem

  • Observatør/Lytter - at objekter kan 'abonnere' på, at en ting (hændelse) sker

  • Dynamisk Binding - at understøtte "plugin"-klasser, der kan indlæses under kørslen og dermed udvide programmet, efter at det er skrevet

I kapitel 18, Andre designmønstre, behandles:

  • Uforanderlig - objektet kan ikke ændres, når det først er oprettet

  • Fluevægt - begræns antallet af objekter ved at sørge for, at der ikke bliver oprettet objekter med de samme data. I stedet er der mange referencer til de samme (unikke) objekter

  • Filter - objekter, der "filtrerer" en strøm af data. Filtrene kan kombineres vilkårligt

  • Lagdelt Initialisering - klienten opretter et objekt direkte, og dette objekt opretter eller fremskaffer det, der i virkeligheden skal bruges og videredelegerer det meste af arbejdet til det

  • Komposit/Rekursiv Komposition - skabe et meget fleksibelt objekthierarki, ved at definere objekter, der kan indeholde andre objekter inkl. sin egen slags

  • Kommando - registrér brugeren ændringer af data, sådan at de kan fortrydes igen.

I kapitel 19, Model-View-Controller-arkitekturen, behandles et designmønster beregnet til programmer med en brugergrænseflade, der anbefaler at man opdeler programmet i en datamodel, som repræsenterer data, forretningslogikken og de bagvedliggende beregninger, en præsentation af data over for brugeren, og en kontrol-del, der giver brugeren mulighed for at ændre i data.

1Ordene "amerikansk" og "skandinavisk" tankegang bruges bl.a. i et citat af Erik Ernst i artiklen "The Origins of Object Orientation", som kan findes på http://ootips.org/history.html

2Se f.eks. kapitel 1, Samlinger af data, hvor ArrayList og LinkedList let kan skiftes ud med hinanden, fordi deres fælles funktionalitet er defineret i interfacet List.

3Egentlig signaturen, dvs. metodenavn og antal og type af parametre.

4Det kunne f.eks. være StakMedNedarving og StakMedDelegering.

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.