Rajapinta sopimuksena

Rajapintasuunnittelussa pohdittavia kysymyksiä ovat:

  • Miten rajapintaa on lupa käyttää?

  • Mitä rajapinnan toiminnot lupaavat tehdä?

  • Millaisia virheitä toiminnoissa voi tulla?

  • Miten rajapintaa tulisi testata?

Valmiista ohjelmistokomponentista sen käyttäjän tulee saada kaikki käyttämiseen tarvittava tieto. Vastaavasti sen toteuttajalla tulee olla kaikki ohjelmistokomponentin vastuiden toteuttamiseen tarvittava tieto. Kahdella eri ohjelmoijalla – käyttäjällä ja toteuttajalla – on siten molemmilla oma näkökulmansa rajapintaan. Samoin molemmille asettuu vastuita rajapinnan toiminnasta.

Sopimussuunnittelu

Sopimussuunnittelu (Design by Contract) on alkujaan Bertrand Meyerin luoma suunnittelun menetelmä, jossa rajapinta määritellään sopimuksena kutsujan ja toteuttajan välillä. Sopimussuunnittelu antaa selkeän metaforan itse suunnitteluprosessille. Siinä rajapinnalla ajatellaan olevan asiakas (kutsuja) ja toimittaja (toteuttaja), joille molemmille syntyy rajapinnasta sekä velvoitteita että hyötyjä. Kutsujalla ja toteuttajalla on rajapinnasta myös jaettu vastuu.

Sopimussuunnittelun etuna on, että se pakottaa miettimään rajapinnan palvelut tarkasti ja myös dokumentoimaan ne hyvin. Se myös osaltaan yksinkertaistaa toteutustyötä, koska sopimus asettaa selkeät raamit sille, miten rajapintaa tullaan käyttämään ja millaisiin tilanteisiin toteutuksessa tulee varautua. Sopimussuunnittelussa:

  • Rajapinnan toteutus lupaa jokaisen rajapinnan palvelun toimivan tietyllä, sovitulla tavalla, kun sitä kutsutaan sovitulla tavalla. Rajapinnan palveluista kerrotaan siis myös, mitkä ovat niiden sallittuja käyttötapoja. Tällaisia ovat mm. parametrien sallitut arvoalueet tai palveluiden mahdolliset kutsujärjestykset.

  • Rajapinnan käyttäjä puolestaan lupaa kutsua rajapintaa vain määrittelyn mukaisesti.

Esi- ja jälkiehto

Sopimuksessa sekä kutsujalle että toteuttajalle syntyy vastuita. Itse sopimus tehdään määrittelemällä jokaiselle palvelulle esiehto (engl. precondition) ja jälkiehto (engl. postcondition). Esiehto (P) on looginen lause tai väittämä, jonka tulee olla voimassa, kun palvelua kutsutaan. Jälkiehto (Q) on taas looginen lause tai väittämä, joka on voimassa palvelun suorituksen päätyttyä.

\[\{P\} palvelu() \{Q\}\]

Kutsujan vastuulla on kutsua rajapinnan palveluita sopimuksen edellyttämällä tavalla eli kutsujan velvoite koskee sitä, miten rajapintaa saa kutsua. Kutsuja siis pitää huolta siitä, että esiehto toteutuu. Tässä kohtaa suunnittelua, kysymys, johon vastataan on: Milloin/miten palvelua saa kutsua? tai Mitä palvelu odottaa?

Toteuttajan vastuulla puolestaan on pitää huoli siitä, että sopimuksen mukaisesti kutsuttu palvelu toteutuu. Toteuttajan velvoite koskee siis sitä, mitä rajapinta lupaa tehdä. Toteuttajan tehtävä on pitää huolta siitä, että jälkiehto toteutuu. Tässä kohtaa ei ole enää tarvetta tarkastaa kutsujan vastuulla olleita asioita eli esiehdon voi toteutuksessa olettaa olevan voimassa. Tämä helpottaa palvelun toteuttamista, koska jokaiseen mahdolliseen tilanteeseen ei enää tarvitse varautua. Jos palvelua ei voida toteuttaa eli jälkiehto ei täyty, vaikka palvelua on kutsuttu sopimuksen mukaisesti, syntyy virhetilanne, joka on ilmaistava selkeästi kutsujalle. Jälkiehto vastaa kysymyksiin: Mitä palvelu lupaa tehdä? tai Mitä se takaa?

Rajapinnan sopimuksessa otetaan kantaa tavalla tai toisella seuraaviin asioihin:

  • syötearvojen hyväksyttävät ja hylättävät arvot ja merkitys (sekä mahdollisesti tyypit. Tyypitys riippuu ohjelmointikielestä eikä staattisesti tyypitetyssä kielessä ole yleensä tarvetta asettaa ehtoja tyypille.)

  • paluuarvojen arvo (ja mahdollisesti tyyppi) ja merkitys

  • virhearvot ja poikkeukset ja niiden merkitys

  • mahdolliset sivuvaikutukset

  • toiminnallisuuteen liittyvät esi- ja jälkiehdot (esim. datan tulee olla järjestetty)

  • joskus suorituskykyyn liittyviä takuita

Ohjelman testausvaiheessa esiehdon voimassaoloa voidaan tarkastaa. Virheettömän ohjelman suorituksessa sopimusta rikkovia kutsuja ei kuitenkaan koskaan tulisi olla. Ehtoja tarkistavaa koodia ei siten tyypillisesti enää ole valmiissa ohjelmassa vaan niitä käytetään vain ohjelmaa testattaessa toteutusvaiheessa. Esiehdon rikkominen on siten tilanne, johon ei lähtökohtaisesti tarvitse toteutuksessa varautua. Joskus voi kuitenkin olla mielekästä huomauttaa kutsujalle sopimusta rikkovasta kutsusta sopivalla poikkeuksella. Tällöin poikkeus liitetään osaksi ehtoja. Jos toteutus ei saa täytettyä jälkiehtoa eli ei pysty toteuttamaan sopimuksen mukaista palvelua, tämä ilmaistaan yleensä poikkeuksella.

Sopimusuunnittelussa kantaluokan sopimus sitoo myös aliluokkaa. Aliluokka ei voi vaatia kantaluokkaa tiukempaa esiehtoa eikä tuottaa kantaluokan toteutusta heikompaa jälkiehtoa. Aliluokka voi siis hyväksyä kantaluokkaansa heikomman esiehdon ja vastaavasti vahvemman jälkiehdon, koska näin kantaluokan sopimusta ei aliluokan rajapinnassa rikota.

Luokkainvariantti

Luokkien palveluille voidaan esi- ja jälkiehtojen lisäksi määritellä invariantti (engl. invariant) eli pysyväisväittämä. Invariantti kuvaa luokan tilaa palvelukutsujen välillä. Invariantti on voimassa luokan olioille heti niiden luomishetkestä alkaen ja aina ennen ja jälkeen luokan metodien kutsuja. Invariantin tehtävänä on taata, että luokan olio on käyttökelpoinen. Sillä ei ole merkitystä metodien kutsujalle, mutta se auttaa palveluiden toteuttajaa. Invariantti saa olla hetkellisesti rikki metodin koodin sisällä, kunhan se on jälleen voimassa, kun metodin suoritus saadaan päätökseensä. Invariantin kanssa palvelun sopimus siis muotoutuu:

\[\{ INV ⋀ P \} o.palvelu() \{ INV ⋀ Q \}\]

Esimerkkejä

Otetaan esimerkiksi päivämäärän toteutuksen funktio, joka asettaisi uuden päivän tietyn kuukauden ja vuoden päiväksi:

public class Date {
   private int year;
   private int month;
   private int day;

   // Esiehto: day >=1 && day <= päiviä kuukaudessa kyseisenä vuonna
   // Jälkiehto: päivämäärä on muuttunut kuukauden päivän osalta uuteen päivään
   // Invariantti: päivämäärä on laillinen päivämäärä
   public void setDay( int day ) {
       this.day = day;
   }

Virhetilanteet ovat osa jälkiehtoa. Lisäksi on aina suunnitellusta käyttötilanteesta riippuvaista, millaiseksi sopimus muodostuu. setDay olisi siten mahdollista määritellä myös niin, ettei päivämäärän laillisuus olisikaan kutsujan vastuulla.

// Esiehto: -
// Jälkiehto: jos päivä on laillinen kuukauden päivä, päivämäärä on muuttunut kuukauden päivän osalta uuteen päivään
// Jälkiehto: jos päivä on laiton kuukauden päivä, heitetään DateException
// Invariantti: päivämäärä on laillinen päivämäärä
public void setDay( int day )
      throws DateException {
    if(!isLegalDate(day, month, year)) {
        throw new DateException(String.format("Illegal date %2d.%2d.%2d", day, month, year));
    }
    this.day = day;
}

Toisena esimerkkinä voimme katsoa puolitushakua, joka on Tietorakenteet ja algoritmit 1 -kurssilta tuttu hakualgoritmi:

public class Search{

    //Esiehto: taulukko array ei ole tyhjä
    //Esiehto: taulukko array on haun odottamassa suuruusjärjestyksessä
    //Jälkiehto: palauttaa taulukon indeksin, josta etsitty alkio löytyy tai -1, jos alkio ei ole taulukossa
    //Jälkiehto: taulukko pysyy muuttumattomana
    public static int binarySearch( int[] array, int value )
    {
        int low = 0;
        int high = array.length-1;
        int mid = 0;

        while( low <= high )
        {
            mid = (high+low)/2;
            if( array[mid] == value )
            {
                return mid;
            }
            else if( array[mid] > value )
            {
                high = mid-1;
            }
            else{
                low = mid+1;
            }
        }
        return -1;
    }
}

Käytännössä ehdot kirjoitetaan rajapinnalle muodossa, josta rajapintadokumentaatio voidaan tuottaa helppolukuisessa muodossa samalla tarjoten se kooditasolla. Javassa tämä tehdään käyttämällä määrämuotoisia Javadoc kommentteja rajapinnan palveluiden kommentoimisessa. Javadociin tutustutaan kurssilla vielä tarkemmin myöhemmin.

Sopimussuunnittelu (kesto 24:48)

Sopimussuunnittelun kolme kysymystä

Sopimussuunnittelu voidaan tiivistää ohjelman suunnittelijalle esitettäviin kolmeen kysymykseen rajapinnan sopimuksesta:

  1. Mitä sopimus odottaa?

  2. Mitä sopimus takaa?

  3. Mitä sopimus pitää yllä?

Sopimussuunnittelussa

Esiehto

Jälkiehto

Luokkainvariantti