Ohjelmointitekniikka (Java) -materiaalia © Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

Ohjelmointitekniikka (Java): esimerkkejä suunnittelumalleista

(Muutettu viimeksi 5.2.2004)

Nämä esimerkit ovat peräisin Kai Koskimiehen teoksesta Oliokirja, Suomen Atk-kustannus Oy, 2000. Ne on tässä julkaistu Koskimiehen luvalla. Joitakin yksityiskohtia on paikoin vähän muutettu, kommentteja ja tulostuksia lisäilty.

Abstrakti tehdas (Abtract factory)

Luontimalli (Creational Pattern)

Henkilöt käyttavät vaatteinaan takkia ja housuja, miehet ja naiset erilaisia. Tarvitaan luokka Henkilo, joka sisältää vaatteiden hankinnan ja käytön ottamatta kantaa, onko kyseessä nainen vai mies. Jokaisella henkilöllä on joko naisen tai miehen vaatteet, ei sekaisin molempia. Abstraktin tehtaan tuotteet ovat siis takki ja housut. (Esimerkin luokat löytyvät käännösyksiköistä Henkilo.java, Nainen.java ja TestaaNainen.java.)


interface Takki {
   void napita();
   // ...
}

interface Housut {
  void panePäälle();
  // ...
}

interface Vaateluoja {
  Takki  teeTakki();
  Housut teeHousut();
}

abstract class Henkilo {

  protected String nimi;
  private int ikä;

  private Takki takki;           // Näihin joko naisen tai miehen
  private Housut housut;         // vaatteet.

  protected Vaateluoja vaatturi; // Joko naisen tai miehen vaatturi
                                 // asetetaan ei-abstr. aliluokassa.
  public Henkilo(String n) {
    nimi = n;
    ikä = 0;
  } 

  public void hankiVaatteet() {    // Hankinta
    takki  = vaatturi.teeTakki();
    housut = vaatturi.teeHousut();
  }

  public void käytäVaatteita() {   // Käyttö
    // ...
    housut.panePäälle();
    // ...
    takki.napita();
  }

  public void vanhene() {++ikä;}

  public abstract void tervehdi();
}
Nämä luokat eivät ole mitenkään riippuvaisia siitä, onko kyseessä nainen tai mies; ne eivät itse asiassa tiedä mitään takin, housujen tai vaatturin toteutuksesta.

Toteutuksia:

class NaisTakki implements Takki {
  public void napita() {
     // ... napita naisnapitus ...
     System.out.println("Nainen napittaa.");
  }
}

class NaisHousut implements Housut {
  public void panePäälle() {
     // ... pue naisen housut ...
     System.out.println("Nainen laittaa housut.");
  }
}

class NaisVaatturi implements Vaateluoja {

  public Takki teeTakki() {
    return new NaisTakki();
  }
  public Housut teeHousut() {
    return new NaisHousut();
  }
}

Ja viimein ei-abstrakti aliluokka Henkilo-luokalle:
class Nainen extends Henkilo {

  public Nainen(String n) {
    super(n);
    vaatturi = new NaisVaatturi();   // Huom!
  }

  public void tervehdi() {
    System.out.println("... niiaus ...");
    System.out.println("Hyvää päivää!");
  }
}
Kokeilua:
public class TestaaNainen {
  public static void main(String[] args) {

    Henkilo möttönen = new Nainen("Maija");
    möttönen.hankiVaatteet();
    möttönen.käytäVaatteita();
    möttönen.vanhene();
    möttönen.tervehdi();
  }
}
Tulostus:
Nainen laittaa housut.
Nainen napittaa.
... niiaus ...
Hyvää päivää!


Kooste (Composite)

Rakennemalli (Strucural Pattern)

Yksittäisille henkilöille ja ruokakunnille annetaan avustuksia. Ruokakunnat muodostuvat henkilöistä ja mahdollisesti aliruokakunnista (sic!). Avustusten jakaja haluaa jakaa avustuksia samalla ohjelmakoodilla sekä yksittäisille henkilöille että (mahdollisesti rekursiivisesti muodostetuille) ruokakunnille. Siis avun tarpeen kysyminen ja avun antaminen tehdään molemmissa tapauksissa aivan samalla tavalla. (Esimerkin luokat löytyvät käännösyksiköistä AvunKohde.java, Henkilo.java, Ruokakunta.java ja TestaaAvunAntoa.java.)


interface AvunKohde {
  int annaTarve();
  void otaApu(int apu);
  int koko();
}

class Henkilo implements AvunKohde {

  protected String nimi;
  protected int ikä;
  private int varasto;

  public Henkilo(String n) {
    nimi = n;
    ikä = 0;
    varasto = 0;
  }

  public void vanhene() {++ikä;}
  public int annaIkä() {return ikä;}

  public void kuluta(int i) {
    if (varasto >= i)
      varasto = varasto - i;
  }

  public int annaTarve() {
    if (ikä < 10) return 100;
    if (ikä < 50) return 200;
    return 100;
  }

  public void otaApu(int apu) {
    varasto = varasto + apu;
     /* testaukseen: */
     System.out.println("Henkilö otti apua " + apu);
  }

  public int koko() {return 1;}

}

Ruokakunta toteutetaan java.util-pakkauksesta löytyvällä Collection-rajapintaluokalla ja sen toteuttavalla LinkedList-oliolla. Ruokakunnan jäsenetkin siis voivat olla Ruokakunta-oliota, koska myös Ruokakunta toteuttaa rajapintaluokan AvunKohde!

import java.util.*;

class Ruokakunta implements AvunKohde {

  private int lkm;
  private Collection jäsenet;

  public Ruokakunta() {
    jäsenet = new LinkedList();
    lkm = 0;
  }

  public void lisääJäsen(AvunKohde h) {
    if (!jäsenet.contains(h)) {
      jäsenet.add(h);
      lkm = lkm + h.koko();
    }
  }

  public void poistaJäsen(AvunKohde h) {
    if (jäsenet.remove(h))
      lkm = lkm - h.koko();
  }

  public int annaTarve() {
    int tarve = 0;
    Iterator iter = jäsenet.iterator();
    while (iter.hasNext()) {
      AvunKohde h = (AvunKohde)iter.next();
      tarve = tarve + h.annaTarve();
    }
    return tarve;
  }

  public void otaApu(int apu) {
    Iterator iter = jäsenet.iterator();
    double apuPerHenkilö = apu/this.koko();    // <- tässä on pieni korjaus(?)
    while (iter.hasNext()) {                   //    Koskimiehen koodiin
      AvunKohde h = (AvunKohde)iter.next();    //
      h.otaApu((int)(h.koko()*apuPerHenkilö)); // <-        "

        /* testaukseen: */
        System.out.println("Per henkilö " + (int)(h.koko()*apuPerHenkilö));

    }
  }

  public int koko() {
    return lkm;
  }
}

Nyt avunantaja voi avustaa sekä yksilöä että (rekursiivisestikin rakentunutta) ryhmää aivan samalla tavalla:

public class TestaaAvunAntoa {
  public static void main(String[] args) {

    Henkilo masa = new Henkilo("Masa");
    for (int i=0; i<20; ++i)
       masa.vanhene();

    System.out.println("Masan avustus:");
    avusta(masa);

    Ruokakunta abc = new Ruokakunta();
    abc.lisääJäsen(new Henkilo("Aapeli"));
    abc.lisääJäsen(new Henkilo("Beepeli"));
    abc.lisääJäsen(new Henkilo("Ceebeli"));

    System.out.println("\nAapelin, Beepelin ja Ceebelin avustus:");
    avusta(abc);

    Ruokakunta porukka = new Ruokakunta();
    porukka.lisääJäsen(new Henkilo("Matti"));
    porukka.lisääJäsen(new Henkilo("Maija"));

    System.out.println("\nMatin ja Maijan avustus:");
    avusta(porukka);

    porukka.lisääJäsen(abc);

    System.out.println("\nABC ovat ryhmänä liittyneet MM:ään:");
    avusta(porukka);
  }

  private static void avusta(AvunKohde avustettava) {
    System.out.println("Tarve on " + avustettava.annaTarve());
    avustettava.otaApu(1000);
  }
}
Tulostus:
Masan avustus:
Tarve on 200
Henkilö otti apua 1000

Aapelin, Beepelin ja Ceebelin avustus:
Tarve on 300
Henkilö otti apua 333
Per henkilö 333
Henkilö otti apua 333
Per henkilö 333
Henkilö otti apua 333
Per henkilö 333

Matin ja Maijan avustus:
Tarve on 200
Henkilö otti apua 500
Per henkilö 500
Henkilö otti apua 500
Per henkilö 500

ABC ovat ryhmänä liittyneet MM:ään:
Tarve on 500
Henkilö otti apua 200
Per henkilö 200
Henkilö otti apua 200
Per henkilö 200
Henkilö otti apua 200
Per henkilö 200
Henkilö otti apua 200
Per henkilö 200
Henkilö otti apua 200
Per henkilö 200
Per henkilö 600

Strategia (Strategy)

Käyttäytymismalli (Behavioural Pattern)

Halutaan voida vaihtaa jotakin olion (tms.) käyttämää algoritmia olion elinkaaren aikana. (Vrt. joidenkin kielten "funktiopointtereihin".)

Henkilön halutaan dynaamisesti vaihtavan tervehtimissanojaan iän myötä: nuorena sanotaan "Moi", vanhempana "Hyvää paivää!. (Esimerkin luokat löytyvät käännösyksiköistä Tervehdyssanat.java, Henkilo.java ja TestaaTervehdykset.java.)


interface Tervehdyssanat {
  void sanat();
}

class NuorenTervehdys implements Tervehdyssanat {
  public void sanat() {
    System.out.println("Moi!");
  }
}

class VanhanTervehdys implements Tervehdyssanat {
  public void sanat() {
    System.out.println("Hyvää päivää!");
  }
}

class Henkilo {

  protected String nimi;
  private int ikä;
  protected Tervehdyssanat terv;

  public Henkilo(String n) {
    nimi = n;
    terv = new NuorenTervehdys();
    ikä = 0;
  }

  public void tervehdi() {
    terv.sanat();
    // ... muuta tervehtimiseen liittyvää ...
  }

  public void vanhene() {
    ++ikä;
    if (ikä == 18)
      terv = new VanhanTervehdys();
  }

}

public class TestaaTervehdykset {
  public static void main(String[] args) {

     Henkilo paavo = new Henkilo("Paavo");
     for (int i=0; i<25; ++i) {
       paavo.vanhene();
       paavo.tervehdi();
     }
  }
}
Tulostus:
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Moi!
Hyvää päivää!
Hyvää päivää!
Hyvää päivää!
Hyvää päivää!
Hyvää päivää!
Hyvää päivää!
Hyvää päivää!
Hyvää päivää!

Silta (Bridge)

Rakennemalli (Strucural Pattern)

Miehet ja naiset tervehtivät eri tavalla ("Terve" ja "Hei hei!"). He myös ilmoittavat ikänsä eri tavalla. Jossakin merkillisessä maailmassa henkilöt aina silloin tällöin vaihtavat sukupuoltaan. Laaditaan luokka Henkilo, jossa on rajapintaluokan Henkilototeutus tyyppinen kenttä, jonka arvoksi voidaan sopivalla aksessorilla asettaa milloin nais-, milloin miestoteutus noille operaatioille. (Esimerkin luokat löytyvät käännösyksiköistä Henkilototeutus.java, Henkilo.java ja TestaaHenkilot.java.)


interface Henkilototeutus {
  void terve();
  int ikäMuunnos(int i);
}

class Miestoteutus implements Henkilototeutus {
  public void terve() {
    System.out.println("Terve");
  }
  public int ikäMuunnos(int i) {
    return i;
  }
}

class Naistoteutus implements Henkilototeutus {
  private final int maxIkä = 29;

  public void terve() {
    System.out.println("Hei hei");
  }
  public int ikäMuunnos(int i) {
    if (i > maxIkä)
      return maxIkä;
    else
     return i;
  }
}

class Henkilo {

  protected String nimi;
  protected int ikä;
  private Henkilototeutus ht;

  public Henkilo(String n, Henkilototeutus t) {
    nimi = n;
    ikä = 0;
    ht = t;
  }

  public void muutaToteutus(Henkilototeutus t) {
    ht = t;
  }

  public void tervehdi() {
    ht.terve();
    System.out.println("Nimeni on " + nimi);
  }

  public void vanhene() {++ikä;}

  public int annaIkä() {
    return ht.ikäMuunnos(ikä);
  }
}

public class TestaaHenkilot {
  public static void main(String[] args) {

    Naistoteutus nt = new Naistoteutus();
    Miestoteutus mt = new Miestoteutus();

    Henkilo h1 = new Henkilo("Vieno", nt); // tästä tuli nainen
    Henkilo h2 = new Henkilo("Kaino", mt); // tästä tuli mies

    h1.tervehdi();
    h2.tervehdi();

    h2.muutaToteutus(nt);
    h2.tervehdi();
  }
}
Tulostus:
Hei hei
Nimeni on Vieno
Terve
Nimeni on Kaino
Hei hei
Nimeni on Kaino

Ainokainen (Singleton)

Luontimalli (Creational Pattern)

Tarvitaan olio, josta on olemassa vain yksi ilmentymä. Kyseessä voi olla esimerkiksi jokin koko järjestelmän yhteinen globaali palvelu.

Vain yhden ilmentymän synty varmistetaan siten, että luokan konstruktori on yksityinen ja luokan ainoa ilmentymä asetaan luokkamuuttujan arvoksi. Luokka-aksessori palauttaa arvonaan viitteen ainokaiseen. Esimerkissä luokan ainutkertainen ilmentymä luodaan dynaamisesti ensimmäisellä kerralla, kun viitettä ainokaiseen pyydetään. (Esimerkin luokat löytyvät käännösyksiköistä Ylimmainen.java ja Testi.java.)

class Ylimmainen {  // there can be only one ...

  private static Ylimmainen yli=null;

  private Ylimmainen() {}

  private boolean ok(String nimi) { //... hyväksymiskriteerit ...
    return true;  // tms...
  }

  public void checkIn(String nimi) {
    if (ok(nimi))
      ; // ... otetaan sisään ...
    else
      ;// ... alakertaan sorry ...
  }

  public static Ylimmainen pyydän() {
    if (yli==null)
      yli = new Ylimmainen();
    return yli;
  }
}


class Henkilo {
  private String nimi;
  // ...
  public Henkilo(String n) {
    nimi = n;
  }

  public void kuole() {
     // testamentti();
     // ...
     Ylimmainen.pyydän().checkIn(nimi);
  }
  // ...
}


public class Testi {
  public static void main(String[] args) {

    Henkilo metusalem = new Henkilo("Metu");
    metusalem.kuole();
  }
}
Vaihtoehtoinen tapa on käyttää Javan staattista alustuslohkoa (YlimmainenB.java):
class YlimmainenB {  // there can be only one ...

  private static YlimmainenB yli=null;

  static {
    yli = new YlimmainenB();
  }

  private YlimmainenB() {}

  public static YlimmainenB pyydän() {
    return yli;
  }

  private boolean ok(String nimi) { //... hyväksymiskriteerit ...
    return true;  // tms...
  }

  public void checkIn(String nimi) {
    if (ok(nimi))
      ; // ... otetaan sisään ...
    else
      ;// ... alakertaan sorry ...
  }
}