Klasser og objekter Mildrid Ljosland og Grethe Sandstrak, Institutt for informatikk og e-læring ved NTNU Lærestoffet er utviklet for faget IFUD1002 C#.NET 2 Klasser og objekter Resymé: Denne leksjonen introduserer objekter med deres identitet, tilstand og oppførsel. Deretter ser vi på hvordan det programmeres med klasser, metoder og referanser. Vi ser også på forskjellen mellom verdityper og referansetyper, samt gjennomgår ulike måter å overføre argumenter til og fra metoder. Til slutt ser vi litt på objektorientert design. Leksjonen er knyttet til kapittel 3 i læreboka. Det anbefales å lese leksjonen først, og deretter læreboka. Innhold 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.3 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.4 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.5 2.5.1 2.5.2 2.5.3 2.5.4 OBJEKTER ............................................................................................................................................. 2 Hva er et objekt? ............................................................................................................................. 2 Objekter og referanser .................................................................................................................... 3 Verdityper og referansetyper........................................................................................................... 4 KLASSER ............................................................................................................................................... 4 Oversikt over mulige medlemmer .................................................................................................... 4 Metoder ........................................................................................................................................... 4 Medlemsvariabler ........................................................................................................................... 5 Konstruktører .................................................................................................................................. 6 Klassemedlemmer ........................................................................................................................... 7 Eksempel ......................................................................................................................................... 8 MER OM KLASSER OG DERES INNHOLD ............................................................................................... 10 Egenskaper .................................................................................................................................... 10 Konstanter ..................................................................................................................................... 11 Strukturer ...................................................................................................................................... 11 Overloading................................................................................................................................... 12 Partielle klasser ............................................................................................................................ 13 ARGUMENTOVERFØRING .................................................................................................................... 14 Innargumenter, utargumenter og kombinerte argumenter ............................................................ 14 Verdioverføring av verdityper ....................................................................................................... 14 Referanseoverføring av verdityper ................................................................................................ 14 Verdioverføring av referansetyper ................................................................................................ 16 Referanseoverføring av referansetyper ......................................................................................... 16 OBJEKTORIENTERT DESIGN ................................................................................................................. 17 Use-case og scenarier ................................................................................................................... 17 UML, klassediagram og sekvensdiagram...................................................................................... 18 Samarbeid mellom objekter og en-del-av-forhold ......................................................................... 19 Litt om bruk av grensesnitt ............................................................................................................ 22 Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 2 av 22 2.1 Objekter 2.1.1 Hva er et objekt? Når vi programmerer objektorientert, finner vi de ”tingene” problemet dreier seg om, og bygger opp programmet rundt dem. Hvis vi skal lage et program som holder orden på data om ansatte, vil hver ansatt bli et objekt. Objekter har en identitet, en tilstand og en oppførsel. I en bedrift har vi mange ansatte. Hver av dem er egne individer med sin egen identitet. Identiteten må være entydig, Pål Hansen på verkstedet må ikke blandes med Pål Hansen i salgsavdelingen. Hver ansatt har et navn, en fødselsdato, en ansettelsesdato, en stilling, en lønn osv. – dette er objektets tilstand. Tilstanden kan endres etter hvert som tiden går, for eksempel kan en ansatt få lønnstillegg. Objektene har også en oppførsel, for eksempel kan ansatte betjene kunder, drikke kaffe eller skrive timeliste. Når vi skal programmere, lager vi en klasse der vi lar tilstanden være medlemsvariabler (kalles også datamedlemmer) og oppførselen være metoder (medlemsfunksjoner). Så kan vi lage objekter av denne klassen (vi sier at vi instansierer klassen). Hvert objekt får sine spesielle verdier på medlemsvariablene, mens oppførselen er den samme for alle objektene av en klasse. (I seinere leksjoner skal vi se på hvordan vi kan lage objekter som oppfører seg litt forskjellig, men foreløpig må alle oppføre seg likt.) En klasse er altså en beskrivelse av objekter av en bestemt type. Vi kan se på klassen som en arkitekttegning, mens objektene er de husene som bygges etter denne tegningen. Hvert objekt får sin egen identitet, knyttet til plassen i primærlageret der objektet er lagret. Men akkurat som vi i det daglige foretrekker å omtale de ansatte ved navn, ikke ved personnummer, foretrekker vi også å omtale objekter med navn, ikke ved lagerplass. Vi definerer derfor en variabel og lar den inneholde objektets identitet, vi har fått en referanse til objektet. La oss se på et enkelt eksempel. Vi trenger objekter som inneholder datoer. Datoer består som kjent av dag, måned og år, og vi lager derfor en klasse Dato som inneholder medlemsvariablene dag, måned og år. Deretter kan vi opprette ulike datoer, for eksempel julaften (24, 12, 2015), Martins fødselsdato (7, 3, 2009), krigsutbruddet (9, 4, 1945) osv. Dette blir objekter av klassen, og kan brukes som variabler i programmet vårt. For å kunne bruke objektene i et program, må vi lage referanser til dem. For eksempel kan vi bruke referansen julaften om det objektet som har tilstanden (24, 12, 2015). Seinere kan vi sende en melding til julaften og be objektet endre sin tilstand til (24, 12, 2016). Hvilken oppførsel er det naturlig å knytte til datoer? Her er noen eksempler: Finne neste dag, sammenlikne en dato med en annen dato for å finne ut hvilken som kommer først, finne antall dager mellom to datoer, lage en tekstrepresentasjon av datoen på et bestemt format. Dette programmeres som metoder, og blir tjenester som klassen tilbyr til omgivelsene. Omgivelsene kan sende en melding til et objekt og be om at en slik tjeneste utføres, og objektet vet selv hvordan det skal oppføre seg for å utføre denne tjenesten. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 3 av 22 2.1.2 Objekter og referanser Objekter kan endre tilstand og vi kan endre hvilken referanse som skal referere til hvilket objekt. Eksempel: Vi lager de to Dato-objektene (24,12,2015) og (24, 12, 2016). Det første lar vi julaften referere til, mens nesteJulaften referer til det andre. Se figuren under. Hvis vi sender melding til julaften om at den skal endre tilstand til (24, 12, 2016), får vi følgende bilde: Vi har altså to objekter med samme tilstand men ulik identitet. Hvis vi i stedet for å endre på tilstanden til julaften, lar julaften referere til det samme objektet som nesteJulaften refererer til, slik, vil det ene objektet ikke få noen referanse til seg, mens det andre får to. Et objekt som ikke har noen referanse til seg, kan ikke brukes mer, og vil bli tatt av søppelinnsamlingen (garbage collection). Lagerplassen som det opptar, vil da bli frigjort og kan brukes til andre ting. Vi kan også ha en referanse som ikke viser til noe objekt. Den vil da inneholde den spesielle verdien null. forrige julaften (null) Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 4 av 22 2.1.3 Verdityper og referansetyper Datatypene skilles i to store grupper, verditypene og referansetypene. I verditypene lagrer vi verdiene direkte, for eksempel inneholder en int-variabel et bitmønster som sier hvilket tall vi skal ha. I en referansevariabel derimot, inneholder variabelen adressen til der verdien er lagret. Når vi lager et objekt av en klasse må vi alltid ha en referansevariabel til å lagre adressen. Når vi skriver new, avsettes lagerplass til selve objektet, og i referansen lagres adressen til denne lagerplassen. Før new er utført, finnes ikke objektet, bare en tom referanse (null). Vi har mange datatyper som er innebygd i språket. De enkle typene, int, double, bool, char og varianter av disse, er verdityper. Av innebygde referansetyper har vi string og object. En referanse av string-type kan inneholde adressen til en tekststreng, mens en referanse av object-type kan inneholde adressen til et hvilket som helst objekt. Vi kommer etter hvert til å lage mange egne datatyper, nemlig klasser. Da får vi altså referansetyper. Det er også mulig å lage egne verdityper, et eksempel på det er struct (se punkt 2.3.3). Når programmet vårt kjører (utføres), tildeles programmet to områder i datamaskinen der variabler kan lagres. Disse to områdene kalles stakken og heapen. Alle variabler av verditype lagres på stakken, mens alle variabler av referansetype lagres på heapen. Alle objekter lagres dermed på heapen, mens referansen til dem (den variabelen som inneholder adressen til objektet) lagres normalt på stakken. Variabler lagret på heapen må alltid ha (minst) en referanse til seg for å kunne brukes. 2.2 Klasser 2.2.1 Oversikt over mulige medlemmer Klasser kan inneholde medlemmer av ulike typer. Generelt deler vi klassens medlemmer i to grupper, datamedlemmer og funksjonsmedlemmer. Datamedlemmene deles igjen i tre grupper, variabler, konstanter og hendelser. Funksjonsmedlemmene deles i metoder, egenskaper, konstruktører, destruktører, operatorer og indekserere. De mest grunnleggende av disse, variabler, konstanter, metoder, egenskaper og konstruktører, behandles i denne leksjonen. Noen av de andre, for eksempel hendelser, kommer vi tilbake til senere i kurset. 2.2.2 Metoder En metode er en beskrivelse av hvordan en bestemt oppgave skal utføres (i andre programmeringsspråk kan ordene funksjon eller prosedyre brukes om det samme). Vi kan be om at denne oppgaven skal gjøres, det kalles et metodekall. Vanligvis trenger vi å sende med noen opplysninger for at metoden skal kunne gjøre jobben sin, dette kalles argumentene til metoden. Og vi får ofte tilbake en verdi som resultat av metodekallet, dette er returverdien. Eksempel: Vi skal lage en metode som beregner volumet av en kloss. For å kunne gjøre jobben, må metoden vite lengden, bredden og høyden på klossen. Vi kan for eksempel skrive double volum = beregnVolum(2, 4, 8); Da vil metoden regne ut volumet ut fra argumentene 2, 4 og 8. Returverdien blir 64, altså 2 * 4* 8, som i vårt eksempel lagres i variabelen volum. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 5 av 22 En metode består av et metodehode og et metodeinnhold. Metodehodet forteller navnet på metoden, hvilken datatype returverdien er, hvor mange argumenter den krever, og hvilken type og rekkefølge disse argumentene har. Metodeinnholdet er de programsetningene som skal utføres under metodekallet. Vi kan definere beregnVolum() slik: double beregnVolum(double lengde, double bredde, double høyde) { return lengde * bredde * høyde; } Her har vi satt navn på argumentene, nemlig lengde, bredde og høyde. De er blitt til metodens parametere. Vi bruker betegnelsen argument om det som kommer inn ved metodekall, og parameter om det navnet vi bruker internt i metoden (læreboka bruker disse to begrepene litt om hverandre). Argumenter og parametere må listes opp i samme rekkefølge. Argumentet 8 blir til parameteren høyde siden begge deler står sist i opplistinga. (Unntak: vi kan spesifisere hvilket argument som skal tilhøre hvilken parameter ved å angi navnet, f.eks. kan vi skrive høyde: 8, lengde: 2, bredde: 4. Da spiller rekkefølgen ingen rolle.) Det reserverte ordet return sørger for at metoden avsluttes og en verdi sendes ut som returverdi. Hvis en metode har en returtype, må vi ha minst en return inni metodedefinisjonen. Hvis metoden ikke skal returnere noen verdi (den er definert med returtypen void), behøver vi ikke noen return. Hvis ingen return finnes, avsluttes metoden når siste setning i den er utført. Hvis vi vil at den skal avsluttes tidligere, kan vi skrive return; uten noen verdi. I tillegg til parametre og lokale variabler, kan metoden også bruke objektets medlemsvariabler. Bruk av medlemsvariabler gjør at vi ofte kan klare oss uten, eller med svært få, parametre. 2.2.3 Medlemsvariabler En av de store fordelene med den objektorienterte måten å programmere på, er at vi kan knytte metodene tett sammen med de dataene de skal jobbe med. Det gjør vi ved å la objektene ha medlemsvariabler som metodene kan bruke direkte, uten å gå via argumenter. Hvis vi for eksempel skal be en Dato endre sitt årstall, vet objektet selv hvilket årstall det inneholder, og derfor også hvilket årstall som skal endres. Får metoden beskjed om hvor mye årstallet skal endres (via et argument), kan metoden regne ut hva det nye årstallet blir, og lagre dette i sitt datamedlem. Det er viktig å unngå feil i programmer. En måte å minske risikoen på, er å sørge for at data ikke kan endres på ukontrollerte måter. I forbindelse med klasser gjøres dette ved å la medlemsvariablene være private (nøkkelordet private), noe som innebærer at bare klassen selv har adgang til dem. I den grad andre deler av programmet har behov for å hente fram verdien eller endre verdien til en medlemsvariabel, må disse delene gå via såkalte tilgangsmetoder. Dette er metoder som er definert i klassen, og som har til oppgave å tilgjengeliggjøre dataene på en kontrollert måte. I dato-eksemplet vårt kan det for eksempel være behov for å endre verdi på måneden i et objekt. Hvis hvem som helst kunne gjøre det direkte, kunne vi risikere at det ble puttet inn en ulovlig verdi, for eksempel at måneden ble Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 6 av 22 satt til 13. Men ved at klassen selv ordner det, kan den som programmerer klassen sørge for å legge inn en test som forhindrer slike feil. Det å la medlemsvariablene være private, kalles å innkapsle dem. Når dataene er innkapslet, er de beskyttet mot uautorisert bruk. Vi lar alltid dataene være innkapslet, selv om det er mulig å programmere slik at de ikke er det. De fleste metoder derimot, er offentlige (public). Det betyr at hvem som helst kan bruke dem. Det er naturlig siden metoder nettopp har til hensikt å tilby tjenester til utenverdenen. Noen metoder er imidlertid tenkt som hjelpemetoder (”underleverandører”) til en eller flere andre metoder, da kan det være aktuelt å ha dem som private metoder. Tommelfingerregelen er at vi lar alt som ikke utenverdenen trenger å bruke, være privat. Selv om klassens metoder har tilgang til de private dataene, er det ofte lurt å gå via tilgangsmetodene likevel. Da slipper vi å passe på at dataene blir brukt riktig alle plasser de brukes, det gjøres en gang for alle i tilgangsmetodene. Dermed står vi mye friere til å endre på de private delene. For eksempel kan vi tenke oss at vi finner det mer praktisk å la månedsnumrene gå fra 0 til 11 i stedet for fra 1 til 12. Brukes månedsnummeret direkte i mange metoder, må vi gå gjennom alle disse og gjøre de nødvendige endringene. Hvis de andre metodene derimot kun bruker tilgangsmetodene, trenger vi bare å endre tilgangsmetodene. 2.2.4 Konstruktører En konstruktør har til oppgave å lage et nytt objekt. Den har alltid samme navn som klassen. Når vi skriver Dato dato = new Dato(); bruker vi konstruktøren Dato(). Her sier vi at dato er en referanse til et objekt av typen Dato. Operatoren new lager et nytt Dato-objekt, og konstruktøren Dato() forteller hvordan det skal gjøres. En klasse kan ha mer enn en konstruktør, for eksempel er det naturlig å kunne putte inn dag, måned og år i en Dato, slik: Dato julaften = new Dato(24, 12, 2015); Eller vi kan ønske at en ny dato får samme verdi som en annen: Dato kopi = new Dato(julaften); Alle disse konstruktørene må programmers i klassen Dato for å kunne brukes. Vi får altså tre metoder med samme navn, men med ulik argumentliste, så de blir ”overloaded” (mer om dette i punkt 2.3.4). En konstruktør uten argumentliste kalles en standardkonstruktør. Kompilatoren lager automatisk en enkel standardkonstruktør hvis vi ikke definerer egne konstruktører. Men hvis vi har laget minst en konstruktør selv, og dette ikke er en standardkonstruktør, vil ikke klassen få noen standardkonstruktør. En annen spesiell ting med konstruktører er at de ikke har noen returtype, ikke en gang returtypen void. I eksemplet i 2.2.6 skal vi se nærmere på hvordan konstruktører kan programmeres. Hva er forskjellen på å skrive Dato kopi = new Dato(julaften) og Dato kopi = julaften? I det første tilfellet lager vi et nytt objekt og gir det samme startverdier på medlemsvariablene som julaften har. Disse verdiene kan vi seinere endre uten at det har Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 7 av 22 noen innvirkning på verdiene som er lagret i julaften. I det andre tilfellet får vi to navn (referanser) på samme objekt. Hvis vi endrer verdiene i kopi, vil verdiene også endres i julaften. Motsatsen til en konstruktør er en destruktør (finalizer). Den har som oppgave å tilintetgjøre et objekt. Destruktører kan hjelpe oss til å unngå at ressurser blir brukt opp fordi de er knyttet til objekter som ikke lenger er i bruk. 2.2.5 Klassemedlemmer Noen ganger er det mer naturlig å knytte en metode til en hel klasse, ikke til bestemte objekter av klassen. Da har vi det som kalles en klassemetode (statisk metode) (i motsetning til en vanlig metode som i denne sammenhengen kalles en objektmetode). Det er aktuelt når metoden ikke bruker data knyttet til objektene, men bare tilbyr en tjeneste som jobber mot data som medsendes metoden som argumenter. Når vi bruker en slik metode, angir vi klassenavnet, ikke navnet på et objekt. Klassen Math er et godt eksempel på dette. Vi kan for eksempel skrive Math.Sqrt(5.0) for å få beregnet kvadratrota av 5.0. Det kan vi gjøre fordi Sqrt() er definert som en klassemetode. Hadde den ikke vært det, måtte vi først ha laget oss et objekt, Math m = new Math(), og deretter laget metodekallet m.Sqrt(5.0). (Men siden Math ikke har noen objektmetoder, er det aldri behov for å instansiere klassen. De som har laget C# har derfor valgt å la en slik instansiering gi kompileringsfeil.) Vi kan også ha klassevariabler. Vi vet at for vanlige medlemsvariabler (objektvariabler) får vi en variabel for hvert objekt vi lager. Disse kan endres uavhengig av hverandre. En klassevariabel finnes det derimot en og bare en utgave av, uansett hvor mange objekter vi lager (også hvis vi ikke lager noen objekter av klassen). Og eventuelle endringer gjort av ett objekt får virkning for alle de andre objektene også. Men oftere vil det være aktuelt å ha klassekonstanter. Hvis en verdi ikke endrer seg, er det sjelden noen grunn til at hvert objekt skal ha sin egen utgave av den. En tredje variant er en klassekonstruktør. Det er en konstruktør som utføres bare en gang for klassen, ikke en gang for hvert objekt som lages. Dette kan brukes hvis vi trenger å initiere et eller annet før det lages objekter av klassen. Nøkkelordet static brukes for å fortelle at vi har et klassemedlem, const at vi har en klassekonstant. Du har allerede brukt klassemetoden Main(). En klassemetode kan ikke kalle objektmetoder direkte siden objektmetoder krever et objekt. Derfor vil du få feilmelding hvis Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 8 av 22 du i Main() prøver å bruke en objektmetode uten å knytte den til et objekt. Det samme gjelder bruk av objektvariabler. 2.2.6 Eksempel Vi skal lage en klasse som skal representere sirkler. En sirkel har en radius. Radiusen må vi kunne sette og hente fram igjen. Når vi vet radiusen, kan vi beregne areal og omkrets. Dermed får vi en medlemsvariabel, radius, og fire metoder: settRadius(), finnRadius(), beregnAreal() og beregnOmkrets(). Klassen kan se slik ut: public class Sirkel { private int radius; // Konstruktører public Sirkel() { // Tom, radius settes automatisk lik 0 } public Sirkel(int radius) { this.radius = radius; } public Sirkel(Sirkel original) { radius = original.finnRadius(); } // Tilgangsmetoder public int finnRadius() { return radius; } public void settRadius(int radius) { this.radius = radius; } // Andre metoder public double beregnAreal() { return Math.PI * finnRadius() * finnRadius(); } public double beregnOmkrets() { return 2 * Math.PI * finnRadius(); } } // slutt klasse Sirkel Klassen er definert som offentlig (public). Det gjør at alle kan bruke objekter av klassen. Først kommer den private medlemsvariabelen radius. For enkelhets skyld har vi latt den være et heltall, vi kunne også latt den være et desimaltall. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 9 av 22 Deretter har vi tre offentlige konstruktører. Den første lager en sirkel med radius 0 og blir standardkonstruktør. Den kan være kjekk å ha hvis vi trenger et Sirkel-objekt før vi vet hvor stor sirkelen skal være. Da kan vi senere endre radiusen ved hjelp av tilgangsmetoden settRadius(). Selv om den er tom (inneholder ingen setninger), må vi ha den med for at klassen skal få en standardkonstruktør (siden vi har andre konstruktører også). Hvis vi vet hvor stor sirkel vi skal lage, bruker vi den andre konstruktøren. Her sender vi med beskjed om hvor stor radiusen skal være. Legg merke til at parameteren radius har samme navn som medlemsvariabelen radius, men det er to forskjellige variabler! For å skille dem fra hverandre, kan vi bruke det reserverte ordet this. this er navnet på det objektet vi holder på med akkurat nå, så this.radius betyr medlemsvariabelen radius. Vi setter altså medlemsvariabelen radius lik den radius vi får inn via parameteren. Det er nemlig slik at hvis en parameter (eller lokal variabel) har samme navn som en medlemsvariabel, kan ikke medlemsvariabelens navn brukes direkte, det skjules av den andre variabelens navn. For likevel å kunne bruke det, må vi bruke this til å markere at det er medlemsvariabelen vi mener. Vi kan også bruke this selv om det ikke er nødvendig, for å markere at det er dette objektet, og ikke et annet, vi bruker. Den tredje konstruktøren lager en kopi av et Sirkel-objekt. Det gjør vi ved å sette radius i det nylagede objektet lik den radius som originalen har. Generelt lager vi en kopi av et objekt ved å kopiere originalens tilstand, dvs. verdien på alle medlemsvariablene. Her har vi valgt å bruke metoden finnRadius() for å hente fram originalens radius. Vi kunne også ha brukt objektets medlemsvariabel, og skrevet radius = original.radius;1. Men som nevnt tidligere, er det ofte lurt å gå via tilgangsmetodene. Det kunne vi også ha gjort for å sette radiusen, vi kunne skrevet settRadius(original.finnRadius());. I eksemplet over gjør vi litt av hvert for å demonstrere flere forskjellige måter å uttrykke ting på. Siden vi her har to objekter å forholde oss til, kan det være en idé å bruke this for å tydeliggjøre hvilket objekt vi mener, altså skrive this.radius = original.radius; eller this.settRadius(original.finnRadius()); Så kommer vi til tilgangsmetodene. Her har vi en metode for å hente fram verdien av radius, og en for å sette ny verdi på den. Når vi programmerer en klasse, må vi vurdere hvilke tilgangsfunksjoner vi trenger. Vanligvis er det naturlig å ha finn-metoder for alle medlemsvariablene, men det er slett ikke sikkert at vi skal ha sett-metoder for alle. Det kommer an på om vi ønsker at objektene skal være mutable (foranderlige) eller immutable (uforanderlige). Immutable objekter får sine verdier gjennom konstruktøren, og kan seinere aldri endres. Mutable objekter har derimot metoder for å endre (noen av) medlemsvariablene. I vårt eksempel har vi valgt å la Sirkel-objektene være mutable. Vi har derfor en sett-metode som lar oss endre på radius. Til slutt har vi metodene beregnAreal() og beregnOmkrets(). Klassekonstanten PI finnes i klassen Math. Her er noen eksempler på hvordan vi kan lage objekter av klassen Sirkel, og bruke dem i beregninger: Sirkel minSirkel = new Sirkel(10); // en sirkel med radius 10 1 Selv om radius er privat, kan this få tak i original sin radius, for tilgangskontrollen går på klassen, ikke det enkelte objekt. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 10 av 22 double areal = minSirkel.beregnAreal(); Sirkel enAnnen = new Sirkel(); enAnnen.settRadius(2 * minSirkel.finnRadius()); // radius 20 if (enAnnen.beregnOmkrets() > enAnnen.beregnAreal()) { Console.WriteLine(”Omkretsen er større enn arealet”); } Sirkel endaEn = new Sirkel(minSirkel); if (endaEn.finnRadius() != minSirkel.finnRadius()) { Console.WriteLine(”Her er noe alvorlig galt!”); } 2.3 Mer om klasser og deres innhold 2.3.1 Egenskaper Vi har sett at vi trenger tilgangsmetoder for medlemsvariablene. I C# har vi en spesiell mekanisme for å forenkle og standardisere slike tilgangsmetoder. Dette kalles egenskaper (properties). En egenskap defineres som en metode, men brukes som en medlemsvariabel. Vi tar fram igjen klassen Sirkel. Her er hvordan vi kan lage egenskapen Radius ut fra medlemsvariabelen radius: public int Radius { get { return radius; } set { radius = value; } } Vi ser at finnRadius() er blitt til get og settRadius er blitt til set. Det reserverte ordet value betegner den verdien som skal settes (tilsvarer parameteren radius i settmetoden). Radius kan nå brukes som om den var en medlemsvariabel, for eksempel int r = minSirkel.Radius; eller minSirkel.Radius = 20;. Siden den er offentlig, kan den brukes av hvem som helst, slik at denne setningen er gyldig også utenfor klassen. Det vi har oppnådd, er å gi utenverdenen en enkel måte å få tilgang på medlemsvariablene på, uten at vi har gitt avkall på den beskyttelsen som tilgang via tilgangsmetoder gir oss. Hvis vi vil at det skal være umulig å endre på en egenskap, sløyfer vi set og har bare med get. Eller vi kan skrive private set for å oppnå at bare klassen selv kan endre egenskapen. Fra og med c# 2008 er det innført en enkel måte å lage egenskaper på, ved bare å skrive public int Radius { get; set; } eller eventuelt Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 11 av 22 public int Radius { get; private set; } Da trengs ikke den private variabelen radius, det blir opprettet en tilsvarende variabel automatisk. Denne måten å gjøre det på, kan bare brukes hvis vi ikke trenger å ha noen annen kode enn akkurat det å sette og hente verdien, og begge deler må være med. Vi kan for eksempel ikke bruke denne skrivemåten hvis vi ønsker å kontrollere at radiusen alltid er større eller lik 0, eller hvis vi bare ønsker get, ikke set. 2.3.2 Konstanter Vi så tidligere at vi kunne lage en klassekonstant ved å bruke nøkkelordet const, for eksempel slik: public const double Skatteprosent = 0.28; (Konstanter kan godt være offentlige siden det ikke er noen fare for at noen kan endre dem.) Denne måten å gjøre det på, er grei hvis alle objekter skal ha samme verdi på konstanten. Men noen ganger trenger vi å lage ulik verdi for de ulike objektene. For eksempel er det ikke sikkert at alle har samme skatteprosent. Da trenger vi en objektkonstant. Det kan lages ved å bruke nøkkelordet readonly, slik: public readonly double Skatteprosent; Readonly-konstanter får sin verdi i konstruktøren, og kan senere ikke endres. Vi kan for eksempel lage en konstruktør som tar inn skatteprosenten som et argument, eller vi kan få konstruktøren til å beregne den på en eller annen måte. Readonly-konstanter kan også deklareres som static og altså bli klassekonstanter. Fordelen med dette framfor å bruke const, er at de fortsatt settes i konstruktøren, altså under kjøring, ikke ved kompilering, slik at vi kan gjøre beregninger før verdien fastsettes. 2.3.3 Strukturer Objekter kan være enten av klassetype eller av strukturtype. Hovedforskjellen på en klasse og en struktur, er at klasser er referansetyper, mens strukturer er verdityper. Ellers gjelder det aller meste av det som er sagt ovenfor om klasser, også for strukturer. Strukturer er beregnet til bruk for små datamengder. Har vi f.eks. bruk for å returnere to intverdier fra en metode, kan vi definere en struktur for dem og returnere denne. Det at strukturer er verdityper, medfører at Vi slipper den ekstra minneadministreringen som kreves når vi oppretter referansetyper. Men hvis en slik variabel settes lik en annen, kopieres hele innholdet, ikke bare en referanse (gjelder for eksempel ved verdioverføring av variabelen). Til sammen gjør disse argumentene at strukturer er effektivt ved små datamengder, mens klasser egner seg ved større. For små, enkle objekter med begrenset levetid, kan vi tillate oss å slakke litt på kravet om at datamedlemmer skal være private og ha tilgangsmetoder. Da kan vi lage en enkel struktur slik: public struct Returverdi { Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 12 av 22 public int Lengde; public int Høyde; } Et objekt av strukturen kan lages slik: Returverdi r = new Returverdi(); eller slik: Returverdi r; Begge måtene gjør at vi får en variabel som lagres på stakken (er verditype), ikke på heapen (slik referansetypene blir). I begge tilfeller kan vi sette datamedlemmenes verdier slik: r.Lengde = 10; r.Høyde = 5; Hvis vi har laget en konstruktør med to argumenter, kan vi bruke den første metoden til å sette verdier på datamedlemmene også: Returverdi r = new Returverdi(10, 5); Konstruktøren lages på samme måte som før: public Returverdi(int lengde, int bredde) { Lengde = lengde; Bredde = bredde; } 2.3.4 Overloading Vi kan lage flere metoder med samme navn. Dette er gunstig når vi har situasjoner der vi egentlig skal gjøre det samme, men har argumenter med ulik type. WriteLine() er et typisk eksempel på dette. Vi må kunne skrive ut både tall, tegn og tekststrenger, ja til og med objekter kan det være kjekt å kunne skrive ut. Derfor tilbys metoden i mange ulike varianter. Kompilatoren må ha en måte å skille mellom kall til ulike metoder med samme navn. Det kan den gjøre ved å se på antallet, typen og rekkefølgen til parameterne. En metode med en parameter er forskjellig fra en med to parametre, en metode der parameteren er en tekststreng er forskjellig fra en der parameteren er et heltall og en metode der første parameter er en tekststreng og andre parameter et heltall er forskjellig fra en metode der første parameter er et heltall og andre parameter er en tekststreng. Derimot spiller det ingen rolle hvilke navn vi bruker på argumenter eller parametere, og det spiller heller ingen rolle hvilken returtype metoden har. En metodes signatur er metodenavnet og parameterlista, altså metodehodet minus returtypen. Hvis to metoder har samme signatur, blir de regnet som samme metode, og vi får kompileringsfeil hvis vi prøver å definere den to ganger. Hvis signaturen er forskjellig, men metodenavnet er likt, er det to forskjellige metoder med overloading. Her er alle de ulike variantene av WriteLine() (hentet fra dokumentasjonen). Du ser at det finnes en variant uten noen argumenter (hvorfor kan det ikke finnes fler enn en?), 12 varianter med ett argument, to med to argumenter, to med tre argumenter og en med fire argumenter. Alle har returtype void. Det er praktisk for oss som skal bruke dem så vi slipper å huske så mye forskjellig, men det er ikke noe krav fra kompilatorens side. Legg også merke til at vi ikke har noe navn på parameterene. Det er ikke nødvendig siden disse navnene ikke har noen betydning for signaturen. Det viktige er antallet, typen og rekkefølgen av parameterene. (Noen av parameterene er oppgitt med datatyper du kanskje ikke kjenner. Det er ikke Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 13 av 22 meningen at du skal forstå detaljene i alt dette, poenget er å se at de finnes mange metoder med samme navn men forskjellige signaturer.) public public public public public public public public public public public public public public public public public public static static static static static static static static static static static static static static static static static static void void void void void void void void void void void void void void void void void void WriteLine(); WriteLine(bool); WriteLine(char); WriteLine(char[]); WriteLine(decimal); WriteLine(double); WriteLine(int); WriteLine(long); WriteLine(object); WriteLine(float); WriteLine(string); WriteLine(uint); WriteLine(ulong); WriteLine(string, object); WriteLine(string, params object[]); WriteLine(char[], int, int); WriteLine(string, object, object); WriteLine(string, object, object, object); Når kompilatoren skal finne ut hvilken metode som skal kalles, går den gjennom lista av metoder med det aktuelle navnet. Hvis den finner en metode med en signatur som passer nøyaktig, brukes den. Hvis ikke, prøver den å omforme argumentene slik at det passer. Eksempel: Gitt metodene finnDato(int dag) og finnDato(string dag). Hvis vi sender inn en char som argument, vil den første versjonen brukes siden char kan omformes til int, men ikke til string. En annen variant (som ikke er overloading) er metoder som har et eller flere standardargumenter. Et standardargument er et argument som får verdi når metoden defineres. Så kan den som skal bruke metoden bestemme om standardverdien skal brukes eller ikke. Eksempel: public void skrivLinjer (string hvaSkalSkrives, int antallLinjer = 1) { for (int i=0; i < antallLinjer; i++) { Console.WriteLine(hvaSkalSkrives); } } Denne metoden kan enten kalles ved å skrive skrivLinjer(tekst); Da får vi en linje. Eller vi kan be om 4 linjer ved å skrive skrivLinjer(tekst, 4); Parametre med standardargumenter må alltid stå bakerst i parameterlista. 2.3.5 Partielle klasser Normalt lar vi koden for en hel klasse finnes på en og bare en fil. Men i noen tilfeller kan det være hensiktsmessig å dele den på mer enn en fil. Dette blir aktuelt for oss når vi begynner å lage Web-applikasjoner. Da lager Visual Studio to filer. På den ene lagres den koden vi lager Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 14 av 22 selv, på den andre lagres det som Visual Studio generer for oss. Da lages altså to partielle klasser, og disse settes sammen til en fullstendig klasse ved kompilering. 2.4 Argumentoverføring 2.4.1 Innargumenter, utargumenter og kombinerte argumenter Når vi skal lage en metode, er det lurt å starte med å finne ut hvilke opplysninger metoden trenger for å kunne gjøre jobben sin. Dette blir argumenter som kommer inn til metoden, og vi kaller dem inn-argumenter. Deretter må vi finne ut hva som skal komme ut av metoden. Noen ganger skal metoden bare gjøre noe, uten at vi forventer noe spesielt svar tilbake – for eksempel skrive til skjermen. Da får vi en metode med returtype void. Andre ganger vil resultatet være en eller flere verdier som skal formidles tilbake til den kallende metoden. Er det bare én verdi som skal ut, kan vi bruke returverdien fra metoden. I C# er det (i motsetning til for eksempel i Java) imidlertid også mulig å bruke argumenter til dette, da får vi det som kalles ut-argumenter (de sender verdier ut av metoden). Det siste tilfellet er når vi først må få en opplysning inn til metoden, deretter endre den og sende den ut igjen. Da får vi et kombinert inn-/ut-argument. Eksempel: Vi skal ha en metode som skal være slik at første og andre argument skal bytte verdi. Da trenger vi verdien til første argument for å kunne endre verdi på andre argument, og vi trenger verdien til andre argument for å kunne endre verdien på første argument. 2.4.2 Verdioverføring av verdityper I de eksemplene vi har sett på hittil, har vi for det meste brukt verdityper som argumenter til metodene. Da får vi (hvis vi ikke spesifikt angir noe annet) det som kalles verdioverføring av argumentene: metodens parametere får samme verdi som argumentene, de blir altså kopier av argumentene. Parametrene fungerer som de lokale variablene, de opprettes når metoden kalles og forsvinner igjen straks metoden er ferdig. Inni metoden kan de godt endres, men siden de bare er kopier av argumentene, har dette ingen effekt på argumentenes verdier. Verdioverføring egner seg derfor bare til innargumenter. 2.4.3 Referanseoverføring av verdityper Hvis vi vil endre på et argument, må vi bruke referanseoverføring. Referanseoverføring innebærer at det ikke er verdien, men adressen til variabelen som overføres, og metoden får dermed adgang til den lagerplassen som argumentet er lagret i. Argument og parameter blir derfor samme variabel, og endring i parameteren gir tilsvarende endring i argumentet. For å fortelle at vi ønsker referanseoverføring, bruker vi det reserverte ordet ref. Her er metoden byttVerdi() som bytter om verdiene på de to argumentene: static void byttVerdi(ref double a, ref double b) { double temp = a; a = b; b = temp; } Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 15 av 22 Når vi bruker metoden, må vi også passe på å få med oss ordet ref: byttVerdi(ref lengde, ref høyde); fører til at lengde får den verdien som høyde hadde, og høyde får den verdien som lengde hadde. Men nøkkelordet ref forteller også at argumentet må ha fått en verdi før metoden kalles. Det egner seg for kombinerte inn/ut-argumenter siden disse nettopp har bruk for argumentets startverdi. Derimot er det ikke nødvendig med en startverdi på utargumenter, siden slike argumenter skal få sin verdi inni metoden. Da skriver vi out i stedet for ref og forteller dermed kompilatoren at startverdi ikke er nødvendig. Vi får fortsatt referanseoverføring av argumentene. Eksempel: Beregning av kroner og øre static void omformTilKronerOgØre(double beløp, out int kr, out int øre) { kr = (int)beløp; double temp = (beløp – kr) * 100; øre = (int)(temp + 0.5); // Avrunding i stedet for avkutting } Bruk: ... double beløp = 100.0 / 3.0; int kr; int øre; omformTilKronerOgØre(beløp, out kr, out øre); Console.WriteLine("beløpet blir " + kr + " krone(r) og " + øre + " øre."); ... Referanseoverføring er grei å bruke hvis det er mer enn en ting vi ønsker å få ut av metoden fordi vi kan referanseoverføre så mange argumenter som vi måtte ønske. Er det bare en ting som skal ut, bruker vi returverdi, er det flere, bruker vi referanseoverføring. Referanseoverføring har imidlertid et par ulemper i forhold til verdioverføring: Ved verdioverføring kan et argument være et hvilket som helst uttrykk – en variabel, en konstant eller et sammensatt uttrykk. Ved referanseoverføring må argumentet ha en adresse, og derfor kan bare en variabel brukes. Verdioverføring er en sikrere overføringsmetode. Ved referanseoverføring må den kallende metoden overlate sin variabel til en annen metode og må stole på at denne metoden faktisk gjør det som forventes og ikke ødelegger variabelen. Oppsummering: Bruk verdioverføring mest mulig. Referanseoverføring brukes bare når det er helt nødvendig. Innargumenter trenger ingen spesielle merker på seg. De blir verdioverført. Kombinerte argumenter trenger nøkkelordet ref for å fortelle at de er referanseoverført med startverdi. Utargumenter trenger nøkkelordet out for å fortelle at de er referanseoverført uten krav om startverdi. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 16 av 22 2.4.4 Verdioverføring av referansetyper For referansetyper fungerer argumentoverføringen annerledes. Der er det selve referansen som verdioverføres og som dermed kopieres. Men siden dette er adressen til objektet, vil altså argumentet og parameteren bli to referanser til samme objekt. Vi får derfor direkte tilgang til selve objektet, og kan endre dets tilstand (hvis det er et mutabelt objekt). Derfor sier vi ofte at objektet referanseoverføres, når det helt presist er referansen til objektet som verdioverføres. Eksempel: I konstruktøren public Sirkel(Sirkel original) { radius = original.finnRadius(); } vil parameteren original være en referansetype. Hvis vi inni denne konstruktøren legger til setningen original.radius = 0; og senere skriver Sirkel denne = new Sirkel(10); Sirkel denAndre = new Sirkel(denne); vil denAndre få radius 10, mens denne får endret sin radius til 0, siden original viser til samme objekt som denne. Se figuren under. Utføring av konstruktøren Sirkel denAndre = new Sirkel(denne) denne denne 0 denAndre 10 10 Før starten original Like før avslutning denne 0 denAndre 10 Etter avslutning 2.4.5 Referanseoverføring av referansetyper Det er også mulig å referanseoverføre referansen. Da kan vi ikke bare endre på objektet, men også på referansen, dvs. vi kan få referansen til å referere til et annet objekt. Det kan være nyttig i noen spesielle tilfeller. Eksempel: Vi ønsker en metode som sorterer to tekststrenger slik at første argument inneholder den minste og andre argument den største. Da kan vi skrive Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 17 av 22 static void sorter(ref string a, ref string b) { if (a.CompareTo(b) > 0) { string temp = a; a = b; b = temp; } } Men også her gjelder regelen om at vi bruker verdioverføring mest mulig, og referanseoverføring bare hvis det er nødvendig. 2.5 Objektorientert design 2.5.1 Use-case og scenarier Til slutt skal vi se litt nærmere på prinsippene for objektorientert design. Dette er ikke dekket i læreboka, men kan være nyttig å vite litt om likevel. Før vi begynner å programmere, må vi vite hva vi skal lage og hvordan det skal fungere. En måte å starte på, er å tenke seg systemet i bruk, og følge prosessen fra start til mål. Da lister vi opp typiske måter å bruke systemet på (use-case), og lager scenarioer som viser interaksjonen mellom ulike deler av systemet etter hvert som prosessen går framover. Eksempel: Tenk deg at vi skal lage et system for administrering av lønnsutbetalinger i en bedrift. Alle ansatte får betaling i henhold til innleverte timelister der de angir hvilken type arbeid de har gjort og hvor mange timer. Use-caser: - Hver av de ansatte skriver inn en arbeidstype og et antall timer. Kanskje må prosessen gjentas fordi flere typer arbeid skal registreres. - Lønningssjefen skal produsere en liste med antall timer arbeidet og lønna for alle ansatte. - Produksjonssjefen skal holde oversikt over hva som faktisk gjøres. Hun må derfor produsere lister over antall timer jobbet med ulike typer arbeid. Et scenario for første use-case kan være: En ansatt logger seg inn på systemet og blir bedt om å oppgi arbeidstype og antall timer. Deretter finner systemet riktig person i sin database, og registrerer den angitte arbeidstypen og timetallet. Deretter spør systemet om brukeren vil registrere flere arbeidstyper og timer. Hvis ja, foreta samme prosess en gang til. Hvis nei, logg ut bruker. Sideløp: Brukeren ønsker å rette opp en feil – Hva skal skje da? Objekter: Bruker, system, database. Flere vil nok dukke opp hvis vi begynner å gå mer detaljert til verks (detaljdesign og/eller programkode). Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 18 av 22 2.5.2 UML, klassediagram og sekvensdiagram UML (Unified Modelling Language) er en standard for hvordan ulike sammenhenger innen objektorientert design/programmering kan framstilles. Det er definert mange typer diagrammer, hvorav klassediagram kanskje er det enkleste og mest benyttede. I et klassediagram kan vi blant annet framstille 1. Objektenes innhold (variabler, metoder) 2. En-del-av-forhold mellom objekter 3. Arvesammenheng mellom klasser Et klassediagram for en enkel klasse kan bestå av klassenavnet og de medlemsvariabler og metoder som finnes der, for eksempel slik: Bruker navn SjekkPassord FinnArbeidstype FinnTimeantall Figur 1: Klassediagram for klassen Bruker Her er Bruker navnet på klassen. Den har medlemsvariabelen navn og metodene SjekkPassord, FinnArbeidstype og FinnTimeantall. Vi skal se på mer kompliserte klassediagram når vi kommer til en-del-av-forhold (i punkt 2.5.3 og arv (i leksjon 4). Et sekvensdiagram prøver å framstille samarbeidet mellom objekter, og i hvilken rekkefølge ting skjer. Se figuren under. Der gir først brukeren systemet beskjed om at han ønsker å gjøre en ny registrering. Da må systemet først verifisere brukerens identitet, noe det gjør ved å sende en forespørsel (melding) til databasen. Etter at databasen har gitt beskjed om at brukeren er ok, sender systemet en forespørsel til brukeren om å angi data. Brukeren sender dataene tilbake til systemet, hvorpå systemet sjekker dataene ved å sende en forespørsel til (en annen del av) seg selv. Når dataene er godkjent, sendes de til databasen for registrering. Databasen gir beskjed tilbake om at registreringen er utført, og systemet gir til slutt brukeren beskjed om det samme. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 19 av 22 En bruker Systemet Databasen Ny registering Sjekk brukerid ok Angi data Register data Sjekk data Register data Ok Ok Figur 2: Sekvensdiagram for registering av data 2.5.3 Samarbeid mellom objekter og en-del-av-forhold Objekter samarbeider ved å sende meldinger til hverandre. Meldingene består i å be et objekt utføre en metode og gjerne få et svar tilbake. I vårt eksempel sender objektet enBruker meldingen NyRegistering() til objektet Systemet som igjen sender meldingen SjekkBrukerId til objektet Databasen. Databasen utfører metoden og sender svaret tilbake til Systemet (se figur 2). Ofte vil det være et mer komplisert samarbeid som foregår. La oss tenke oss at vi holder på å programmere en banktjeneste. Da trenger vi kanskje et bank-objekt og en rekke kontoobjekter. Hvis vi skal lage en metode for å overføre et beløp fra en konto til en annen, kan bank-objektet sende melding til et konto-objekt om at det skal redusere sin saldo med et bestemt beløp, og til et annet konto-objekt om at det skal øke sin saldo tilsvarende. Bankobjektet behøver ikke å vite hvordan de to konto-objektene faktisk utfører forespørselen, det er nok å vite at de gjør det. En konto har (blant annet) et kontonummer, en kontoeier og en saldo. Dette blir medlemsvariabler i konto-klassen. Vi sier at kontonummeret er en del av konto-objektet. I figur 3 har vi tegnet klassediagram for Konto-klassen, men har for enkelhets skyld droppet metodene. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 20 av 22 Konto kontonummer saldo kontoeier Figur 3: Klassediagram for klassen Konto Bank-objektet på sin side, har mange konto-objekter, ett for hver konto som finnes i banken. Kontoene er altså en del av bank-objektet. Vi kan bygge opp komplekse objekter ved å sette det sammen av mange deler, som hver for seg kan være objekter bestående av nye deler. Slike en-del-av-forhold kan deles i ulike typer: Komposisjon: Delene er fast knyttet til det sammensatte objektet, og har bare mening så lenge det overordnede objektet eksisterer. For eksempel har det ingen mening å snakke om kontonummeret hvis kontoen ikke eksisterer. I figur 4 er dette vist i et UML-diagram. Den svarte firkanten forteller at det er en komposisjon. Datamedlemmet kontonummer er trukket ut og viser hvilken rolle klassen String spiller i denne sammenhengen – vi skal ha en tekststreng som inneholder kontonummeret. Ett-tallet nærmest Konto-klassen forteller at kontonummeret tilhører en og bare en konto, mens ett-tallet nærmest String-klassen forteller at hver konto bare inneholder ett kontonummer. Konto 1 kontonummer 1 String saldo kontoeier Figur 4: Komposisjon Aggregering: Delene er mindre fast knyttet til det overordnede objektet. En del kan være knyttet til flere objekter. For eksempel kan en kontoeier være tilknyttet flere kontoer. I figur 5 har vi tegnet en aggregering, vist ved den hvite firkanten. Vi sier at rollen som kontoeier skal fylles av et objekt av typen Navn. Figuren viser også at hver konto bare kan ha en eier (ett-tallet nærmest klassen Navn), mens en eier kan eie mange kontoer (stjerna nærmest klassen Konto). Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 21 av 22 Konto 1 kontonummer 1 String kontoeier 1 Navn saldo * fornavn etternavn finnFornavn finnEtternavn Figur 5: Aggregering Assossiasjon: Det er en viss tilknytning mellom objektene, men det er ikke lenger gitt hvem som ”eier” hvem (eller at noen av dem eier den andre). For eksempel kan vi tenke oss at bank-objektet har en rekke konto-objekter samt en rekke kontoeierobjekter. Konto og kontoeier er knyttet sammen med en assosiasjon, men det er bankobjektet som "eier" begge. Se figur 6, der vi har sagt at en konto kan være assosiert til mange navn, og et navn kan være assossisert til mange kontoer. Bank kontoer 1 * Konto * kontonummer * kontoeier 1 kontoeiere * Navn Figur 6: Assossiasjon Hvilken type en-del-av-forhold vi har, vil ha betydning for hvordan vi programmerer klassene. Har vi en komposisjon, skal delene eksistere bare når det overordnede objektet eksisterer. Hvis utenverdenen skal ha tak i en av delene, får de en kopi, slik at vi ikke risikerer at noen refererer til en del som ikke lenger eksisterer. For eksempel vil ikke bank-objektet la en applikasjon få direkte tilgang til sine konto-objekter, bare til kopier av dem. Har vi derimot en aggregering, vil det være naturlig at andre får tilgang til selve objektene. Har flere kontoer samme kontoeier, ønsker vi at hvis den ene kontoen endrer adressen til kontoeieren, vil den endrede adressen også gjelde for de andre kontoene som har samme eier. Og det kan vi ikke få til hvis de ulike kontoene har hver sin kopi av kontoeier-objektet, de må ha tilgang til det samme objektet. Opphavsrett: Forfatter og Stiftelsen TISIP Klasser og objekter side 22 av 22 For begge disse typene en-del-av-forhold vil ”kommandolinjen” vanligvis gå fra det overordnede objektet til de ulike delene. Bank-objektet gir for eksempel beskjed til et kontoobjekt om å oppdatere saldoen. Derimot er det ikke naturlig at konto-objektet ber bankobjektet utføre en oppgave. I praksis programmerer vi det slik at det overordnede objektet inneholder en referanse til del-objektet. For assosiasjoner kan det være aktuelt med kommandoer begge veier. Kontoeier-objektet kan ønske å få sjekket saldoen på alle sine kontoer mens kontoen kan gi kontoeier-objektet beskjed om å endre adresse. I slike tilfeller må vi ha forbindelse begge veier. Som en fellesbetegnelse for disse typene bruker vi begrepet en-del-av-forhold. Bare hvis vi eksplisitt trenger å presisere typen nærmere, vil vi bruke de presise betegnelsene. 2.5.4 Litt om bruk av grensesnitt Vi har sagt tidligere at utenverdenen ikke behøver å vite hvordan en metode er implementert, det er nok å vite hvordan den skal brukes. I design-fasen er det derfor greit å bare navngi metoder og spesifisere hensikten med dem, så kan implementasjonen vente til senere. Noen ganger finner man ut at en metode kan være kjekk å ha i flere forskjellige klasser. For eksempel kan vi sammenlikne to tekststrenger, men det kan også være nyttig å kunne sammenlikne to konto-objekter eller to dato-objekter. Generelt vil mange klasser kunne ha nytte av en sammenlikningsmetode. I slike tilfeller definerer vi et grensesnitt (interface) for den (eller de) felles metoden(e) og lar klassene implementere dette grensesnittet. Grensesnitt navngis gjerne med en I (for interface) først, for eksempel har vi grensesnittet IComparable som inneholder metoden CompareTo(). CompareTo() er definert slik at den returnerer en negativ verdi hvis objektet som meldingen blir sendt til er mindre enn det objektet som sendes med som argument, 0 hvis de er like og en positiv verdi returneres hvis det objektet som meldingen sendes til er større enn argumentet. Klassen String (og mange andre) implementerer dette grensesnittet. Dermed vet vi at strengobjekter kan sammenliknes, men for å finne ut hvordan det faktisk gjøres, må vi se på dokumentasjonen for String-klassen. For objekter av andre klasser vil sammenlikningen skje på andre måter, hver klasse må lage sin egen implementasjon av metoden. Poenget med grensesnitt er at det skal være et enhetlig ansikt utad, kjenner du bruken i en klasse, vet du at den brukes på samme måte i andre klasser. Kontoer kan sammenliknes hvis vi skriver følgene: class Konto : IComparable { … public virtual int CompareTo(object obj) { Konto detAndre = (Konto)obj; int sml = kontonummer.CompareTo(detAndre.kontonummer); return sml; } } Opphavsrett: Forfatter og Stiftelsen TISIP
© Copyright 2024