Ohjelmoinnin jatkokurssi

Matti Paksula, Arto Vihavainen, Matti Luukkainen

Olioiden suunnittelusta ja toteutuksesta

Seuraavassa esitellään joukko neuvoja olioiden suunnitteluun ja toteutukseen.

Olioiden attribuuteista

Olion attribuutit, eli luokkaan liittyvät ei-staattiset muuttujat, kuvaavat olion rakennetta ja ominaisuuksia. Esimerkiksi luokalla Opiskelija on opiskelijan nimi ja opiskelijanumero.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;  
  ...

Opiskelijan attribuutit ovat vain Opiskelija-tyyppiin, eli konkreettiseen opiskelijaan liittyviä tietoja. Rakentaessamme tallennus- ja latausmahdollisuutta opiskelijalle käyttäen Javan valmista luokkakirjastoa, eivät opiskelijan lataamiseen tai tallentamiseen käytetyt luokat kuulu Opiskelija-olion ominaisuuksiin. Esimerkiksi seuraavanlainen toteutus, missä opiskelijan tallennukseen käytettävä FileWriter-luokka on opiskelijan attribuuttina on väärin.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  private FileWriter tallentaja;
  
  ...
  
  public void tallenna() throws Exception {
    tallentaja = new FileWriter(nimi);
    tallentaja.write(nimi + "\n");
    tallentaja.write("" + opiskelijanumero + "\n");
    tallentaja.close();
  }  
  ...

Vaikka esimerkki toimii, ei se rakenteellisesti ole hyvin suunniteltu. Olioiden attribuuttien on tarkoitus kuvata aina tiettyä käsitettä (esimerkiksi opiskelija) selkeärajaisesti. Opiskelija nimeltä Matti, jolla on opiskelijanumero on selkeä opiskelijan määritelmä. Toisaalta, jos opiskelija määritellään siten, että sillä on nimen ja opiskelijanumeron lisäksi myös attribuutti tallentaja, kadotamme olion selkeärajaisuuden. Selkeärajaisuudesta puhutaan myös termillä koheesio, jolla tarkoitetaan sitä, että luokan rakenne liittyy aina tiettyyn selkeästi rajattavaan toiminnallisuuteen. Esimerkiksi Opiskelija-luokan attribuuttien tulee liittyä opiskelijaan.

Yllä oleva esimerkki pitääkin muuttaa sellaiseksi, että FileWriter-olio liittyy vain metodiin, ei koko olioon. Tällöin opiskelijaan liittyy vain siihen kuuluvat attribuutit, jotka oikeasti kuvaavat opiskelijan rakennetta.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  
  ...
  
  public void tallenna() throws Exception {
    FileWriter tallentaja = new FileWriter(nimi);
    tallentaja.write(nimi + "\n");
    tallentaja.write("" + opiskelijanumero + "\n");
    tallentaja.close();
  }  
  ...

Nyt myös metodi tallenna() on selkeärajainen. Tallenna-metodi käyttää vain siinä tarvittavaa FileWriter-oliota, ja rajaa tallentamistoiminnallisuuden omaan toteutukseensa. Huom! Selkeärajaisuudella ei kuitenkaan tarkoiteta sitä, että kaikki toiminnallisuus pitäisi toteuttaa aina saman metodin sisällä. Luettavuutta voidaan helpottaa rajaamalla metodien toteutusta siten, että metodit ovat lyhyitä. Metodista tallenna() voikin erottaa vielä tallennettavan tiedon luomisen omaan metodiinsa tallennettavaOpiskelija() seuraavasti.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private String opiskelijaNumero;
  
  ...
  
  public void tallenna() throws Exception {
    FileWriter tallentaja = new FileWriter(nimi);
    tallentaja.write(tallennettavaOpiskelija());
    tallentaja.close();
  }  
  
  private String tallennettavaOpiskelija() {
    StringBuilder sb = new StringBuilder();
    sb.append(nimi + "\n");
    sb.append("" + opiskelijanumero + "\n");
    ...
    return sb.toString();    
  }
  ...

Olioiden suunnittelusta

Jatketaan vielä yllä-esitetyn Opiskelija-luokan avulla. Opiskelijalla on attribuutteina opiskelijan nimi ja opiskelijanumero.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;  
  ...

Olioiden suunnittelussa oliot pyritään pitämään yksittäisinä kokonaisuuksina. Esimerkiksi jos opiskelija on ilmoittautunut kursseille, on opiskelijalla attribuuttina vain viitteet Kurssi-olioihin. Kurssi-oliot taas sisältävät kurssien tiedot, kuten kurssikoodin ja nimen.

public class Kurssi {
  // yksittäisen kurssin attribuutit
  private String kurssikoodi;
  private String nimi;
  ...

Kun Opiskelija-luokalle lisätään attribuutiksi kurssit, joihin opiskelija on ilmoittautunut, halutaan lisätä viitteet oikeisiin kursseihin. Seuraava esimerkki, jossa kursseihin viitataan kurssikoodin avulla (String-tyyppinen muuttuja) ei ole oikein toteutettu, sillä kurssin sisällön ja tietojen muuttuessa ilmoittautumisviite pysyy samana ja opiskelija-olio ei tiedä kurssin muutoksista.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  
  private ArrayList<String> kurssiIlmoittautumiset;  
  ...

Jos ilmoittautumiset eivät viittaa Kurssi-olioihin, eivät mahdolliset muutokset Kurssi-olioissa näy opiskelijan kurssilistalla, sillä viitteet eivät viittaa oikeisiin olioihin. Parempi tapa onkin toteuttaa kurssi-ilmoittautuminen siten, että kurssi-ilmoittautumiset ovat viitteitä oikeisiin Kurssi-olioihin.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  
  private ArrayList<Kurssi> kurssiIlmoittautumiset;  
  ...

On myös mahdollista rakentaa ilmoittautuminen siten, että vastuu opiskelijoiden kurssi-ilmoittautumisten ylläpidosta ei ole opiskelijoilla, vaan jollain muulla oliolla. Olio KurssiIlmoittautumiset on esimerkki vastuun siirtämisestä kolmannelle osapuolelle. Tällöin Opiskelija-olion ei myöskään tarvitse pitää kirjaa ilmoittautumisista.

public class KurssiIlmoittatumiset {
  private HashMap<Opiskelija, ArrayList<Kurssi>> opiskelijoidenKurssit;
  ...
public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  ...

Jos haluamme tietää jokaiseen kurssiin liittyneet opiskelijat voimme joko käydä KurssiIlmoittautumiset-tyyppisen olion opiskelijoidenKurssit-hajautustaulun läpi yksitellen, tai muuttaa toteutusta siten, että se pitää kirjaa myös jokaiseen Kurssi-olioon ilmoittatuneista opiskelijoista. Tällöin tiettyyn Kurssi-ilmentymään liittyneet opiskelijat saadaan nopeasti selville.

public class KurssiIlmoittatumiset {
  private HashMap<Opiskelija, ArrayList<Kurssi>> opiskelijoidenKurssit;
  private HashMap<Kurssi, ArrayList<Opiskelija>> kurssienOpiskelijat;
  ...

Toteutettavaa järjestelmää määriteltäessä tulee suunnitella myös järjestelmän hienojakoisuus, eli kuinka pieniin osiin järjestelmä jaetaan. Esimerkiksi kursseihin voi liittyä myös harjoitusajat ja luennoitsijat. Luennoitsijat voidaan myös toteuttaa omana luokkana. Miten kannattaa toimia jos luennoitsija on (jatko-)opiskelija?

Gettereistä ja settereistä

Get- ja set-metodien tarkoituksena on luoda näkyvyyttä luokan sisäisiin (kapseloituihin) attribuutteihin, joihin ei muuten ole mahdollista päästä käsiksi luokan ulkopuolelta. Luokan sisäisessä toteutuksessa näihin muuttujiin kuitenkin pääsee käsiksi. Getterien käyttö luokan sisäisessä toteutuksessa kannattaa jättää väliin, jos sille ei ole erityistä tarvetta. Seuraavassa esimerkissä metodi lisaaElementti() tekee yhden ylimääräisen askeleen kutsuessaan metodia getElementit().

public class Dokumentti {
  private ArrayList<Elementti> elementit;
  ...
  
  public ArrayList<Elementti> getElementit() {
    return elementit;
  }
  
  public void lisaaElementti(Elementti elementti) {
    getElementit().add(elementti);
  }
  ...

Yllä oleva metodi lisaaElemetti() kannattaakin kirjoittaa siten, että se käyttää luokan sisäistä attribuuttia elementit suoraan. Tällöin toteutus näyttää seuraavanlaiselta.

public class Dokumentti {
  private ArrayList<Elementti> elementit;
  ...
  
  public ArrayList<Elementti> getElementit() {
    return elementit;
  }
  
  public void lisaaElementti(Elementti elementti) {
    elementit.add(elementti);
  }
  ...

Nyrkkisääntönä voidaan ajatella seuraavaa; Jos muuttuja on näkyvissä, käytä sitä suoraan. Jos muuttuja ei ole näkyvissä, käytä getteriä. Tämä nyrkkisääntö vaatii toteuttajalta huolellisuutta olioiden suunnittelussa ja kapseloinnissa, sillä ilman kapselointia kaikki muuttujat ovat näkyvissä.

Static ja ei-static

Staattiset ja ei-staattiset muuttujat rajaavat eron olioiden ja luokkien välille. Staattiset muuttujat liittyvät luokkiin, ei olioihin, mutta niihin voi viitata olion sisältä. Ei-staattiset muuttujat taas liittyvät aina olioon, eli jos luokasta luodaan uusi olio, luodaan olioon liittyvät (ei staattiset) muuttujat myös oliota varten. Opiskelija-luokalla on staattinen muuttuja OPISKELIJANUMERO, jonka avulla pidetään kirjaa opiskelijanumeroista siten, että yksikään opiskelija ei saa samaa opiskelijanumeroa. Luokalla Opiskelija on myös ei-staattinen muuttuja opiskelijanumero, joka liittyy aina kyseiseen olioon.

public class Opiskelija {
  private static int OPISKELIJANUMERO = 0;
  
  private String nimi;
  private int opiskelijanumero;
  
  public Opiskelija(String nimi) {
    OPISKELIJANUMERO++;

    this.nimi = nimi;
    this.opiskelijanumero = OPISKELIJANUMERO;
  }
  ...

Kun luomme luokasta Opiskelija ilmentymän, kasvatetaan luokkamuuttujaa OPISKELIJANUMERO yhdellä, ja kopioidaan sen arvo uuden olion opiskelijanumero-muuttujan arvoksi. Havainnollistetaan tilannetta vielä hifigrafiikalla. Alussa on luokka Opiskelija, josta ei ole luotu yhtään ilmentymää.

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 0

Uutta Opiskelija-ilmentymää luotaessa (kutsulla new) ajetaan luokan konstruktori läpi. Luodaan uusi opiskelija viitteen matti taakse (Opiskelija matti = new Opiskelija("Matti");). Huomaa että matti on vain muuttujan nimi, johon viite on tallennettu.

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 1
  
Luokasta opiskelija tehdyt ilmentymät (oliot):

          _______________________
matti->  | private:              |
         |   nimi: Matti         |
         |   opiskelijanumero: 1 |
          ----------------------- 

Luodaan toinen Opiskelija-ilmentymä. Uudelle oliolle luodaan kentät nimi ja opiskelijanumero. Luodaan uusi opiskelija viitteen arto taakse (Opiskelija arto = new Opiskelija("Arto");).

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 2
      
Luokasta opiskelija tehdyt ilmentymät (oliot):

          _______________________
matti->  | private:              |
         |   nimi: Matti         |
         |   opiskelijanumero: 1 |
          ----------------------- 
          
          _______________________
arto->   | private:              |
         |   nimi: Arto          |
         |   opiskelijanumero: 2 |
          -----------------------          

Staattiset muuttujat siis liittyvät aina luokkaan. Staattisiin muuttujiin voi viitata ei-staattisesta ympäristöstä, eli olioiden maailmasta.

Tutkitaan samaa tilannetta vielä siten, että luokalla on staattinen metodi opiskelijoitaLuotu(), ja ei-staattinen metodi getOpiskelijanumero(). Metodit näkyvät kaikkialle, eli niillä on näkyvyysmääre public.

public class Opiskelija {
  private static int OPISKELIJANUMERO = 0;
  
  private String nimi;
  private int opiskelijanumero;
  
  public Opiskelija(String nimi) {
    OPISKELIJANUMERO++;

    this.nimi = nimi;
    this.opiskelijanumero = OPISKELIJANUMERO;
  }
  
  public int getOpiskelijanumero() {
    return opiskelijanumero;
  }
  
  ...
  
  public static int opiskelijoitaLuotu() {
    return OPISKELIJANUMERO;
  }

Staattiset metodit liittyvät luokkaan Opiskelija, kun taas ei-staattiset metodit liittyvät luokasta tehtyihin ilmentymiin. Luokassa Opiskelija olevaan staattiseen metodiin opiskelijoitaLuotu() voidaan viitata suoraan kutsulla Opiskelija.opiskelijoitaLuotu(), mutta metodiin getOpiskelijanumero() ei ole pääsyä luokasta, sillä metodi ei liity luokkaan vaan olioon. Jatketaan hifigrafiikkaamme vielä hieman.

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 2
  public:
    opiskelijoitaLuotu()
  
Luokasta opiskelija tehdyt ilmentymät (oliot):

          _________________________
matti->  | private:                |
         |   nimi: Matti           |
         |   opiskelijanumero: 1   |
         | public:                 |
         |   getOpiskelijanumero() |
          ------------------------- 
          
          _________________________
arto->   | private:                |
         |   nimi: Arto            |
         |   opiskelijanumero: 2   |
         | public:                 |
         |   getOpiskelijanumero() |
          -------------------------        

Metodi getOpiskelijanumero() liittyy siis olion ilmentymään, eikä sitä voi kutsua ilman viitettä itse olioon. Esimerkiksi metodi opiskelijoitaLuotu() ei voi kutsua matti-viitteen takana olevan olion getOpiskelijanumero()-metodia ilman viitettä matti-olioon. Toisaalta, oliot voivat käyttää luokan muuttujia ja metodeja.

Olioihin liittyvien metodien toteutus ei kopioidu, eli metodeista ei tehdä kopioita uusia olioita luotaessa. Vain olioihin liittyvät muuttujat kopioituvat uudelle oliolle, jolloin jokaisella oliolla on omat arvot muuttujissaan. Ei-staattisten metodien toiminta liittyy kuitenkin aina viitattavaan olioon.

Staattisia luokkamuuttujia käytetään siis silloin, kun tarvitaan jotain luokkaan yleisesti liittyvää. Hyvä esimerkki yleisesti käytettävästä muuttujasta on tekstikäyttöliittymissä tarvittu Scanner-luokka, jolla voidaan lukea syötettä näppäimistöltä. Scanner-luokasta kannattaa tehdä vain yksi ilmentymä näppäimistön lukemiseen, jolloin kaikki metodit voivat käyttää samaa näppäimistönlukijaa.

Esimerkiksi seuraava luokka syötteiden lukemiseen on toteutettu vaikeasti (luokassa on muitakin pulmia!), sillä jokaisessa metodissa luodaan aina uusi Scanner-ilmentymä.

public class MunLukija {

  public static String lueEkaSana() {
    Scanner lukija = new Scanner(System.in);
    
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[0];    
    }
  }
  
  public static String lueVikaSana() {
    Scanner lukija = new Scanner(System.in);
    
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[sanat.length-1];    
    }
  }
  ...

Scanner-olio kannattaa siirtää luokkamuuttujaksi, jolloin siihen voidaan viitata aina tarvittaessa.

public class MunLukija {
  private static final Scanner lukija = new Scanner(System.in);

  public static String lueEkaSana() {
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[0];    
    }
  }
  
  public static String lueVikaSana() {
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[sanat.length-1];    
    }
  }
  ...

Luokan metodeissa on myös paljon toistoa, itse lukeminen kannattaakin tässä tapauksessa siirtää kokonaan omaan metodiinsa.

public class MunLukija {
  private static final Scanner lukija = new Scanner(System.in);

  public static String lueEkaSana() {
    String[] sanat = lueRivinSanat();
    return sanat[0];    
  }
  
  public static String lueVikaSana() {
    String[] sanat = lueRivinSanat();
    return sanat[sanat.length-1];    
  }
  
  private static String[] lueRivinSanat() {
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      return rivi.split(" ");
    }
  }
  ...

Viite this

Viitteen this-käytöstä on ollut paljon puhetta, ja tärkeintä sen käytössä on jatkuvuus. Jos viitettä this käytetään tilanteissa, joissa sitä ei vaadita, kannattaa sitä käyttää aina. Viitteen this vaativat tilanteet ovat tilanteita, joissa metodin (tai konstruktorin) parametrina saadaan muuttuja, jolla on sama nimi, kuin olioon liittyvällä muuttujalla. Tällöin parametrina saatu muuttuja peittää olion muuttujan.

Seuraava konstruktorikutsu Opiskelija-luokalle asettaa parametrina saadulle muuttujalle nimi parametrina saadun muuttujan nimi arvon. Tällöin Opiskelija-luokan attribuutti nimi ei saa mitään arvoa.

public class Opiskelija {
  private String nimi;
  ...
  
  public Opiskelija(String nimi) {
    nimi = nimi;
    ...

Toinen tapa tehdä samankaltainen virhe on asettaa oliomuuttuja nimi parametrina saatuun muuttujaan. Tällöin parametrina saatu muuttuja saa oliomuuttujassa nimi olevan arvon, mutta olion muuttujan arvo ei muutu.

public class Opiskelija {
  private String nimi;
  ...
  
  public Opiskelija(String nimi) {
    nimi = this.nimi;
    ...

Oikea tapa tehdä ylläoleva asetus on seuraava. Tällöin luotuun opiskelijaan liittyvä muuttuja nimi saa arvokseen parametrina annetun muuttujan nimi arvon.

public class Opiskelija {
  private String nimi;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...

Esimerkki this-viitteen epäkonsistentista (ei jatkuvasta) käytöstä:

public class Opiskelija {
  private String nimi;
  private int opiskelijanumero;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...
    
  public String getNimi() {
    return nimi;
  }
  
  public int getOpiskelijanumero() {
    return this.opiskelijanumero;
  }
  ..

Tarkoitus onkin rakentaa lähdekoodi siten, että se seuraa samoja sääntöjä kaikkialla. Seuraavat esimerkit ovat molemmat paremmin tehty kuin ylläoleva esimerkki.

public class Opiskelija {
  private String nimi;
  private int opiskelijanumero;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...
    
  public String getNimi() {
    return this.nimi;
  }
  
  public int getOpiskelijanumero() {
    return this.opiskelijanumero;
  }
  ..
public class Opiskelija {
  private String nimi;
  private int opiskelijanumero;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...
    
  public String getNimi() {
    return nimi;
  }
  
  public int getOpiskelijanumero() {
    return opiskelijanumero;
  }
  ..

Ongelmia ja ratkaisuja

Seuraavassa esitellään joitakin viikkojen 3-4 laskarikertoimesta poimittuja esimerkkejä.

Asetus ylä- ja alaluokassa

Ongelma: rekisterinumeron etuliite asetetaan alaluokassa yläluokassa määriteltyyn kenttään.

public Saab(String rekkari){
	super(rekkari);
	rekkari = "SA-" + rekkari;
}

Ongelma: Rekisterinumeron alkuosa asetetaan yläluokan metodilla setRekisteriKilpi(String).

public Saab(String rekisteriNumero) {
	super(rekisteriNumero);
	super.setRekisteriKilpi("sa");
}

Ongelma: Yläluokassa ja alaluokassa on molemmissa rekisterinumero.

public abstract class Auto {
    private String rekisterinumero;

    public Auto(String rekisterinumero) {
        this.rekisterinumero=rekisterinumero;
    }

    public void aja(){
        System.out.println(this.rekisterinumero+" Ajetaan...");
    }

}

// Saab
public class Saab extends Auto{
    private String rekisterinumero;

    public Saab(String numero) {
        super("SA-"+numero);
        numero="SA-"+numero;
        this.rekisterinumero=numero;
    }

Ongelma: Alaluokka asettaa yläluokan attribuutin rekisteroi(String) "konstruktorilla".

public abstract class Auto {

   String rekisterinro;

   public String rekisteroi(String numero) {
       return this.rekisterinro = numero;
   }

   public void aja() {
       System.out.println(this.rekisterinro + " sanoo Vrrrrrrooooooooooom!");
   }

   public String toString() {
       return "" + this.rekisterinro;
   }

}

// Saab
public class Saab extends Auto {

   public Saab(String numero) {
       this.rekisterinro = "SA-" + this.rekisteroi(numero);
   }

}

Ratkaisu: Etuliite asetetaan siten, että se välittyy yläluokan konstruktorille ja asettuu siellä olion attribuutiksi.

public Saab(String rekkari){
	super("SA-" + rekkari);	
}

Metodia aja() ei ole toteutettu yläluokassa

Ongelma: Metodin aja() toteutus on toistettu kaikissa alaluokissa.

public abstract class Auto {
    String rekisterinumero;

    public Auto(String rekisterinumero) {
        this.rekisterinumero = rekisterinumero;
    }

    public abstract void aja();

}

// Saab

public class Saab extends Auto {

    public Saab(String rekisterinumero) {
        super(rekisterinumero);
    }

    public void aja() {
        System.out.println("SA-" + rekisterinumero + " sanoo Vrrrrrrooooooooooom!");
    }
}

Ratkaisu: Korjataan siten, että tehdään rekisterinumeron etuliitteen asetus kuten edellisessä esimerkissä ja siirretään aja()-metodin toteutus yläluokkaan.

public abstract class Auto {
    String rekisterinumero;

    public Auto(String rekisterinumero) {
        this.rekisterinumero = rekisterinumero;
    }

    public abstract void aja() {
        System.out.println(this.rekisterinumero + " sanoo Vrrrrrrooooooooooom!");
    }
        
}

// Saab

public class Saab extends Auto {

    public Saab(String rekisterinumero) {
        super("SA"+rekisterinumero);
    }

}

Abstrakti luokka kuten rajapinta

Ongelma: Abstrakti luokka on toteutettu kuten rajapinta, tällöin jokaisessa alaluokassa täytyy toteuttaa kaikki samansisältöiset metodit.

public abstract class Auto {
	public abstract void talletaRekkari(String rekkari);
	public abstract void aja();
	public abstract String getRekkari();
	public abstract void lisaaBongaus();
	public abstract int getBongaukset();
}

Bongausmuistiolla laskuri

Kaksi seuraavaa esimerkkiä eivät toimi ollenkaan oikein. Ohjelman tarkoituksena on bongata rekisterinumeroita ja niiden esiintymiskertoja.

Ongelma: Bongausmuistio-oliolla on attribuutti arvo. Tällöin arvo kasvaa jokaisen bongauksen kanssa, eikä jokaiselle rekisterinumerolle erikseen.

public class Bongausmuistio {
   int arvo=0;
   HashMap<Auto,Integer> muistio;

   public Bongausmuistio() {
       muistio=new HashMap();
   }

   public void bongaa(Auto a) {
       this.arvo=arvo+1;
       muistio.put(a, arvo);
   }

   public String toString() {
       return muistio.toString();
   }

}

Ongelma: Kuten edellisessä esimerkissä. Tämän lisäksi ensimmäinen bongausluku asettaa arvon nolla.

   private int bongausluku = 0;

   public Bongausmuistio() {
       muistio = new HashMap<Auto, Integer>();
   }

   public void bongaa(Auto a) {
       muistio.put(a, bongausluku);
       bongausluku++;
   }

Seuraavassa kolmessa esimerkissä ohjelma toimii oikein, mutta ratkaisu voisi olla vielä parempi.

Ongelmia: ylimääräinen muuttuja paljon ja komento put kahdesti.

public void bongaa(Auto a){
	if (muistio.containsKey(a)){
   		int paljon = muistio.get(a);
		paljon++;
		muistio.put(a, paljon);
	} else {
		muistio.put(a, 1);
	}
}
public void bongaa(Auto a) {
	if (this.bongaukset.containsKey(a)) {
		int nahty = this.bongaukset.get(a);
		nahty++;
		this.bongaukset.put(a, nahty);
	} else
		this.bongaukset.put(a, 1);
}
public void bongaa(Auto auto) {
	if (bm.containsKey(auto)) {
		int montako = bm.get(auto);
		montako++;
		bm.put(auto, montako);
	} else {
		bm.put(auto, 1);
	}
}

Ratkaisu: Alustetaan muuttuja aikaisemmat ja haetaan siihen arvo HashMap:stä. Tämän jälkeen asetaan yhdellä komennolla aikaisemmat + 1.

public void bongaa(Auto auto) {

	int aikaisemmat = 0;

	if (bm.containsKey(auto))
		aikaisemmat = bm.get(auto);
	
        bm.put(auto, aikaisemmat + 1);
}

Periytymisen suunnittelu

Rekisterinumero on toteutettu jakamalla "SA-313" tyyppiin ("SA") ja numerotunnisteeseen ("313"). Toteutuksessa alaluokka kysyy yläluokalta metodia getRekkari(), joka taas kysyy alaluokalta tyyppiä ja yhdistää sen rekisterinumeroon, jonka vihdoin palauttaa. Jos tyyppi ja numerotunniste halutaan pitää edelleen, voisi toteutuksessa käyttää suoraan attribuutteja, eikä gettereitä.

Lisäksi aja()-metodia ei ole toteuttetu yläluokassa.

public abstract class Autot {
    protected String rekkari;

    public Autot(String rekkari) {
        this.rekkari = rekkari;
    }

    public abstract void aja();

    public abstract String getTyyppi();

    public String getRekkari() {
        return getTyyppi() + this.rekkari;
    }
}

public class Saab extends Autot {

    public Saab(String rekkari) {
        super(rekkari);
    }

    public void aja() {
        System.out.println(getRekkari() + " sanoo Vrrrrroooooooooom");
    }

    public String getTyyppi() {
        return "SA-";
    }
}

public class Volvo extends Autot{

    public Volvo(String rekkari) {
        super(rekkari);
    }

    public void aja() {
        System.out.println(getRekkari() + " sanoo Vrrrrroooooooooom");
    }

    public String getTyyppi() {
        return "VO-";
    }
}

Olioden vastuut

Bongauksessa bongausten lukumäärä talletetaan bongausmuistioon. Seuraavassa esimerkissä auto tietää bongauksiensa lukumäärän. Tämä tarkoittaisi reaalimaailmassa sitä, että autobongaaja raaputtaisi auton kohdatessaan bongatun auton kylkeen merkinnän.

public void bongaa(Auto a) {
	a.talletus();
	int lkm = a.getLkm();
	String rek = a.getRekisterinumero();
	lista.put(rek, lkm);
}
public class Saab extends Auto {

    private String apu;

    public Saab (String rekisterinumero) {
      this.rekisterinumero = rekisterinumero;
      apu = this.rekisterinumero;
      this.rekisterinumero = "SA-"+apu;
    }

}

Attribuuttien näkyvyys

Metodissa aja() nähdään attribuutti rekisterinumero, sitä voi käyttää suoraan.

Lisäksi muuttuja aja on turha, sen sisältämä String voidaan antaa suoraan tulostuskomennolle. Metodien nimissä käytetään aina camelCasea: getrekisterinro()getRekisterinumero.

public String getrekisterinro(){
    return this.rekisterinro;
}

protected void aja(){
    String aja = " -"+ getrekisterinro() + " sanoo Vrrrrrrooooooooooom!";
    System.out.println(aja);
}

Hyviä käytäntöjä

while(true)

Toistot tehdään useasti kunnes jokin ehto täyttyy. Usein voi olla helpompaa muotoilla ehto siten, että toistoa tehdään ikuisesti ja poistuminen tarkistetaan toistorakenteen sisällä.

int valinta;

while (true) {
	int valinta = lukija.nextInt();

	if (valinta > 0)
		break;
}

Portsari

Ehtolauseita käytettäessä metodien luettavuus helposti kärsii. Portsarin tavoitteena on, että sisäkkäisiä tai hankalasti luettavia ehtolauseita ei enää tarvita.

Seuraavasta esimerkistä ei suoraan näe millä ehdoilla käyttäjä lisätään ilmoittautuneisiin:

void ilmoittaudu(String kayttaja) {
	if (!ilmoittautuneet.contains(kayttaja)) {
		
		if (ilmoittautuneet.size() < maksimiKoko) {
			ilmoittautuneet.add(kayttaja);
		}
	}

}

Tai sama toisella tavalla:

void ilmoittaudu(String kayttaja) {
	if (ilmoittautuneet.contains(kayttaja)) {
		// Ei tehdä mitään
	} else if (ilmoittautuneet.size() < maksimiKoko) {
		ilmoittautuneet.add(kayttaja);
	}
}

Muotoillaan ehtolauseet uudestaan. Metodin alussa on kaksi ehtolausetta, eli portsaria, jotka tarkistavat pääseekö käyttäjä sisään. Jos ensimmäinen ehto "Käyttäjä on jo ilmoittautunut" toteutuu, ohjaa portsari käyttäjän ystävällisesti ulos. Tämän jälkeen voidaan tarkistaa toinen ehto välittämättä ensimmäisestä, koska suoritus on jo päättynyt jos ehto on ollut tosi.

Samoin tomitaan jos toinen ehto "Ilmoittautuneita on maksimimäärä" toteutuu. Tämän jälkeen voidaan haluttu tapaus, ilmoittautuminen suorittaa ilman ympäröivää sotkuista ehtolausetta.

void ilmoittaudu(String kayttaja) {
	if (ilmoittautuneet.contains(kayttaja))
		return;

 	if (ilmoittautuneet.size() == maksimiKoko)
		return;

	ilmoittautuneet.add(kayttaja);
}

Saman voi tietysti sanoa vielä tiiviimmin:

void ilmoittaudu(String kayttaja) {
	if (ilmoittautuneet.contains(kayttaja) || ilmoittautuneet.size() == maksimiKoko)
		return;

	ilmoittautuneet.add(kayttaja);
}