6.3. Eksempel: Et matador-spil

Med arv kan man skabe et hierarki af klasser, der ligner hinanden (fordi de har alle fællestrækkene fra superklassen) og samtidig kan opføre sig forskelligt (polymorfi).

Her er vist klassediagrammet fra et matadorspil. Det er en skitse, et rigtigt matadorspil ville indeholde flere detaljer.

Figur 6-6. Java

Øverst har vi klassen Felt, som indeholder fællestrækkene for alle matadorspillets felter. F.eks. skal alle felter kunne håndtere, at spilleren lander på eller passerer feltet. Vi forestiller os, at metoderne landet() og passeret() bliver kaldt af en anden del af programmet, når en spillers brik henholdsvis lander på eller passerer feltet. I Felt-klassen er metoderne defineret til ikke at gøre noget. Alle felter har også et navn, f.eks "Hvidovrevej".

// Superklassen for alle matadorspillets felter

public class Felt
{
  String navn;

  public void passeret(Spiller sp) // kaldes når en spiller passerer dette felt
  {
    System.out.println(sp.navn+" passerer "+navn);
  }

  public void landet(Spiller sp)   // kaldes når en spiller lander på dette felt
  {
  }
}

Læg mærke til, at der er forskel mellem sp.navn (spillerens navn) og navn (Felt-objektets navn).

Under Felt har vi klasserne Helle, Start, Rederi og Gade, der indeholder data og programkode, der er specifik for de forskellige slags felter i matadorspillet. De arver alle fra Felt og er derfor tegnet med en er-en-relation til Felt.

Klassen Helle er simpel; den skal lægge 15000 kr. til spillerens kassebeholdning, hvis spilleren lander på feltet. Dette gøres ved at tilsidesætte den nedarvede passeret()-metode med en, der overfører penge til spilleren.

// Helle. Hvis man lander her får man en gevinst.

public class Helle extends Felt
{
  double gevinst;

  public Helle (int gevinst)
  {
    navn="Helle";                   // navn er arvet fra Felt
    this.gevinst=gevinst;
  }

  public void landet(Spiller sp)    // tilsidesæt metode i Felt
  {
    System.out.println(sp.navn+" lander på helle, og får overført "+gevinst);
    sp.transaktion(gevinst);        // opdater spillers konto
    System.out.println(sp.navn+"s konto lyder nu på "+sp.konto);
  }
}

I konstruktøren sætter vi feltets navn. Gevinsten ved at lande her er en parameter til konstruktøren. Metodekaldet sp.transaktion(gevinst) beder spiller-objektet om at føje gevinsten til kontoen.

Klassen Start skal overføre 5000 kr. til spilleren, der passerer eller lander på feltet. Dette gøres ved at tilsidesætte både landet() og passeret().

// Startfeltet

public class Start extends Felt
{
  double gevinst;

  public Start(double gevinst)
  {
    navn="Start";
    this.gevinst=gevinst;
  }

  public void passeret(Spiller sp)                 // tilsidesæt metode i Felt
  {
    System.out.println(sp.navn+" passerer start og modtager "+gevinst);
    sp.transaktion(gevinst);                       // kredit/debit af konto
    System.out.println(sp.navn+"s konto lyder nu på "+sp.konto);
  }

  public void landet(Spiller sp)                   // tilsidesæt metode i Felt
  {
    System.out.println(sp.navn+" lander på start og modtager "+gevinst);
    sp.transaktion(gevinst);
    System.out.println(sp.navn+"s konto lyder nu på "+sp.konto);
  }
}

Nu kommer vi til felter, der kan ejes af en spiller, nemlig rederier og gader. De har en ejer-variabel, der refererer til en Spiller (og er derfor tegnet med en har-en-relation til klassen Spiller), en pris og en leje for at lande på grunden.

// Rederier

public class Rederi extends Felt
{
  Spiller ejer;
  double pris;
  double grundleje;

  public Rederi(String navn, double pris, double leje)
  {
    this.navn = navn;
    this.pris = pris;
    this.grundleje = leje;
  }

  public void landet(Spiller sp)
  {
    System.out.println(sp.navn+" er landet på "+navn);
    if (sp==ejer)
    {                                       // spiller ejer selv grunden
      System.out.println("Dette er "+sp.navn+"s egen grund");
    }
    else if (ejer==null)
    {                                       // ingen ejer grunden, så køb den
      if (sp.konto > pris)
      {
        System.out.println(sp.navn+" køber "+navn+" for "+pris);
        ejer=sp;
        sp.transaktion( -pris );
      }
      else System.out.println(sp.navn+" har ikke penge nok til at købe "+navn);
    }
    else
    {                                       // feltet ejes af anden spiller
      System.out.println("Husleje: "+grundleje);
      sp.betal(ejer, grundleje);            // spiller betaler til ejeren
    }
  }
}

Når en spiller lander på et rederi, skal der overføres penge fra spilleren til ejeren af grunden. Dette gøres ved at tilsidesætte den nedarvede landet()-metode med en, der overfører beløbet mellem parterne. Først tjekkes om spilleren er den samme som ejeren (sp==ejer). Hvis dette ikke er tilfældet, tjekkes om der ingen ejer er (ejer==null), og hvis der ikke er, kan spilleren købe grunden (ejer sættes lig spilleren). Ellers beordres spilleren til at betale et beløb til ejeren: sp.betal(ejer, grundleje).

Klassen Gade repræsenterer en byggegrund, og objekter af type Gade har derfor, ud over ejer, pris og grundleje, en variabel, der husker, hvor mange huse der er bygget på dem.

Når en spiller lander på grunden, skal der ske nogenlunde det samme som for et Rederi bortset fra, at hvis det er ejeren der lander på grunden, kan han bygge et hus.

// En gade der kan bebygges

public class Gade extends Felt
{
  Spiller ejer;
  double pris;
  double grundleje;
  int antalHuse;
  double huspris;

  public Gade(String navn, double pris, double leje, double huspris)
  {
    this.navn=navn;
    this.pris=pris;
    this.grundleje=leje;
    this.huspris=huspris;
    antalHuse = 0;
  }

  public void landet(Spiller sp)
  {
    System.out.println(sp.navn+" er landet på "+navn);

    if (sp==ejer)
    {                                        // eget felt
      System.out.println("Dette er "+sp.navn+"s egen grund");
      if (antalHuse<5 && sp.konto>huspris)
      {                                     // byg et hus
        System.out.println(ejer.navn+" bygger et hus på "+navn+" for "+huspris);
        ejer.transaktion( -huspris );
        antalHuse = antalHuse + 1;
      }
    }
    else if (ejer==null)
    {                                        // ingen ejer grunden, så køb den
      if (sp.konto > pris)
      {
        System.out.println(sp.navn+" køber "+navn+" for "+pris);
        ejer=sp;
        sp.transaktion( -pris );
      }
      else System.out.println(sp.navn+" har ikke penge nok til at købe "+navn);
    }
    else
    {                                        // felt ejes af anden spiller
      double leje = grundleje + antalHuse * huspris;
      System.out.println("Husleje: "+leje);
      sp.betal(ejer, leje);                 // spiller betaler til ejeren
    }
  }
}

Et spil kunne opbygges ved at lægge forskellige felter ind i en vektor for at få et bræt:

// Matadorspil for to spillere
import java.util.*;

public class SpilMatador
{
  public static void main(String[] args)
  {
    Spiller sp1=new Spiller("Søren",50000);   // opret spiller 1
    Spiller sp2=new Spiller("Gitte",50000);   // opret spiller 2

    Vector felter=new Vector();               // indeholder alle felter
    felter.addElement(new Start(5000));
    felter.addElement(new Gade("Gade 1",10000, 400,1000));
    felter.addElement(new Gade("Gade 2",10000, 400,1000));
    felter.addElement(new Gade("Gade 3",12000, 500,1200));
    felter.addElement(new Rederi("Maersk",17000,4200));
    felter.addElement(new Gade("Gade 5",15000, 700,1500));
    felter.addElement(new Helle(15000));
    felter.addElement(new Gade("Gade 7",20000,1100,2000));
    felter.addElement(new Gade("Gade 8",20000,1100,2000));
    felter.addElement(new Gade("Gade 9",30000,1500,2200));

    // løb igennem 20 runder
    for (int runde = 0; runde<20; runde=runde+1)
    {
      sp1.tur(felter);
      sp2.tur(felter);
    }
  }
}

Man kan så lave en simpel tur()-metode, der rykker en spiller rundt på felterne ved at hente objekterne i vektoren, reference-typekonvertere dem til Felt og kalde objekternes passeret()-metode og landet()-metoden på det sidste objekt.

Denne tur()-metode placerer vi i klassen Spiller sammen med oplysningerne om spilleren.

// Definition af en spiller

import java.util.*;

public class Spiller
{
  String navn;
  double konto;
  int feltnr;

  public Spiller(String navn, double konto)
  {
    this.navn=navn;
    this.konto=konto;
    feltnr = 0;
  }

  public void transaktion(double kr)
  {
    konto = konto + kr;
  }

  public void betal(Spiller modtager, double kr)
  {
    System.out.println(navn+" betaler "+modtager.navn+": "+kr+" kr.");
    modtager.transaktion(kr);
    transaktion(-kr);
  }

  public void tur(Vector felter)
  {
    int slag=(int)(Math.random()*6)+1;                // terningkast
    System.out.println("***** "+navn+" på felt "+feltnr+" slår "+slag+" *****");

    // nu rykkes der
    for (int i=1;i<=slag;i=i+1)
    {
      // gå til næste felt: tæl op, hvis vi når over antal felter så tæl fra 0
      feltnr = (feltnr + 1) % felter.size();
      Felt felt;
      felt = (Felt) felter.elementAt(feltnr);
      if (i<slag) felt.passeret(this);  // kald passer() på felter vi passerer
      else felt.landet(this);           // kald land() på sidste felt
    }
    try {Thread.sleep(3000);} catch (Exception e) {}  // vent 3 sek.
  }
}

Fidusen er, at denne tur()-metode kan skrives uafhængigt af, hvilke felt-typer der findes: tur()-metoden kalder automatisk de rigtige landet()- og passeret()-metoder, selvom den kun kender Felt-klassen.

Bemærk i øvrigt, hvordan vi med this overfører en reference til spilleren selv når vi kalder passeret() og landet() på Felt-objekterne.

Linjen

    try {Thread.sleep(3000);} catch (Exception e) {}

får programmet til at holde en pause i tre sekunder inden det går videre (try og catch vil blive forklaret i kapitlet om undtagelser).

Bemærk også hvordan vi sørger for, at variablen feltnr forbliver at have en værdi mellem 0 og antallet af felter med operatoren %, der giver resten af en division (se Kapitel 3).

Her ses uddata af en kørsel af programmet:

***** Søren på felt 0 slår 3 *****
Søren passerer Gade 1
Søren passerer Gade 2
Søren er landet på Gade 3
Søren køber Gade 3 for 12000.0
***** Gitte på felt 0 slår 5 *****
Gitte passerer Gade 1
Gitte passerer Gade 2
Gitte passerer Gade 3
Gitte passerer Maersk
Gitte er landet på Gade 5
Gitte køber Gade 5 for 15000.0
***** Søren på felt 3 slår 2 *****
Søren passerer Maersk
Søren er landet på Gade 5
Husleje: 700.0
Søren betaler Gitte: 700.0 kr.
***** Gitte på felt 5 slår 4 *****
Gitte passerer Helle
Gitte passerer Gade 7
Gitte passerer Gade 8
Gitte er landet på Gade 9
Gitte køber Gade 9 for 30000.0
***** Søren på felt 5 slår 1 *****
Søren er landet på helle, og får overført 15000.0
Sørens konto lyder nu på 52300.0
***** Gitte på felt 9 slår 5 *****
Gitte har passeret start og modtager 5000.0
Gittes konto lyder nu på 10700.0
Gitte passerer Gade 1
Gitte passerer Gade 2
Gitte passerer Gade 3
Gitte er landet på Maersk
Gitte har ikke penge nok til at købe Maersk
***** Søren på felt 6 slår 1 *****
Søren er landet på Gade 7
Søren køber Gade 7 for 20000.0
***** Gitte på felt 4 slår 1 *****
Gitte bygger et hus på Gade 5 for 1500.0
Gitte er landet på Gade 5
Dette er Gittes egen grund
***** Søren på felt 7 slår 1 *****
Søren er landet på Gade 8
Søren køber Gade 8 for 20000.0
***** Gitte på felt 5 slår 4 *****
Gitte passerer Helle
Gitte passerer Gade 7
Gitte passerer Gade 8
Gitte bygger et hus på Gade 9 for 2200.0
Gitte er landet på Gade 9
Dette er Gittes egen grund
***** Søren på felt 8 slår 2 *****
Søren passerer Gade 9
Søren er landet på start og modtager 5000.0
Sørens konto lyder nu på 17300.0
***** Gitte på felt 9 slår 1 *****
Gitte er landet på start og modtager 5000.0
Gittes konto lyder nu på 12000.0
***** Søren på felt 0 slår 3 *****
Søren passerer Gade 1
Søren passerer Gade 2
Søren bygger et hus på Gade 3 for 1200.0
Søren er landet på Gade 3
Dette er Sørens egen grund
***** Gitte på felt 0 slår 5 *****
Gitte passerer Gade 1
Gitte passerer Gade 2
Gitte passerer Gade 3
Gitte passerer Maersk
Gitte bygger et hus på Gade 5 for 1500.0
Gitte er landet på Gade 5
Dette er Gittes egen grund
***** Søren på felt 3 slår 5 *****
Søren passerer Maersk
Søren passerer Gade 5
Søren passerer Helle
Søren passerer Gade 7
Søren bygger et hus på Gade 8 for 2000.0
Søren er landet på Gade 8
Dette er Sørens egen grund
***** Gitte på felt 5 slår 1 *****
Gitte er landet på helle, og får overført 15000.0
Gittes konto lyder nu på 25500.0
***** Søren på felt 8 slår 3 *****
Søren passerer Gade 9
Søren har passeret start og modtager 5000.0
Sørens konto lyder nu på 19100.0
Søren er landet på Gade 1
Søren køber Gade 1 for 10000.0

... (og så videre)

6.3.1. Polymorfi

Polymorfi vil sige, at objekter af forskellig type bruges på en ensartet måde uden hensyn til deres præcise type.

Matadorspillet udnytter polymorfi til at behandle alle feltobjekter ens (ved at kalde landet() og passeret() fra Spiller's tur()-metode), selvom de er af forskellig type.

Polymorfi er et kraftfuldt redskab til at lave meget fleksible programmer, der senere kan udvides, uden at der skal ændres ret meget i den eksisterende kode.

For eksempel kan vi til enhver tid udbygge matadorspillet med flere felttyper uden at skrive programmet om. Den programkode, der arbejder på felterne, Spiller-klassens tur()-metode, kender faktisk slet ikke til andre klasser end Felt!

En forudsætning for at udnytte polymorfi-mekanismen er, at objekterne "sørger for sig selv", dvs. at data og programkode er i de objekter, som de handler om.