Objektorientācijas pārskats

Ievads

Prezentācijas mērķis - vispārīgā līmenī iepazīties ar dažām objektorientācijas kategorijām, it īpaši pievēršoties Javas OO programmēšanas vadlīnijām, kuras būtiskas Web aplikāciju izstrādē.

Tēmas

  • Java kā objektorientēta valoda
  • Javas klases deklarācijas paraugs
  • Iekapsulēšana (encapsulation)
  • Reprezentācijas invariants
  • DRY (Don’t Repeat Yourself)
  • Mantošana (inheritance)
  • Mantošana vs. agregācija (aggregation).
  • Kolekcijas valodā Java (collections)
  • Atkarība no klases un no interfeisa
  • Objektu diagrammas un spriedumi par referencēm

Java un OO

Šo piezīmju rakstīšanas brīdī Javas pēdējā versija ir JDK 1.6 (sk. JDK6 Update 2 Abās jaunākajās versijās (1.5 un 1.6) ir virkne jaunu jēdzienu - anotācijas, parametrizētie tipi (generic types), uzskaitījuma tipi (enum types).

Valodas Java (Sun Microsystems) un C# (Microsoft .NET) savas attīstības gaitā savstarpēji ietekmējas.

Javas klases un objekti

Javas klases deklarācijas piemērs:

package lv.mycompany.myproject.model;

import java.util.Date;
// more imports

public class User {
    // atributes
        private Integer id;
    protected String name;
    protected Date birthdate;
    
    // constructor
    public User(String name, Date birthdate) {
        this.name = name;
        this.age = age;
    }
    
    // methods
        public Integer getId() {
                return id;
        }
        public void setId(Integer id) {
                this.id = id;
        }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Date getBirthdate() {
        return birthdate;
    }
    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
    }
}
  1. Kā redzams no minētā koda parauga, katras klases kods (izejas teksts) sākas ar pakotnes deklarāciju
    package some.packagename;
    
  2. Pēc tam var sekot "import" komandas - gadījumiem, ja dotās klases kompilācijai ir vajadzīgas citas klases (ja vien tās neatrodas pakotnē java.lang). Augšminētajā piemērā String un Integer nav jāimportē, jo šīs klases ir definētas pakotnē java.lang. Turpretī klase Date ir jāimportē, jo klase ar nosaukumu "Date" ir varētu būt atrodama vairākās pakotnēs un bez atbilstoša importa nebūtu iespējams saprast, par kuru klasi ir runa.
  3. Pēc importiem seko klases deklarācija:
    <modifiers> class <class_name> {
        // class body
    }
    

    Klases definīcijas priekšā var rakstīt dažādus modificētājus - "public", "final", u.c. To nozīmes precīzi paskaidrosim vēlāk. Vienkāršākajos gadījumos var visas klases deklarēt kā "public class class_name".

  4. Klases ķermenī (t.i. starp figūriekavām var rakstīt klases locekļus. Ir vairāku veidu locekļi - atribūti, konstruktori, metodes, iekšējas klases. Labs programmēšanas stils prasa, lai šie locekļi sekotu nosauktajā secībā - vispirms atribūti, tad konstruktori, utml.

Iekapsulēšana

Iekapsulētu kodu var pazīt pēc mērķtiecīgas Javas piekļuves modifikatoru (piemēram, public/private) lietošanas. Parasti visi klases atribūti ir deklarēti kā private vai protected (t.i. ja vēlamies, lai tie būtu pieejami arī mantotajās klasēs). Savukārt daļa metožu parasti ir publiskas.

Metodes, kuras domātas, lai varētu nolasīt vai uzstādīt atribūtu vērtības pakļaujas sekojošai nosaukuma konvencijai (naming convention) - ja atribūta nosaukums ir "xxx" (tas sākas ar mazo burtu - sk. Javas programmu stils), tad metodi šī atribūta nolasīšanai sauks "getXxx()" un metodi tā vērtības uzstādīšanai - attiecīgi "setXxx()".

Sk. iekapsulēšanas piemēru - lietotāja definēta datumu klase.

Iekapsulēšana kā Projektēšanas Paraugs

Projektēšanas paraugi (design patterns) ir risinājumi, kurus lieto dažādās tipiskās situācijās.

Problēma:
Publiskus objekta laukus klients var tieši manipulēt, izjaucot objekta reprezentācijas invariantu (t.i. radot objektā nekonsistentu stāvokli) vai arī radot nevēlamas atkarības, kas neļauj mainīt atbilstošās klases implementāciju.
Risinājums:
Noslēpt dažus klases locekļus (atribūtus vai metodes), atļaujot ārpuses klientiem piekļuvi objektam caur noteiktā stilā izveidotu interfeisu.

Piemērs: Klase pirms iekapsulēšanas

/**
 * Invariant: degrees value should always
 * be equal or more than -273.15
 */
public class RoomTemperature {
    public float degrees;
}

public class RoomTemperatureClient {
   public invalidValue() {
       RoomTemperature tr = new RoomTemperature();
       tr.degrees = -300.0F;
   }
}

Arī tad, ja RoomTemperature klienti šo klasi lieto pareizi un piešķir pareizas vērtības, var rasties grūtības, ja implementācijā ir jāsāk lietot Kelvina, nevis Celsija grādi. Jo tad visas klientu piešķirtās vērtības kļūst nepareizas.

Iekapsulēšanas rezultāts

/**
 * Invariant: degrees value should always
 * be equal or more than -273.15F
 */
public class RoomTemperature {
    private float degrees;
    public void setDegrees(float degrees) {
        if (degrees < -273.15F) {
            throw new IllegalArgumentException(
                "Temperatures below -273.15F not allowed");
        }   
        this.degrees = degrees;
    }
    public float getDegrees() {
        return degrees;
    }
}

public class RoomTemperatureClient {
   public invalidValue() {
       RoomTemperature tr = new RoomTemperature();
       tr.degrees = -300.0F;
   }
}

Reprezentācijas invarianta princips

Katrai klasei var definēt objekta iekšējo stāvokļu (atribūtu vērtību) pieļaujamās vērtības, t.i. reprezentācijas invariantu. Katra klases metode garantē, ka reprezentācijas invariants pēc attiecīgās metodes izsaukuma saglabāsies, pieņemot, ka šis invariants bija spēkā arī pirms metodes izsaukuma.

Reprezentācijas invariants parasti parādās tur, kur pareizi izmanto iekapsulēšanu. Ja nav reprezentācijas invariantu, tad klases iekšējo stāvokli neviens sabojāt nevar un iekapsulēšana (teiksim, privāti atribūti) šai klasei nav nepieciešami vai arī tie kalpo tikai tam, lai varētu mainīt implementāciju un aizstāt šos atribūtus ar citiem.

Piemēri
  1. Kā reprezentācijas invariantu var prasīt, lai ikvienam objektam no klases User dzimšanas diena jeb birthday atribūts nebūtu nākotnes datums. Lai šo invariantu nodrošinātu, klases konstruktoriem un metodei setBirthdate() ir jāpārbauda, vai izsaukuma parametrā norādītais datums nav nākotnes datums. Kodu var pārrakstīt šādi:
        public void setBirthdate(Date birthdate) {
            Date current = new Date();
            if (current.getTime() < birthdate.getTime()) {
                throw new IllegalArgumentException("Future dates not allowed");
            }
            else {
                this.birthdate = birthdate;
            }
        }
    

Neatkārtošanas princips (DRY)

Kodu nedrīkst kopēt vairākās vietās ("Don't repeat yourself", "Write it once and only once"). Šo principu nav vienkārši ievērot objektorientētās programmās, kurās ir ierobežota piekļuve atribūtiem. Lai izvairītos no koda atkārtošanās, ir ir izmantojama klašu mantošana, kopīgo koda fragmentu iznešana atsevišķās statiskās metodēs un citi paņēmieni.

Vispārīgi principi, lai uzlabotu koda struktūru, nemainot tā uzvedību tiek saukti refaktorizācija. Sk. Refaktorizācija.