Grensesnitt

De fleste objekter har en tilstand, og når en skal forklare oppførselen til objektet, dvs. hva operasjonene til objektet gjør, så er det naturlig å referere til hvordan disse leser og endrer tilstanden. Ta som eksempel et teller-objekt med metodene void count() og int getCounter(). Hvis oppførselen skal forklares er det naturlig å si at count() øker telleren og at getCounter() returnerer tellerverdien, altså en beskrivelse basert på objektets interne tilstand. Bildet vi gir av objektet er en kombinasjon av tilstand (attributter) og oppførsel (operasjoner), som vist til venstre i figuren under. Dette lekker på en måte informasjon om hvordan oppførselen er realisert, og bryter prinsippet om innkapsling, hvor kun operasjonene skal være offentlig kjent. En innkapslet versjon av telleren er vist i midten, og her får en frem at det kun er operasjonene som er ment å være kjent. Men for at innkapslingen skal være effektiv som skjuling av informasjon om intern tilstand og implementasjonsdetaljer, ønsker en egentlig kun å fokusere på objektets grensesnitt mot utenomverdenen, som er de operasjonene og attributtene med offentlig synlighet (se figur til høyre og fotnote om notasjonen).

Counterint counterint getCounter()void count()

Attributter og operasjoner

Counterint counterint getCounter()void count()

Innkapsling av tilstand vha. synlighet

Counterint getCounter()void count()

Grensesnitt, bare operasjoner

Grensesnitt-oppførsel

Grensesnittet til et objekt består altså av det som er åpent tilgjengelig, og ved beskrivelse av oppførselen ønsker en å unngå å trekke inn en evt. intern tilstand, siden denne uansett er ment å være skjult. Dersom vi forsøker å gjøre dette for Counter, så ser vi at oppførselen til count() og getCounter() er koblet, så beskrivelsen av count() må referere til getCounter(): Dersom getCounter() returnerer n, så vil et kall til count() gjøre at getCounter() returnerer n+1. Dette er typisk for operasjoner som leser og endrer samme underliggende tilstand, slik tilfellet er for getter- og setter-par: getX() returnerer argument-verdien til siste kall til setX(...).

I praksis er det ofte enklest å beskrive oppførselen med et kode-eksempel med konkrete verdier, som vist under. Dette er spesielt nyttig når en ønsker å teste om oppførselen er korrekt implementert.

Counter counter = ... // getCounter() => 1
counter.count(); // getCounter() => 2
counter.count(); // getCounter() => 3 osv.

Person p = ...
p.setName("Hallvard"); // getName() => "Hallvard"

Eksplisitt grensesnitt

Alle objekter/klasser har altså et grensesnitt, som er de offentlig kjente operasjonene som tilbys andre objekter/klasser og den oppførselen som disse implementerer. Dette grensesnittet kan det være nyttig å gjøre eksplisitt, uavhengig av om det er implementert (ennå i noen spesifikk klasse). For det første er det jo dette andre klasser er interessert i og som disse må kode mot. Ved å gjøre grensesnittet eksplisitt kan en ta det i bruk uavhengig av og kanskje før implementasjonen er klar. For det andre fungerer grensesnittet og spesielt beskrivelsen av oppførselen som en spesifikasjon for implementasjonen, og den er det alltid greit å ha klar på forhånd før en begynner på implementasjonen. For det tredje kan det være aktuelt med flere implementasjoner, med ulike egenskaper og variasjoner innenfor rammen av den foreskrevne oppførselen.

Counterint getCounter()void count()CounterImplCounterImpl(int start, int end)Counterint getCounter()void count()UpCounterUpCounter(int start, int end)DownCounterDownCounter(int end, int start)

Figuren over viser hvordan forholdet mellom et (eksplisitt) grensesnitt og en klasse som implementerer grensesnittet illustreres. Til venstre vises hvordan Counter-grensesnittet er implementert av CounterImpl-klassen. Til høyre vises hvordan to ulike klasser kan implementerer samme grensesnitt (se fotnote om notasjonen), og navnene indikerer at de representerer ulike varianter av en overordnet oppførsel, nemlig å telle fra et tall (i retning av og) til et annet.

Merk at det i dette tilfellet er viktig at de to klassene ikke bare har de nødvendige metodene, men implementerer oppførselen i henhold til kravene.

Spørsmål til refleksjon

  • Prøv å beskrive grensesnittet til en stabel (eng: stack), med metodene push(), peek(), pop() og isEmpty()

1) I-symbolet står for interface, som er det engelske begrepet, mens C'en står for class. En bruker en stiplet pil fra en den implementerende klassen til grensesnittet, for å skille det fra arv mellom grensesnitt.