23 minute read

4.4 Arv

41 <ul> 42 {#each forfattere as forfatter} 43 <li>{forfatter.fornavn} {forfatter.etternavn} har skrevet:</li> 44 <ul>

45 46

{#each forfatter.verker as verk} <li>"{verk.tittel}"</li> 47 {/each} 48 </ul> 49 {/each} 50 </ul>

Når vi skal lage en ny klasse, hender det ofte at ideen vi har til løsning, ligner veldig på noe som finnes fra før av. Da kan vi bruke en teknikk innenfor objektorientert programmering som heter arv. Når en ny klasse arver en gammel klasse, betyr det at den nye klassen får med seg alt innholdet i den gamle klassen, i tillegg til alle nye metoder og felter som vi definerer i den nye klassen.

For at en ny klasse skal arve fra en eksisterende klasse, må vi bruke et nytt nøkkelord som heter extends. Det setter vi inn i linjen der vi oppretter klassen, slik:

class NyKlasse extends KlassenViArverFra.

LEGGE TIL METODER

Vi skal lage klassen HundSomKanEngelsk. Hunder som kan engelsk, oppfører seg akkurat som hunder som kan norsk, men de kan også presentere seg på engelsk. Takket være teknikken arv trenger vi bare skrive fem nye kodelinjer for å få en ny klasse som kan alt klassen Hund kan, men som også kan presentere seg på engelsk. Når vi

har skrevet class HundSomKanEngelskOgNorsk, følger vi opp med extends Hund, slik at den nye klassen arver de eksisterende metodene, inkludert konstruktøren.

1 // _hunder.js 2 export class Hund { 3 constructor(navn) { 4 this.navn = navn

5 } 6 7 formulerPresentasjon() { 8 return `Jeg heter ${this.navn}. Voff!`

9 } 10 } 11 12 export class HundSomKanEngelskOgNorsk extends Hund { 13 formulerPresentasjonPaaEngelsk() { 14 return `My name is ${this.navn}. Woof!`

15 } 16 }

Den største nyheten i denne kodesnutten er linjen som inneholder nøkkelordet extends. Som sagt betyr extends at den klassen vi holder på med å definere, skal arve klassen som kommer etter ordet extends. Den nye klassen skal altså få alle metodene og feltene som den gamle klassen inneholder. Det er som om vi hadde kopiert og limt inn kodelinjene fra den gamle klassen.

Fordi vi bruker extends, får HundSomKanEngelskOgNorsk med seg både konstruktøren og formulerPresentasjon fra Hund. Fordi vi skriver metoden formulerPresentasjonPaaEngelsk inni den nye klassens klassedefinisjon, kommer denne metoden i tillegg til alle metodene som den arver fra Hund. Nedenfor ser du en kodesnutt der vi oppretter én tospråklig hund og én hund som bare kan norsk, og hilser på begge.

1 <!-- hund-med-arv.svelte --> 2 <script> 3 import { Hund, HundSomKanEngelskOgNorsk } from "./_hunder.js"

4 5 let tospraakligHund = new HundSomKanEngelskOgNorsk("Rover") 6 let enspraakligHund = new Hund("Passop") 7 </script>

8 9 <h1>Hei, hund!</h1> 10 <div> 11 {tospraakligHund.formulerPresentasjon()} 12 </div> 13 <h2>Hello, dog!</h2> 14 <div> 15 {tospraakligHund.formulerPresentasjonPaaEngelsk()} 16 </div>

17 18 <h1>Hei, mindre verdensvante hund!</h1> 19 <div>{enspraakligHund.formulerPresentasjon()}</div>

Slektstreet til det norske kongehuset. Når én klasse arver en annen, sier vi at den er en subklasse av klassen den arvet. Derfor er HundSomKanEngelskOgNorsk en subklasse av Hund. Den klassen som en klasse arver fra, er den nye klassens superklasse. Dermed er Hund superklassen til

HundSomKanEngelskOgNorsk.

Subklasser og superklasser har fått navnet sitt etter måten vi plasserer folk i slektstrær på. Hvis vi ser på et slektstre, er de eldre familiemedlemmene plassert høyere oppe enn de yngre medlemmene, altså de som arver. Sub- og super- er latin for «under-» og «over-». Subklassene befinner seg altså i den nedre delen av slektstreet, mens superklassene er høyere oppe. Til forskjell fra mennesker har en subklasse bare én «forelder»: En subklasse kan bare arve fra én superklasse. En superklasse kan derimot ha flere subklasser.

De endringene vi gjør i subklassen, påvirker aldri superklassen. Vi kan altså ikke bruke metoden formulerPresentasjonPaaEngelsk på den enspråklige hunden Passop ettersom Passop er av klassen Hund, ikke av klassen HundSomKanEngelskOgNorsk. Endringene vi gjorde, gjorde vi i subklassen. Hadde vi prøvd å kalle på metoden, hadde programmet krasjet.

Maud

Ragnhild

Mette-Marit

Marius Olav V Haakon VII

Märtha

Harald V

Haakon

Ingrid Alexandra

Sverre Magnus Sonja

Märtha Louise Astrid

Ari Behn

Maud Angelica

Leah Isadora

Emma Tallulah

Arv, subklasser og superklasser

Hvis vi vil at klassen Ny skal arve klassen Gammel, skriver vi følgende når vi

oppretter klassen Ny: class Ny extends Gammel. Når klassen Ny arver klassen Gammel, sier vi at Ny er en subklasse av Gammel. Vi kan også si at Gammel er superklassen til Ny. Endringene vi gjør i subklassen, påvirker ikke superklassen.

REDEFINERE METODER

Det er ikke sikkert at subklassen skal kunne gjøre alt som superklassen kan. Sett at vi ønsker å opprette en klasse med hunder som bare kan snakke engelsk, og kaller

denne klassen for HundSomBareKanEngelsk. Metoden formulerPresentasjon skal sørge for at hundene i denne klassen presenterer seg på engelsk i stedet for norsk.

1 <!-- hund-med-redefinering.svelte --> 2 <script> 3 import { Hund } from "./_hunder.js"

4

5 class HundSomBareKanEngelsk extends Hund {

6

7 8 formulerPresentasjon() { return `My name is ${this.navn}. Woof!`

9 } 10 11 let rentEngelskHund = new HundSomBareKanEngelsk("Boris") 12 let rentNorskHund = new Hund("Passop") 13 </script>

14 15 <h1>Hello, pure-bred English dog!</h1> 16 <div> 17 {rentEngelskHund.formulerPresentasjon()} 18 </div>

19 20 <h1>Hei igjen, norske hund!</h1> 21 <div> 22 {rentNorskHund.formulerPresentasjon()} 23 </div>

Fordi formulerPresentasjon har samme navn som en metode i superklassen, erstatter den nye definisjonen den gamle for objekter av subklassen.

Som vi har nevnt tidligere, påvirker ikke de endringene vi gjør i en subklasse, superklassen, så resultatet av å bruke metoden formulerPresentasjon på en hund av den opprinnelige Hund-klassen vil fortsatt være en presentasjon på norsk. Det kan vi se når vi hilser på Passop i slutten av programmet.

En fordel med å redefinere metoder er at vi kan kalle på metodene på samme måte selv om de løser oppgavene sine forskjellig.

REDEFINERE KONSTRUKTØREN

Av og til kan det være nyttig å redefinere konstruktøren. Kanskje legger vi til flere felter, eller kanskje vi bare ønsker å sette de eksisterende feltene i en annen rekkefølge.

Vi kan illustrere dette ved hjelp av kontaktlisten vi jobbet med tidligere. I mange kulturer, for eksempel i Korea og Japan, er det slik at folk oppgir slektsnavnet før fornavnet når de sier hele navnet sitt. Vi kan lage en subklasse for folk fra kulturer der dette

er standarden, KontaktMedSlektsnavnFoerst.

1 // _kontakt.js 2 export class Kontakt { 3 constructor(fornavn, etternavn, epostadresse) { 4 this.fornavn = fornavn 5 this.etternavn = etternavn 6 this.epostadresse = epostadresse

7 } 8 9 settSammenFulltNavn() { 10 return `${this.fornavn} ${this.etternavn}`

11 } 12 13 lagVisittkort() { 14 return `${this.settSammenFulltNavn()}, e-post: ${this.epostadresse}`

15 } 16 } 17 18 export class KontaktMedSlektsnavnFoerst extends Kontakt { 19 constructor(slektsnavn, gittNavn, epostadresse) { 20 super(gittNavn, slektsnavn, epostadresse)

21 } 22 23 settSammenFulltNavn() { 24 return `${this.etternavn} ${this.fornavn}`

25 } 26 }

Som du kan se, redefinerer KontaktMedSlektsnavnFoerst metoden settSammenFulltNavn for at etternavnet skal komme før fornavnet. KontaktMedSlektsnavnFoerst redefinerer også konstruktøren, slik at vi kan skrive etternavnet før fornavnet når vi oppretter en ny kontakt av denne klassen.

Vi trenger ikke å endre spesielt mye i brukergrensesnittet. Det eneste vi må gjøre, er å importere den nye klassen i tillegg til den gamle og legge til noen objekter av klassen

KontaktMedSlektsnavnFoerst.

1 <!-- kontaktliste.svelte --> 2 <script> 3 import { Kontakt, KontaktMedSlektsnavnFoerst } from "./_kontakt.js"

4 5 let kontakter = [

6 7 8

9

10 ] 11 </script>

12 new Kontakt("Ole", "Olsen", "ole.olsen@example.no"), new Kontakt("Kari", "Nordmann", "kari.nordmann@example.no"), new KontaktMedSlektsnavnFoerst("Hong", "Gil-Dong", "gildong@example.kr"), new KontaktMedSlektsnavnFoerst("Yamada", "Taro", "yamada.taro@example.jp"),

13 <h1>Mine kontakter</h1> 14 <ul> 15 {#each kontakter as kontakt} 16 <li>{kontakt.lagVisittkort()}</li> 17 {/each} 18 </ul>

Når vi redefinerer konstruktøren, starter vi alltid med et kall til funksjonen super. Dette kallet betyr at vi kaller på superklassens konstruktør. Etter at vi har kalt på super, har vi muligheten til å legge til flere felter og endre verdier i feltene som superklassen opprettet. I konstruktøren til denne subklassen nøyer vi oss med bare å kalle på super.

Legg merke til at lagVisittkort automatisk velger riktig versjon av settSammenFulltNavn. Når vi bruker metoden lagVisittkort på et objekt av klassen KontaktMedSlektsnavnFoerst, kaller vi på den redefinerte metoden, og når vi bruker lagVisittkort på et objekt av klassen Kontakt, kaller vi på den opprinnelige metoden.

LEGGE TIL FELTER I DEFINISJONEN AV EN SUBKLASSE

Dersom vi skal utvide en subklasse med flere felter enn det superklassen har, blir vi nødt til å gjøre mer i konstruktøren enn bare å kalle på super.

Det tidligere nevnte vannautomatfirmaet skal lage en luksusvariant av vannautomaten. Hvis brukerne trykker på en egen knapp, skal de få vann med kullsyre. Automaten har en kullsyrepatron med et forhåndsbestemt antall doser.

Firmaet bestemmer seg for å legge til et felt med antall kullsyredoser og en ny metode som gjør det mulig å tappe vann med kullsyre. Ellers er automatene helt like. Nedenfor ser vi filen de har lagret den nye klassen i.

1 // _luksusautomat.js 2 import { Vannautomat } from "./_vannautomat.js"

3 4 export let DOSER_I_EN_NY_PATRON = 8

5 6 export class Luksusautomat extends Vannautomat { 7 constructor(kapasitetILiter) { 8 super(kapasitetILiter) 9 this.kullsyredoser = DOSER_I_EN_NY_PATRON

10 } 11 12 beskrivVanninnhold() { 13 return `${super.beskrivVanninnhold()} med ${ 14 this.kullsyredoser 15 } kullsyredoser igjen`

16 } 17 18 tappVannMedKullsyre(liter) { 19 this.kullsyredoser = this.kullsyredoser - 1 20 if (this.kullsyredoser < 0) {

21 this.kullsyredoser = 0

22 } 23 return this.tappVann(liter)

24 } 25 }

Filen importerer klassen Vannautomat fra filen der den opprinnelig ble lagret. Klassen er den samme som i delkapittel 4.3. Den lager en subklasse av denne automaten,

Luksusautomat.

Luksusautomat har to metoder som er redefinisjoner av metoder i superklassen. Den første av disse er en redefinert konstruktør. I likhet med alle redefinerte konstruktører kaller den først på superklassens konstruktør med super. Deretter oppretter den et felt som ikke finnes i superklassen, kullsyredoser, ved å skrive this.kullsyredoser og verdien som feltet skal ha.

Den andre metoden som Luksusautomat redefinerer, er beskrivVanninnhold. Vi ønsker at denne skal gjøre akkurat det samme som metoden den redefinerer, men også gi informasjon om antall kullsyredoser. For å slippe å skrive kode på nytt skriver vi super.beskrivVanninnhold() for å kalle på metoden slik den opprinnelig var definert, og legger deretter til informasjonen om kullsyredoser.

Kalle på en av superklassens metoder som subklassen har redefinert

Hvis subklassen skal kalle på en metode fra superklassen som den har

redefinert, kan vi skrive super.metodenavn(argumenter). Dette fungerer bare hvis vi er i klassedefinisjonen til en subklasse. Vi kan ikke bruke superklassens metode på et objekt av subklassen når metoden er redefinert i subklassen.

Luksusautomat definerer også en ny metode: tappVannMedKullsyre. Denne fjerner én kullsyredose fra patronen og kaller deretter på tappVann. Fordi vi ikke har redefinert tappVann, slipper vi å skrive super.tappVann() og kan skrive this. tappVann() som vanlig.

Nedenfor finner du en oppdatert versjon av testgrensesnittet.

1 <script> 2 import { Luksusautomat, DOSER_I_EN_NY_PATRON } from "./_luksusautomat.js"

3 4 let automat = new Luksusautomat(5) 5 let sisteVannmengde = 0 6 let standardVannmengde = 0.5 7 </script>

8 9 <h1>Vannautomat</h1>

10 11 <h2>Synlig for alle</h2> 12 <p>{automat.lagBeskjedTilSkjerm()}</p>

13 14 <p>Det kom ut {sisteVannmengde} liter sist</p>

15 16 <button 17 on:click={() => {

18 19 sisteVannmengde = automat.tappVann(standardVannmengde) automat = automat

20 }} 21 > 22 Tapp {standardVannmengde} liter 23 </button> 24 <button 25 on:click={() => {

26 27 sisteVannmengde = automat.tappVannMedKullsyre(standardVannmengde) automat = automat

28 }} 29 > 30 Tapp {standardVannmengde} liter vann med kullsyre 31 </button>

32 33 <h2>Synlig for teknikere</h2> 34 <button on:click={() => (automat.innhold = 0)}> Tøm tank </button> 35 <button on:click={() => (automat.innhold = automat.kapasitet)}> 36 Fyll tank 37 </button> 38 <button on:click={() => (automat.kullsyredoser = DOSER_I_EN_NY_PATRON)}> 39 Bytt kullsyrepatron 40 </button>

Det har også dukket opp to nye knapper i testgrensesnittet, «Tapp 0,5 liter vann med kullsyre» og «Bytt kullsyrepatron», som interagerer med de nye feltene og metodene. Ellers er grensesnittet det samme som tidligere.

Som vi så med redefinerte metoder i kontaktlisten, velger lagBeskjedTilSkjerm automatisk den redefinerte versjonen av beskrivVanninnhold når vi kaller på et objekt av klassen

Luksusautomat.

For firmaet som lager vannautomatene, er det én stor fordel med å bruke arv i stedet for å kopiere den eksisterende koden. De forbedringene og feilrettingene de gjør i Vannautomat, vil automatisk dukke opp i Luksusautomat også. Hvis de for eksempel legger til metoder for å registrere at tanken er tømt eller fylt på i Vannautomat, vil disse metodene også være tilgjengelige for klassen Luksusautomat.

LAGE EN SAMLEKLASSE SOM FLERE KLASSER SKAL ARVE FRA

La oss si at vi har fått i oppgave å lage to dyreklasser: Frosk og Ku. Begge klassene skal inneholde informasjon om dyrets art, lyden det lager, samt individets navn (navnet på den enkelte kua eller frosken). Dyrene skal kunne presentere seg selv og egenskapene sine når vi kaller på metoden formulerPresentasjon.

Begge klassene skal inneholde felter som motparten ikke skal ha. • Frosk skal inneholde feltet hoppehoeyde, som registrerer hvor høyt frosken kan hoppe. Siden kyr ikke kan hoppe, skal Ku ikke inneholde dette feltet. • Ku skal inneholde feltet pelsfarge, som inneholder informasjon om fargen på kuas pels. Siden frosker ikke har pels, skal Frosk ikke inneholde dette feltet.

Ved første øyekast kan det virke som om vi kan la Frosk arve Ku eller omvendt. Dessverre går ikke det, for det finnes ingen god måte å slette metoder og felter på når klasser arver. Derfor finnes det heller ingen logisk arveretning mellom Frosk og Ku. Uansett hvordan vi setter det opp, ender vi opp med at Frosk har feltet pelsfarge, eller at Ku har feltet hoppehoeyde.

Heldigvis finnes det en løsning. I situasjoner der vi har lyst til å bruke arv, men ender opp med uønskede felter eller metoder i subklassen, kan vi lage en kunstig samleklasse der vi legger inn det begge klassene trenger. Denne klassen finnes ikke nødvendigvis i oppgavebeskrivelsen, og den har heller ikke nødvendigvis noe motsvar i virkeligheten.

Nedenfor ser du hvordan vi har løst oppgaven. Vi har laget en klasse Dyr som vi lager to subklasser av: Ku og Frosk.

1 // _dyr.js 2 class Dyr { 3 constructor(individnavn, artsnavn, lyd) { 4 this.individnavn = individnavn 5 this.artsnavn = artsnavn 6 this.lyd = lyd

7 } 8 9 formulerPresentasjon() { 10 return `Jeg heter ${this.individnavn} og er en ${this.artsnavn}. ${this.lyd}!`

11 } 12 } 13 14 export class Frosk extends Dyr { 15 constructor(individnavn, hoppehoeyde) { 16 super(individnavn, "frosk", "Rrrbit") 17 this.hoppehoeyde = hoppehoeyde

18 } 19 formulerPresentasjon() { 20 return `${super.formulerPresentasjon()} Jeg kan hoppe ${ 21 this.hoppehoeyde 22 } meter høyt.`

23 } 24 } 25 26 export class Ku extends Dyr { 27 constructor(individnavn, pelsfarge) { 28 super(individnavn, "ku", "Mø") 29 this.pelsfarge = pelsfarge

30 } 31 32 formulerPresentasjon() { 33 return `${super.formulerPresentasjon()} Jeg har ${this.pelsfarge} pels.`

34 } 35 }

I stedet for at Frosk er en subklasse av Ku eller omvendt, er begge subklasser av en samleklasse Dyr. Nedenfor ser du et testgrensesnitt for disse klassene.

Her oppretter vi kua Dagros og frosken Franklin og lar dem presentere seg.

1 <!-- dyr-grensesnitt.svelte --> 2 <script> 3 import { Frosk, Ku } from "./_dyr.js"

4 5 let dyr = [

6 7 new Ku("Dagros", "hvit og brunflekkete"), new Frosk("Franklin", 1.5),

8 ] 9 </script>

10 11 {#each dyr as enkeltdyr} 12 <div> 13 {enkeltdyr.formulerPresentasjon()} 14 </div> 15 {/each}

Det gir ikke mening å opprette et objekt av klassen Dyr fordi vi ikke kan finne et standarddyr i den virkelige verden, altså et dyr som verken legger egg eller føder levende unger, og som verken bor i vann eller på land. For å være et virkelig dyr må objektet tilhøre en subklasse. Slik er det som oftest med disse samleklassene. Det gir sjelden mening å lage objekter av slike klasser, men som en felles «stamfar» fungerer de godt.

Hvis du skulle lage klassene Frosk og Ku og ikke kunne lage en samleklasse, hvilken av dem hadde du da satt som superklasse? Hva ville du ha gjort hvis Frosk fikk en metode for å legge egg eller Ku en metode for å tygge drøv?

DISKUTER

EKSTRASTOFF

Eksempel på en behagelig arverekkefølge: Vannautomat og Luksusautomat

Da vi laget klassene Vannautomat og Luksusautomat, sa det seg nesten selv at Vannautomat skulle være superklassen, og at Luksusautomat skulle være en subklasse av Vannautomat. Grunnen til dette var ikke at Vannautomat ble laget først og Luksusautomat etterpå, men at det er åpenbart at alle metoder og felter som finnes i Vannautomat, også må finnes i en identisk eller forbedret variant i Luksusautomat. Hvis vi hadde satt opp arven til å gå motsatt vei, altså at Vannautomat arvet Luksusautomat, måtte vi ha prøvd å slette metoden tappVannMedKullsyre og fjerne

feltet kullsyredoser.

Arverekkefølge og kunstige samleklasser

En subklasse bør være et spesialtilfelle av superklassen, slik at subklassen er superklassen med litt ekstra funksjonalitet. Hvis vi har behov for å slette metoder og felter når vi skriver definisjonen til en subklasse, er det et tegn på at arven går feil vei. Det går ikke an å slette arvede felter og metoder. Dersom vi ikke klarer å organisere arven mellom to eller flere klasser uten å ende opp med uønskede felter og metoder i én eller flere subklasser, er det ofte lurt å lage en kunstig samleklasse som inneholder det alle klassene har til felles, slik at de kan arve fra den.

Som regel bør vi ikke opprette objekter av samleklassen fordi den ikke representerer noe som finnes i virkeligheten.

EKSTRASTOFF

Kunstige samleklasser kalles for abstrakte klasser

På fagspråket kaller vi kunstige samleklasser som vi ikke skal opprette objekter av (bare arve fra), for abstrakte klasser. I noen programmeringsspråk er det mulig å si fra om at en klasse er en abstrakt klasse, slik at programmet krasjer hvis vi prøver å opprette et objekt av klassen. I programmeringsspråket Java (uten «Script» på slutten) sier vi fra ved å skrive abstract class i stedet for bare class. Denne muligheten har vi ikke i JavaScript. Teknikken med å trekke ut en samleklasse er nyttig å kunne, også når programmeringsspråket ikke gir oss muligheten til å si fra om at det ikke skal opprettes et objekt av klassen. Det viktigste med disse klassene er ikke å huske at de kalles for abstrakte klasser, men å forstå teknikken og formålet med dem..

EKSEMPEL

Objektorientert bilkollektiv med en samleklasse

I et bilkollektiv får medlemmene tilgang til å leie ulike biler. Bilkollektivene har ofte apper der medlemmene kan se hvilke biler som er tilgjengelige. Og for hver enkelt bil kan de se hvor fullt bilens energilager er, det vil si hvor mye bensin, diesel eller strøm den har igjen på tanken. Et bilkollektiv har leid oss inn for å lage datasystemet deres. Systemet skal holde styr på kollektivets fossilbiler og elbiler og på hvor fullt energilageret, altså drivstofftanken eller batteriet, for hver enkelt bil er til enhver tid. Fordi de to biltypene er ulike på mange måter, må vi opprette to klasser:

Elbil og Fossilbil. Elbiler og fossilbiler har mye til felles, men siden ingen av dem er et spesialtilfelle av den andre, lager vi en samleklasse Bil som begge skal arve fra. Når medlemmene bruker en bil, betaler de per minutt og per kilometer de har kjørt, uavhengig av om bilen er en fossilbil eller en elbil. Samleklassen skal derfor ha en metode som kan regne ut prisen for en tur basert på hvor langt medlemmet har kjørt, og hvor lenge leietiden varte. Metoden skal også regne ut hvor mye det er igjen i energilageret etterpå. Alle biler har også et registreringsnummer. Fordi både klassen Elbil og Fossilbil bør ha felter som heter registreringsnummer, timepris og kilometerpris, får samleklassen Bil ansvar for disse feltene. Metoden for å regne ut prisen for en tur kan også legges i Bil fordi den bare avhenger av felter som finnes i Bil. Nedenfor ser du den ferdige koden for Bil.

1 // _bil.js 2 export class Bil { 3 constructor(registreringsnummer, kilometerpris, timepris) { 4 this.registreringsnummer = registreringsnummer 5 this.kilometerpris = kilometerpris 6 this.timepris = timepris

7 } 8 9 /** 10 * Regner ut prisen for en tur og oppdaterer 11 * hvor mye det er i bilens energilager etter 12 * turen.

13 * 14 * @param kilometer turlengden i kilometer 15 * @param timer leievarigheten i timer 16 * @returns prisen i kroner for turen

17 */ 18 utfoerTur(kilometer, timer) { 19 this.reduserEnergilager(kilometer) 20 return this.regnUtTurpris(kilometer, timer)

21 } 22 23 regnUtTurpris(kilometer, timer) { 24 return kilometer * this.kilometerpris + timer * this.timepris

25 } 26 }

Her har vi to metoder i tillegg til konstruktøren: utfoerTur og regnUtTurpris. regnUtTurpris er ganske enkel. Den ganger antall kilometer et medlem har kjørt, med kilometerprisen og legger produktet sammen med produktet av antall kjørte timer og timeprisen. Deretter returnerer den svaret.

utfoerTur er litt mer spennende. Den kaller på metoden reduserEnergilager, som ikke finnes i klassen Bil. Med menneskespråk sier Bil: «Her vet jeg at noe skal skje. Hvordan det skal skje, har jeg ikke kunnskap nok til å vite, så jeg overlater til subklassene å definere det.» Bil vet jo ikke hva slags energilager subklassene har, eller hvordan disse påvirkes av at et av medlemmene

kjører et visst antall kilometer. Da er det best å ikke anta noe. Som vi snart skal se, definerer Elbil og Fossilbil hver sin versjon av reduserEnergilager. Etter at programmet har kalt på reduserEnergilager, returneres prisen for den utførte turen. Nedenfor ser du koden for klassen Elbil.

1 // _elbil.js 2 import { Bil } from "./_bil.js"

3 4 export class Elbil extends Bil { 5 constructor(bildata) { 6 super(bildata.registreringsnummer, bildata.timepris, bildata.kilometerpris)

7 8 this.batterikapasitetIWattTimer = bildata.batterikapasitetIWattTimer 9 this.wattTimerPerKilometer = bildata.wattTimerPerKilometer 10 this.energinivaaIWattTimer = 0

11 } 12 13 ladFullt() { 14 this.energinivaaIWattTimer = this.batterikapasitetIWattTimer

15 } 16 17 reduserEnergilager(kilometer) { 18 let stroemBruktPaaTuren = this.regnUtStroemforbruk(kilometer) 19 this.reduserLadning(stroemBruktPaaTuren)

20 } 21 22 regnUtStroemforbruk(kilometer) { 23 return this.wattTimerPerKilometer * kilometer

24 } 25 26 reduserLadning(wattTimer) { 27 this.energinivaaIWattTimer = this.energinivaaIWattTimer - wattTimer

28 } 29 }

Vi har brukt et triks i konstruktøren til Elbil. Konstruktøren har én parameter, som skal være et vanlig JavaScript-objekt med dataene oppgitt i visse felter. Det er fordi konstruktøren trenger fem parametere, og da er det lett å gå surr i rekkefølgen, både når vi skriver og leser koden. I følgende kodesnutt ser du en demonstrasjon av hvordan det ser ut når programmet kaller på konstruktøren:

1 // Kortfattet, men vanskelig å se om tallene står på rett plass 2 let bilenMin = new Elbil("EV73889", 1.9, 58, 58000, 173)

3 4 // Mange bokstaver, men nå trenger vi ikke å huske rekkefølgen, bare rett navn 5 let bilenMin = new Elbil({ 6 registreringsnummer: "EV73889", 7 kilometerpris: 1.9, 8 timepris: 58, 9 batterikapasitetIWattTimer: 58000, 10 wattTimerPerKilometer: 173,

11 })

Tommelfingerregelen er at vi bruker dette objekttrikset når vi har flere enn tre–fire parametere. Konstruktøren til Elbil forventer at objektet den tar inn, skal inneholde to biter informasjon utenom de tre den sender til superklassens konstruktør: batterikapasitetIWattTimer og wattTimerPerKilometer. batterikapasitetIWattTimer oppgir størrelsen på batteriet i wattimer (batterikapasiteten til elbiler er ofte oppgitt i kilowattimer, kWh; 1 kilowattime = 1000 wattimer). wattTimerPerKilometer er enheten for batteriforbruk i elbiler, altså hvor mye energi som forsvinner fra batteriet per kilometer. →

Elbil-konstruktøren begynner med å kalle på super med dataene denne forventer, i likhet med alle redefinerte konstruktører. Deretter setter Elbil-konstruktøren sine egne felter til de verdiene som finnes i objektet. Feltet energinivaaIWattTimer lagrer hvor mye strøm det er i batteriet. Dette settes til 0 når vi oppretter Elbil, for bilprodusentene lader ikke bilene før de leverer dem. Elbil inneholder en metode for å lade batteriet til full kapasitet: ladFullt. Elbil definerer sin egen versjon av reduserEnergilager. For at denne metoden skal gjøre jobben sin, bruker den to hjelpemetoder som er definert lenger nede i funksjonen: regnUtStroemforbruk, som regner ut strømforbruket for turen, og reduserLadning, som reduserer verdien for energimengden i batteriet med det utregnede forbruket. Nedenfor ser du et testgrensesnitt som programmererne som jobber med Elbil-klassen, har laget for å teste klassen. Her simulerer de hvordan bilens batteri påvirkes av at noen lader det og kjører en tur. Underveis tester de også at kostnadene for å kjøre bilen er som forventet.

1 <!-- elbil-testgrensesnitt.svelte --> 2 <script> 3 import { Elbil } from "./_elbil.js"

4 5 let bilenMin = new Elbil({

6 7 8 9 10 11 }) 12 registreringsnummer: "EV73889", kilometerpris: 1.9, timepris: 58, batterikapasitetIWattTimer: 58000, wattTimerPerKilometer: 173,

13 let ladningFraFabrikk = bilenMin.energinivaaIWattTimer 14 bilenMin.ladFullt() 15 let ladningEtterFulladning = bilenMin. energinivaaIWattTimer

16 17 let kilometer = 30 18 let timer = 2 19 let kostnadForTur = bilenMin.utfoerTur(kilometer, timer) 20 </script>

21 22 <h1>Bilen</h1> 23 <p> 24 Vi har en bil med registreringsnummer 25 {bilenMin.registreringsnummer}. Hvis du vil kjøre den, koster det 26 {bilenMin.kilometerpris} kroner per kilometer og 27 {bilenMin.timepris} kroner per time. 28 </p> 29 <p> 30 Da vi fikk bilen, hadde den 31 {ladningFraFabrikk} Wh i batteriet. Etter at vi hadde ladet den, hadde den 32 {ladningEtterFulladning} Wh i batteriet. 33 </p> 34 <p> 35 Så kjørte vi en tur som tok 36 {timer} timer og var 37 {kilometer} kilometer lang. Det kostet 38 {kostnadForTur} kroner. Etter turen var ladningen 39 {bilenMin.energinivaaIWattTimer} Wh. 40 </p>

Nedenfor kan du se hvordan Fossilbil-klassen er kodet. Som du kan se, inneholder den felter og metoder som tilsvarer metodene i Elbil. Den har også et ekstra felt, drivstofftype, som sier om tanken skal fylles med bensin eller diesel.

1 // _fossilbil.js 2 import { Bil } from "./_bil.js"

3 4 class Fossilbil extends Bil { 5 constructor(bildata) { 6 super(bildata.registreringsnummer, bildata.timepris, bildata.kilometerpris) 7 this.bensintankkapasitet = bildata.bensintankkapasitet 8 this.literDrivstoffPerKilometer = bildata.literDrivstoffPerKilometer 9 this.drivstofftype = bildata.drivstofftype 10 this.drivstoffmengde = 0

11 } 12 13 fyllTankenFull() { 14 this.drivstoffmengde = this.bensintankkapasitet

15 } 16 17 reduserEnergilager(kilometer) { 18 this.reduserDrivstoff(this.regnUtDrivstofforbruk(kilometer))

19 } 20 21 regnUtDrivstofforbruk(kilometer) { 22 return this.literDrivstoffPerKilometer * kilometer

23 } 24 25 reduserDrivstoff(kilojouleUtladning) { 26 this.drivstoffmengde = kilojouleUtladning

27 } 28 }

Vi overlater oppgaven med å lage et testgrensesnitt for Fossilbiler til leserne (oppgave x.x)

Bruke et vanlig JavaScript-objekt som parameter i stedet for flere parametere

Når vi skriver og leser kode med flere enn tre–fire parametere, begynner det å bli vanskelig å passe på at vi har satt inn verdiene i riktig rekkefølge. I slike tilfeller er det lurt å bruke et vanlig JavaScript-objekt som parameter heller enn flere parametere.

1 // Kortfattet, men vanskelig å se om tallene står på rett plass 2 let bilenMin = new Elbil("EV73889", 1.9, 58, 58000, 173)

3 4 // Mange bokstaver, men nå trenger vi ikke å huske rekkefølgen, bare rett navn 5 let bilenMin = new Elbil({ 6 registreringsnummer: "EV73889", 7 kilometerpris: 1.9, 8 timepris: 58, 9 batterikapasitetIWattTimer: 58000, 10 wattTimerPerKilometer: 173,

11 })

Dokumentasjonsstrenger

Kommentarer som starter med to asterisker, altså /** … */, kaller vi dokumentasjonsstrenger (eller bare dokstrenger). Slike kommentarer forklarer hva metoden eller variabelen som kommer etter kommentaren, gjør, slik at den som gjenbruker den, slipper å lese kildekoden for å forstå hva den gjør. Mange moderne kodeverktøy, deriblant Visual Studio Code, viser dokumentasjonsstrengen dersom vi hviler musepekeren over et element.

Det er ikke nødvendig å skrive dokumentasjonsstrenger, men det er til stor hjelp for andre som skal gjenbruke koden.

This article is from: