Kode 2 Informasjonsteknologi 2 (LK20) utdrag

Page 1



kode 2 Hei! Du leser nå et utdrag av Kode 2 (2022). Utdraget inneholder innholdsfortegnelsen til boka uten sidetall og teoridelen av kapittel 2, 4 og 6. Sidene som vises her, er ikke ferdig korrekturleste, så det vil bli gjort justeringer før boka trykkes. Vi håper likevel dette gir et godt grunnlag for å vurdere innholdet og at du liker det du ser! Ta gjerne kontakt med oss på kode@cappelendamm.no om du har spørsmål. Med vennlig hilsen forfatterne og redaksjonen


© CAPPELEN DAMM AS, Oslo 2022 ISBN: 978-82-02-73999-7 2. utgave 1. opplag 2022

Tekniske tegninger: Terje Sundby/Keops Omslagsdesign: Anders Bergesen / Superultraplus Designstudio Grafisk formgiving: Kristine Steen / 07 Media Fotografier: Getty Images og Unsplash Forlagsredaktør: Henning Vinjusveen Myhrehagen Sats: Jannicke Pedersen / Type-it AS Trykk og innbinding: Livonia Print SiA, Latvia 2022

Kode 2 følger læreplan (LK20) i informasjonsteknologi 2 programfag i utdanningsprogram for studiespesialisering fra 2020. Materialet i denne publikasjonen er omfattet av åndsverklovens bestemmelser. Uten særskilt avtale med Cappelen Damm AS er enhver eksemplarfremstilling og tilgjengeliggjøring bare tillatt i den utstrekning det er hjemlet i lov eller tillatt gjennom avtale med Kopinor, interesseorgan for rettighetshavere til åndsverk. Utnyttelse i strid med lov eller avtale kan medføre erstatningsansvar og inndragning, og kan straffes med bøter eller fengsel.

www.cdu.no www.kode.cdu.no


INNHOLD 1 Programmering med Svelte

4 Objektorientert programmering

1.1 Programmering og Svelte

4.1 Klasser og objekter

1.2 Statiske nettsider

4.2 Metoder

1.3 Tall

4.3 Bruksområder for klasser og metoder

1.4 Strenger

4.4 Arv

1.5 Funksjoner

4.5 Objektorientert modellering

1.6 Valgsetninger

Sammendrag og oppgaver

1.7 Arrayer og løkker 1.8 Objekter

5 Lagre og visualisere data

Sammendrag og oppgaver

5.1 Lagring 5.2 JSON

2 Gjenbruk og testing

5.3 Datavisualisering

2.1 Gjenbruke variabler og funksjoner

5.4 Sikring av data

2.2 Lage og gjenbruke komponenter

Sammendrag og oppgaver

2.3 Lage interaktive komponenter 2.4 Teste et helt dataprogram

6 Jobbe med IT-prosjekter

2.5 Feilsøking

6.1 Produkter og prosjekter

2.6 Enhetstester

6.2 Utviklingsmetoder

2.7 Vanlige problemer når du tester med tall

6.3 Scrum

Sammendrag og oppgaver

6.4 Oppgavetavler 6.5 Versjoner og versjonskontroll

3 Brukervennlighet 3.1 Hva er brukervennlighet? 3.2 Hvem lager gode brukeropplevelser? 3.3 Hvordan lage en god brukeropplevelse? 3.4 Mål for brukskvalitet 3.5 Ekspertevaluering Sammendrag og oppgaver

Sammendrag og oppgaver


2 Gjenbruk og testing KOMPETANSEMÅL I dette kapittelet skal du jobbe for å nå disse kompetansemålene: в generalisere løsninger ved å utvikle og bruke gjenbrukbar programkode в vurdere og bruke strategier for feilsøking og testing av programkode

Når vi gjenbruker en kodesnutt innenfor programmering, for eksempel en funksjon eller en variabel, skriver vi koden i én fil og henter den i én eller flere andre filer.

Varer som fraktes til Norge, blir eksportert fra et land. Da sier vi at Norge har importert varene.

Én fordel med å gjenbruke kode i flere filer er at vi slipper å gjenta oss selv. Dersom vi har behov for den samme funksjonen eller variabelen ulike steder i et program, trenger vi bare å skrive den ett sted. Dersom vi må endre koden, er det en fordel å bare endre den ett sted. Når vi jobber med et program med mange kodelinjer, blir programmet fort uoversiktlig dersom alt ligger i én fil. Ved å dele programmet opp i mindre filer blir koden mer oversiktlig og det blir lettere å skrive, lese og endre koden. Før du går gjennom dette kapittelet, bør du sette deg inn i hvordan filsystemet i en datamaskin fungerer og hvordan du setter opp et Svelte-prosjekt på en datamaskin. Du finner ressurser om dette på elevnettstedet.


Gjenbruk og testing

2.1 Gjenbruke variabler og funksjoner Vi har skrevet inn filnavnet som en kommentar i de fleste kodeboksene i dette kapittelet, for eksempel <!-- trapes.svelte -->.

EKSPORTERE EN FUNKSJON I mappen src/routes i et Svelte Kit-prosjekt har vi to Svelte-filer: trapes.svelte og addering.svelte. Koden i filene ser slik ut: 1 <!-- trapes.svelte --> 2 <script> 3

let leggSammen = (a, b) => {

4 return a + b 5 } 6 7

let regnUtArealAvTrapes = (topplengde, bunnlengde, hoeyde) => {

8 let sum = leggSammen(topplengde, bunnlengde) 9 return (sum * hoeyde) / 2 10

}

11 12

let a = 1 let b = 2 14 let c = 1 15 </script> 13

16 17 <label>Bunnlinje: <input type="number" bind:value={a} /></label> 18 <label>Topplinje: <input type="number" bind:value={b} /></label> 19 <label>Høyde: <input type="number" bind:value={c} /></label> 20 21 <div> 22 Arealet av trapeset er: {regnUtArealAvTrapes(a, b, c)} 23 </div>

1 <!-- addering.svelte --> 2 <script> 3 4

let a = 1 let b = 2

5 6

let leggSammen = (x, y) => {

7 return x + y 8 } 9 </script> 10 11 <input type="number" bind:value={a} /> + <input type="number" bind:value={b} /> 12 = {leggSammen(a, b)}

7


8

Kapittel 2

I begge filene har vi en funksjon som legger sammen to tall: leggSammen. For at vi skal slippe å skrive den samme funksjonen i begge filene, kan vi legge funksjonen inn i en JavaScript-fil som begge Svelte-filene skal importere funksjonen fra. Vi oppretter filen matematikk.js og lagrer den i den samme mappen som de to Svelte-filene. Vi limer inn funksjonen leggSammen. For at det skal være mulig å importere funksjonen, må vi skrive export foran definisjonen av funksjonen. 1 // matematikk.js 2 3 /* 4 * Fordi vi har skrevet "export" foran funksjonen, 5 * kan vi importere den til andre filer. 6

*/

7 export let leggSammen = (x, y) => { 8 return x + y 9 }

Pass på at filen slutter på .js når du lagrer den, for dette signaliserer til datamaskinen at filen er en ren JavaScript-fil.

Datamaskinen forstår filer som slutter på .js og .svelte forskjellig Datamaskinen bruker bokstavene som står etter punktumet i et filnavn, til å forstå hvordan den skal tolke innholdet i filen. Filer som slutter på .js, inneholder bare kode, mens filer som slutter på .svelte, er nettsider. Hvis vi vil gjenbruke funksjoner og variabler, må de ligge i en fil som slutter på .js.

For å importere funksjonen til en fil skriver vi import { leggSammen } from "./mappen/filen.js". Fordi vi har lagt matematikk.js i samme mappe som filene den skal importeres til, skriver vi import { leggSammen } from "./­matematikk. js" i begge Svelte-filene, altså uten noen mappenavn før filnavnet. Vi sletter også den eksisterende leggSammen-funksjonen fra begge filene. Da ser de første linjene i Svelte-filene slik ut, mens resten av innholdet er uendret: 1 <!-- trapes.svelte --> 2 <script> 3 4

import { leggSammen } from "./matematikk.js" // Slett de neste linjene der vi definerer "leggSammen", og la resten stå


Gjenbruk og testing

1 <!-- addering.svelte --> 2 <script> 3 4

import { leggSammen } from "./matematikk.js" // Slett de neste linjene der vi definerer "leggSammen", og la resten stå

Når du åpner nettsidene igjen, skal de fungere på samme måte som før. Forskjellen er at nå importerer begge filene funksjonen leggSammen fra en felles fil.

Nøkkelordene export og import Hvis vi skriver export foran en funksjon eller en variabel i en JavaScript-fil, , for eksempel export let enVariabel = 1 eller export let enFunksjon = () => { … } kan vi hente denne funksjonen eller variabelen inn i andre filer ved hjelp av import Når vi importerer en variabel eller en funksjon, oppfører koden for disse seg som om de var definert inni filen som vi importerer det til. Det er ingen begrensning på hvor mange filer som kan importere variabelen eller funksjonen når de først er eksportert.

EKSEMPEL Eksportere en variabel Det eneste du trenger å gjøre for å eksportere en variabel, er å skrive export foran den. Når vi jobber med matematikk, kan det for eksempel være nyttig å ha tilgang til hvor mange grader det er i en sirkel, og hva verdien av π er. 1 // matematikk.js 2 export let leggSammen = (x, y) => { 3 return x + y 4 } 5 6 // Disse to linjene er nye 7 export let PI = 3.14 8 export let GRADER_I_SIRKEL = 360

Når vi eksporterer variabler fra en fil, pleier vi som regel å skrive navnene med store bokstaver og med understrekings­tegn mellom ordene. Det signaliserer at verdien er konstant, altså at den ikke bør endres (selv om det er teknisk mulig).

1 <!-- sirkel.svelte --> 2 <script> 3

import { PI, GRADER_I_SIRKEL } from "./matematikk.js"

4 5

let radius = 1

6 </script> 7 8 <h1>Sirkel</h1> 9 <label>Radius: <input type="number"

bind:value={radius} /></label> 10 <div> 11 Areal: {PI * radius * radius}. Sirkelen har

{GRADER_I_SIRKEL} grader (som alle sirkler). 12 13 </div>

9


10

Kapittel 2

EKSEMPEL Gi nytt navn til importert kode Av og til kan det være greit å endre navnet på det vi importerer. Det kan for eksempel hende at det vi importerer, har samme navn som noe annet i filen, eller at det blir lettere å lese koden hvis vi velger et annet navn. For å endre navnet til det vi importerer, skriver vi nøkkelordet as etterfulgt av det nye navnet: import { PI as PI_MED_TO_DESIMALER } from "./matematikk.js"

STANDARDEKSPORT Hvis en fil har et tydelig hovedelement, kan vi definere dette elementet som filens standardeksport. Vi begynner med å se hvordan dette gjøres i praksis.

y

f(x) = ax2 + bx + c x= x1

abc-regelen fra matematikken gir oss løsningene til en andregradsligning. Vi kan også si at vi finner nullpunktene til en andregradsfunksjon.

–b ± √ b2 – 4ac 2a

x

x2

Hvis vi lager en funksjon som løser andregradsligninger, og lagrer den i filen andregradsligninger.js, har funksjonen en tydelig hovedrolle. Derfor lar vi dette være standardeksporten. For å gjøre funksjonen til filens standardeksport skriver vi export default loesAndregradsligning etter at vi har definert den.

En andregradsligning på formen ax 2 bx c 0 har løsningene x

b b2 4 ac 2a

Ligningen har to løsninger dersom b2 4 ac 0 og én løsning dersom b2 4 ac 0 . 1 // andregradsligninger.js 2 export let harKvadratrot = (tall) => { 3 return tall >= 0 4 } 5 6 let loesAndregradsligning = (a, b, c) => { 7 let d = b * b - 4 * a * c 8 9 10 11 12 13 14 15 16 17 18 19 20

let harIngenReellLoesning = !harKvadratrot(d) if (harIngenReellLoesning) { return null } let kvadratrot = Math.sqrt(d) let loesning1 = (-b + kvadratrot) / (2 * a) let loesning2 = (-b - kvadratrot) / (2 * a) let harKunEnLoesning = loesning1 === loesning2 if (harKunEnLoesning) { return [loesning1] } return [loesning1, loesning2] }

21 22 export default loesAndregradsligning


Gjenbruk og testing

Når vi skal importere en standardeksport, skriver vi koden for dette før eventuelle andre importer og uten krøllparenteser. Når vi importerer en standardeksport, står vi fritt til å velge hva funksjonen skal hete i filen vi importerer den til. Koden nedenfor er et program der brukeren kan angi verdier for koeffisientene a, b og c i input-felt. Programmet importerer funksjonen loesAndregradsligning og bruker den til å finne og viser frem eventuelle løsninger av ligningen. Som du ser i koden nedenfor, velger vi å kalle funksjonen loes når vi importerer den, slik at vi sparer litt plass. 1 <!-- loes-andregradsligning.svelte --> 2 <script> 3

import loes, { harKvadratrot } from "./andregradsligninger.js"

4 5 6 7

let a = 0 let b = 0 let c = 0

8 9

$: loesning = loes(a, b, c)

10 </script> 11 12 <h1>Løs andregradsligninger</h1> 13

Slik vises andregradskalkulatoren.

14 <p>Omform andregradsligningen til du har den på formen: </p> 15 16 <div style="margin: 1rem"> 17 ax<sup>2</sup> + bx + c = 0 18 </div> 19 20 <label>a: <input type="number" bind:value={a} /></label> 21 <label>b: <input type="number" bind:value={b} /></label> 22 <label>c: <input type="number" bind:value={c} /></label> 23 24 <div> 25 {#if loesning === null} 26 Ingen reell løsning 27

{:else}

28 Løsninger: {loesning} 29 {/if} 30 </div> 31 32 <h2>Har kvadratrøtter</h2> 33 <ul> 34

<li>{a} har kvadratrot: {harKvadratrot(a)}</li> <li>{b} har kvadratrot: {harKvadratrot(b)}</li> 36 <li>{c} har kvadratrot: {harKvadratrot(c)}</li> 37 </ul> 35

38 39 <style> 40 input { 41 width: 5rem; 42 } 43 </style>

Hvis du vil bytte navn når du importerer en vanlig eksport, det vil si en navngitt eksport (named export på engelsk), må du nevne elementets opprinnelige navn: import { opprinneligNavn as nyttNavn } from …

11


12

Kapittel 2

Med en standardeksport kan du velge navn selv. Her står du fritt til å velge import opprinneligNavn from … eller import nyttNavn from …, uten å vise til det opprinnelige navnet. Standardeksporter er litt kontroversielle. Fordi vi står fritt til å velge hva de skal hete når vi importerer dem, har de ikke har et fast navn. Da kan det være vanskelig å søke dem opp i kodebasen. Hvis du vil finne alle filer der en navngitt eksport er importert, kan du søke gjennom kodebasen etter opprinneligNavn. Da finner du garantert alle filene som inneholder eksporten. Vi har ikke den samme garantien med standard­ eksporter. Hvis kodebasen inneholder mange standardeksporter, kan den bli uoversiktlig. Derfor anbefaler vi at du bare bruker standardeksporter når du har en god grunn til det, og at du ellers bruker navngitte eksporter.

Definere og importere en standardeksport Hvis en funksjon eller en variabel har en tydelig hovedrolle i en fil, kan vi gjøre dette til filens standardeksport (default export på engelsk). Når vi skal definere en standardeksport, må vi først definere funksjonen eller variabelen uten å eksportere den: let etEllerAnnet = … Senere i samme fil skriver vi en linje der det står export default etEllerAnnet. Da er etEllerAnnet blitt filens standardeksport. Når vi skal importere en standardeksport, setter vi ikke krøllparenteser rundt navnet når vi importerer den. Dersom standardeksporten ligger i en fil som heter foo.js, og denne filen ligger i den samme mappen som filen vi importerer til, skriver vi: import etEllerAnnet from "./foo.js". Når vi importerer en standardeksport, kan vi velge et annet navn på eksporten enn den opprinnelig hadde da den ble eksportert. Altså kunne vi også ha skrevet import nyttNavn from "./foo.js". Det regnes imidlertid som dårlig skikk å bytte navn uten en god grunn. En god grunn kunne for eksempel være at det eksporterte navnet krasjer med navnet på noe i filen det importeres til.

Selv om du ikke planlegger å bruke standardeksporter i koden du skriver selv, forekommer slike eksporter ofte i kode andre har skrevet. Derfor bør du kjenne til hvordan standardeksporter fungerer. Som vi snart skal se, spiller standardeksporter en viktig rolle når vi gjenbruker Svelte-filer.


Gjenbruk og testing

2.2 Lage og gjenbruke komponenter

Når vi skriver inn en adresse i nettleserens adressefelt, for eksempel nrk.no eller youtube.com, får vi opp en nettside som inneholder masse tekst og flere videoer og bilder. Skulle vi ha kodet en av disse sidene selv, hadde sluttresultatet sikkert inneholdt tusenvis av linjer dersom vi skrev nettsiden i én og samme fil. Når vi lager en stor nettside, har vi så å si aldri bare én fil på datamaskinen. Nesten alle nettsider er bygd opp av flere, gjenbrukbare biter som kalles komponenter. Komponentene settes til slutt sammen til en nettside.

Komponenter En komponent er en liten bit av en nettside som brukes til å bygge opp en større nettside. En komponent kan brukes i flere filer. Den kan også brukes gjentatte ganger på samme side.

Navigasjonslinjen øverst er synlig på nesten alle nettsidene under nrk.no.

En av de mest åpenbare fordelene med komponenter er at det er lettere å redigere mange små filer enn én gigantisk fil. En annen fordel minner om fordelene vi får ved å dele JavaScript-kode opp i mindre biter: Vi kan gjenbruke en komponent på flere nettsider. De fleste nettsider har for eksempel en navigasjonslinje som går igjen på hele nettstedet. Da er det naturlig å gjøre koden for navigasjonslinjen til en komponent. Ved å trekke ut navigasjonslinjen som en komponent, kan vi importere den til flere ulike nettsider. Ved å gjenbruke en komponent flere ganger på den samme nettsiden slipper vi å repetere kode. Nettsider inneholder mange elementer som stort sett er like, bortsett fra noen små detaljer. I skjerm­dumpen fra youtube.com nedenfor er for eksempel hver av videolenkene veldig like utenom teksten og bildeinnholdet. Her har programmererne hos YouTube sannsynligvis laget en komponent som henter inn videonavnet , thumbnailbildet og litt annen informasjon. Komponenten i koden på neste side heter ­Videolenke. For å bruke komponenter, skriver vi navnet på komponenten som en tagg. Inni taggen kan vi også ha egenskaper som ligner på attributtene til HTML-elementer.

13


14

Kapittel 2

Koden skisserer hvordan YouTube kan ha brukt komponenter med egenskaper. Koden for komponenten er lagret i en annen fil.

1 2 3 4 5 6 7 8 9 10 11

<div> <Videolenke tittel="The Glitch That Kept Sending The FBI To A Tiny Kansas Farm" url=" https://www.youtube.com/watch?v=vh6zanS_epw " /> <Videolenke tittel="Ten years of coding in 13 minutes" url=" https://www.youtube.com/watch?v=1fPWr0d5zBE" /> <!-- ... og så videre --> </div>

IMPORTERE EN SVELTE-FIL SOM EN KOMPONENT La oss si at vi har begynt å lage vår egen hjemmeside. På hjemmesiden har vi tenkt å ha tre nettsider som folk kan navigere til: index.svelte, filmer.svelte, og om-meg.svelte. Vi starter med et tomt Svelte Kit-prosjekt. I mappen src/routes oppretter vi følg­ ende filer:

• • •

index.svelte, som dukker opp hvis folk ikke skriver noen ting etter nettadressen filmer.svelte, som dukker opp hvis folk skriver «/filmer» etter nettadressen om-meg.svelte, som dukker opp hvis folk skriver «/om-meg» etter nettadressen

Når vi oppretter prosjektet, ser filene slik ut: Slik ser src/routes ut når vi starter.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

1 2 3 4 5 6 7 8

<!-- index.svelte --> <nav> <!- nav-elementet endrer ikke utseendet, men sier til nettleseren at innholdet brukes til navigasjon. Dette er nyttig for folk som bruker skjermlesere. --> <ul> <!- verdien i href viser til nettadressen som filene blir gjort om til, ikke hvor siden er plassert i filsystemet. --> <li><a href="/">Hjem</a></li> <li><a href="/filmer">Filmer</a></li> <li><a href="/om-meg">Om meg</a></li> </ul> </nav> <h1>Velkommen!</h1> <p>Dette er hjemmesiden min.</p> <p>Mer innhold kommer.</p> <!-- filmer.svelte --> <h1>Mine favorittfilmer</h1> <ol> <li><a href="https://www.imdb.com/title/tt0068646">Gudfaren</a></li> <li><a href="https://www.imdb.com/title/tt0050083">12 edsvorne menn</a></li> <li><a href="https://www.imdb.com/title/tt1392190">Mad Max: Fury Road</a></li> </ol>


Gjenbruk og testing

1 <!-- om-meg.svelte --> 2 <h1>Om meg</h1> 3 4 <p>Jeg er en elev på videregående som for øyeblikket velger å være anonym.</p>

Vi har lyst til å legge inn den samme navigasjonsmenyen som i index.svelte på toppen av alle sidene, slik at de som besøker hjemmesiden, lettere kan hoppe mellom dem. Vi kunne ha kopiert <nav>...</nav>-delen av index.svelte og limt linjene inn i filmer.svelte og om-meg.svelte, men da hadde det blitt tungvint å gjøre endringer senere. Hvis vi for eksempel la til en linje, måtte vi ha kopiert og limt alt inn på nytt i de to andre filene også. Slik ser src/routes ut når vi har opprettet alle filene vi trenger.

For å slippe å gjøre endringer i mer enn én fil lager vi en komponent av navigasjonslinjen. Inni routes lager vi en mappe som vi kaller _komponenter. Siden Svelte Kit vanligvis gjør filer som slutter på .svelte, om til nettsider, starter vi mappenavnet med understrekingstegn. På den måten sier vi fra til Svelte Kit om at ingen av filene i denne mappen eller i eventuelle undermapper skal gjøres om til nettsider. Inni mappen _komponenter lager vi en fil som vi kaller Navigasjonsmeny.svelte. I Navigasjonsmeny.svelte limer vi inn det som skal være i navigasjonsmenyen. 1 <!-- Navigasjonsmeny.svelte --> 2 <nav> 3

<ul>

4 <li><a href="/">Hjem</a></li> 5 <li><a href="/filmer">Filmer</a></li> 6 <li><a href="/om-meg">Om meg</a></li> 7 </ul> 8 </nav>

Vi trenger faktisk ikke å gjøre noe som helst for å eksportere denne Svelte-filen som en komponent. Alle Svelte-filer vi skriver og lagrer i mappen _komponenter, eksporterer automatisk seg selv som komponenter, og det eneste vi må gjøre, er å importere dem riktig.

15


16

Kapittel 2

Når vi skal importere navigasjonsmenyen som en komponent til de tre nettsidene, legger vi til en script-del i starten av de tre nettsidene. Script-delen inneholder en import-setning som henter standardeksporten fra filen der navigasjonsmenyen ligger. I index.svelte sletter vi den eksisterende navigasjonsmenyen. Deretter skriver vi <Navigasjonsmeny /> i starten av HTML-delen av hver av filene. Da ser starten av hver fil slik ut: 1 <!-- index.svelte --> 2 <script> 3

// Fordi det er en standardeksport, bruker vi ikke krøllparenteser import Navigasjonsmeny from "./_komponenter/Navigasjonsmeny.svelte" 5 </script> 4 6 7 <Navigasjonsmeny /> 8 <h1>Velkommen!</h1> 9 <!-- Resten er likt som før --> 1 <!-- filmer.svelte --> 2 <script> 3

import Navigasjonsmeny from "./_komponenter/Navigasjonsmeny.svelte"

4 </script> 5 6 <Navigasjonsmeny /> 7 <h1>Mine favorittfilmer</h1> 8 <!-- Resten er likt som før -->

1 <!-- om-meg.svelte --> 2 <script> 3

import Navigasjonsmeny from "./_komponenter/Navigasjonsmeny.svelte"

4 </script> 5 6 <Navigasjonsmeny /> 7 <h1>Om meg</h1> 8 <!-- Resten er likt som før -->


Gjenbruk og testing

Grunnen til at vi skriver from "./_komponenter/Navigasjonsmeny.svelte", er at det som står mellom fnuttene, beskriver hvor filen vi skal importere fra, ligger i forhold til filen vi importerer til. Punktumet sier at datamaskinen skal begynne i mappen vi står i, og hver skråstrek forteller hva det neste steget er. Her skal den først gå inn i mappen _komponenter og deretter til filen Navigasjonsmeny.svelte.

Eksportere og importere komponenter Alle Svelte-filer eksporterer automatisk seg selv som komponenter. Vi importerer en Svelte-fil som en komponent med koden import Komponentnavn from "./mappe/Fil.svelte". Når vi skal bruke komponenter, setter vi vinkelparenteser rundt navnet vi ga komponenten da vi importerte den: <Komponentnavn />.

STARTE MAPPE- OG FILNAVN MED UNDERSTREKINGSTEGN FOR Å FÅ SVELTE KIT TIL Å OVERSE DEM Når vi starter Svelte Kit, prøver Svelte Kit å gjøre om alle filer som ligger i mappen src/ routes og eventuelle undermapper, til nettsider. Svelte-filene som ligger i første nivå av src/routes, for eksempel src/routes/filnavn1.svelte og src/routes/filnavn2. svelte, får vi se hvis vi skriver /filnavn1 og /filnavn2 i adressefeltet i nettleseren. Legger vi filene i én eller flere undermapper, legges navnet på disse undermappene til før filnavnet. Hvis vi har en mappe som heter ole, som inneholder en mappe som heter dole, som i sin tur inneholder filen doffen.svelte, vil nettadressen ende med / ole/dole/doffen. Hvis vi starter navnet på en mappe eller en fil med et understrekingstegn, for eksempel _komponenter eller _Hemmelig.svelte, sier vi «Denne mappen/filen er kode vi bare skal bruke til å bygge opp andre nettsider med; ikke lag nettsider av den». Vi kan importere fra disse mappene og filene, men brukerne kan ikke navigere til /_komponenter/Navigasjonsmeny ved å redigere adressen i nettleseren og få se navigasjonsmenyen som en frittstående nettside. Det er ikke farlig om du skulle glemme understrekingstegnene. Det gjør jo ingenting om brukerne kan navigere til en nettside og se navigasjonsmenyen, men visningen av komponenter kan bli feil utenfor konteksten de er ment å brukes i.

Vi bruker understrekingstegn foran navnet til mapper og filer som inneholder komponenter, for eksempel _mappenavn og _Komponent.svelte, for å skille dem fra mapper og filer som skal gjøres om til nettsider.

17


18

Kapittel 2

2.3 Lage interaktive komponenter

I dette delkapittelet skal vi utvikle en nettside med en handleliste. Her skal vi bruke en interaktiv komponent som brukerne kan klikke på og endre. Komponenten heter Teller og brukes for å holde styr på antall ulike varer i handlelisten.

Slik ser src/routes ut når vi jobber med handlelisten.

Nedenfor ser du koden for Teller-komponenten slik den ser ut i utgangspunktet. Telleren har en verdi, og brukeren kan øke og minke denne ved å klikke på pluss- og minusknappene ved siden av verdien. Koden ligger lagret i filen Teller.svelte, som ligger i mappen src/routes/_komponenter. 1 <!-- _komponenter/Teller.svelte --> 2 <script> 3

let verdi = 0 //antallet av en vare

4 </script> 5

Slik ser telleren ut hvis vi prøver koden uten noe annet på en nettside.

6 <span> 7 <button on:click={() => (verdi = verdi - 1)}>-</button> 8 {verdi} 9

<button on:click={() => (verdi = verdi + 1)}>+</button>

10 </span>

Vi har lyst til å bruke telleren i handleliste.svelte. Vi skriver en import-setning som peker på filen Teller.svelte. For å sette telleren inn som en komponent i filen skriver vi <Teller /> der vi vil at komponenten skal være plassert.


Gjenbruk og testing

1 <!-- handleliste.svelte --> 2 <script> 3

import Teller from "./_komponenter/Teller.svelte"

4 </script> 5 6 <h1>Handleliste</h1> 7 <ul> 8

<li>Løk: <Teller /></li> <li>Epler: <Teller /></li> 10 <li>Melk: <Teller /></li> 11 </ul> 9

SKRIVESTILENE KEBAB-CASE, PASCALCASE OG CAMELCASE Når vi vil at en Svelte-fil skal representere en nettside som brukeren kan navigere til, navngir vi den med såkalt kebab-case: smaa-bokstaver-med-bindestreker.svelte. Det er fordi kebab-case følger den anbefalte skrivemåten for nettadresser, slik at bruk­ eren skal slippe å skille mellom små og store bokstaver. Filen med navnet handleliste. svelte blir for eksempel gjort om til en nettadresse som slutter med /handleliste.

Navnet på komponenter vi importerer, kan ikke inneholde bindestreker. Derfor skriver vi navnet på slike komponenter i PascalCase. I PascalCase kutter vi ut alle mellomrom og gjør den første bokstaven i hvert ord stor – også i det første ordet. Vi vil at filen vi importerer fra, skal ha samme navn som komponenten vi importerer. Derfor navngir vi filer som skal brukes som komponenter (og ikke som selvstendige sider), med PascalCase: Teller.svelte.

Vi bruker camelCase når vi navngir variabler og funksjoner: navnetTilVariabelEllerFunksjon Vi bruker PascalCase for eksempel navngir komponenter: Komponentnavn Vi bruker kebab-case i navnet til filer som skal gjøres om til nettsider: navnet-til-filen.svelte

19


20

Kapittel 2

EKSTRASTOFF

Navnene på de ulike skrivestilene og deres opprinnelse kebab-case har fått navnet sitt fordi teksten ser ut som matbiter som er stukket på et grillspyd. Grillspyd kalles «shish kebab» i mange land. PascalCase har fått navnet sitt fra programmeringsspråket Pascal, som igjen er oppkalt etter matematikeren Blaise Pascal. camelCase har fått navnet sitt fordi de store bokstavene i teksten kan se ut som puklene på en kamel. SCREAMING_SNAKE_CASE er en stil vi bruker for variabler som vi eksporterer. Denne stilen har fått navnet sitt fordi de store bokstavene får den til å se ut som en «ropende» variant av skrivestilen snake_case. snake_case er som kebab-case, men med understrekingstegn i stedet for bindestreker.

DEFINERE KOMPONENT-EGENSKAPER Når vi jobber med HTML-elementer, kan vi bruke attributter for å endre måten de oppfører seg og ser ut på. Hvis vi for eksempel skriver <input type="number" min={0} />, blir input-feltet et tallfelt som ikke kan ha lavere verdi enn 0. Vi kan gjøre noe lignende i Svelte-komponenter. Da kaller vi det en egenskap (property på engelsk).

Verdier vi angir i et HTML-element, kalles attributter. Verdier vi angir i en komponent, kalles egenskaper.

I stedet for å skrive <li>Løk: <Teller /></li>, slik vi gjorde i handlelisten tidligere, kan vi for eksempel skrive <li><Teller navn="Løk" /></li>. Da viser telleren navnet på varen med et kolon etterpå. Dette er en fordel hvis vi senere vil endre måten varene vises på, for da kan vi endre koden én gang i Teller.svelte i stedet for tre ganger i handleliste.svelte. Vi kan gjøre en hvilken som helst variabel i en Svelte-komponent til en egenskap ved å skrive export foran den. 1 <!-- _komponenter/Teller.svelte --> 2 <script> 3

export let navn let verdi = 0 5 </script> 4 6 7 <span> 8 {navn}: 9

<button on:click={() => (verdi = verdi - 1)}>-</button> {verdi} 11 <button on:click={() => (verdi = verdi + 1)}>+</button> 12 </span> 10


Gjenbruk og testing

Lage komponent-egenskaper Egenskaper er verdier som sendes inn til en komponent utenfra, via komponentens tagg. For å si at vi vil ha en verdi utenfra i en Svelte-komponent, skriver vi en linje som begynner med export let egenskapensNavn. For å angi en verdi til en egenskap skriver vi <Komponenten egenskapensNavn={verdien} />.

I handleliste.svelte angir vi det som tidligere sto foran Teller-taggen, som verdien til egenskapen navn. 1 <!-- handleliste.svelte --> 2 <script> 3

import Teller from "./_komponenter/Teller.svelte"

4 </script> 5 6 <h1>Handleliste</h1> 7 <ul> 8

<li><Teller navn="Løk" /></li> <li><Teller navn="Epler" /></li> 10 <li><Teller navn="Melk" /></li> 11 </ul> 9

Handlelisten ser ut akkurat slik som den gjorde før.

EGENSKAPER MED STANDARDVERDI Av og til ønsker vi at det skal være mulig å slippe å oppgi verdien for en egenskap. I HTML-attributter kan vi for eksempel la være å oppgi verdien for attributtet type i et input-felt, og da får det automatisk verdien "text". Det er med andre ord likegyldig om vi skriver <input /> eller <input type="text" />. Det er også mulig å gi egenskapene i Svelte-komponenter slike standardverdier. For å angi en standardverdi for en egenskap trenger vi bare å skrive et likhetstegn fulgt av standardverdien etter at vi har definert egenskapen, altså export let egenskapensNavn = standardverdi. Vi vil at brukeren skal kunne oppgi et antall for Teller, og vi vil at standardverdien skal være 0. For å få til dette i Teller-komponenten trenger vi bare å sette inn export foran let verdi = 0. Vi hadde allerede bestemt at verdi skulle starte med verdien 0, men nå tillater vi altså at brukeren kan overstyre denne verdien. 1 2 3 4 5 6 7

<!-- _komponenter/Teller.svelte --> <script> export let navn // Vi skriver "export" foran og "= 0" etter "let verdi" export let verdi = 0 </script> <!-- HTML-delen er lik som før -->

21


22

Kapittel 2

For å angi startverdier skriver vi verdi={ startverdi } i Teller-taggen for varene i Handleliste.svelte. For å teste at standardverdien fungerer som den skal, lar vi være å sende en startverdi til løk-telleren.

Løk får antallet 0, slik standardverdien tilsier.

1 2 3 4 5 6 7 8 9 10 11

<!-- handleliste.svelte --> <script> import Teller from "./_komponenter/Teller.svelte" </script> <h1>Handleliste</h1> <ul> <li><Teller navn="Løk" /></li> <li><Teller navn="Epler" verdi={5} /></li> <li><Teller navn="Melk" verdi={3} /></li> </ul>

Vi angir en standardverdi når vi definerer en egenskap ved å skrive koden export let egenskapensNavn = standardverdi

Vi angir egenskapens verdi i taggen til en komponent, slik: <Komponent egenskapensNavn={verdi} />

BINDE KOMPONENTERS EGENSKAPER TIL VARIABLER I Svelte kan vi binde HTML-elementenes attributter til verdien av variabler. Kodesnutten nedenfor gjør for eksempel at variabelen verdi får samme verdi som attributtet value i input-elementet. Verdien nedenfor input-feltet viser oss at bindingen virker som forventet. 1 2 3 4 5 6

<!-- input-binding.svelte --> <script> let verdi = "" </script> <label>Verdi: <input bind:value={verdi} /></label> <div>Du skrev {verdi}</div>

Det er mulig å binde Svelte-komponentenes egenskaper til verdien til variabler på samme måte. Hvis vi vil binde de ulike varene i handlelisten til en variabel, kan vi skrive bind:verdi={variabelnavn} for hver enkelt vare. Koden i Teller.svelte trenger vi ikke å endre i det hele tatt. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

<!-- handleliste.svelte --> <script> import Teller from "./_komponenter/Teller.svelte" let antallLoek let antallEpler = 5 let antallMelk = 3 </script> <h1>Handleliste</h1> <ul> <li><Teller navn="Løk" bind:verdi={antallLoek} /></li> <li><Teller navn="Epler" bind:verdi={antallEpler} /></li> <li><Teller navn="Melk" bind:verdi={antallMelk} /></li> </ul> <p>Du har {antallLoek + antallEpler + antallMelk} varer i handlelisten.</p>


Gjenbruk og testing

Fordi vi ikke har gitt antallLoek noen verdi, får denne variabelen standardverdien 0 når vi binder den. Du kan prøve å endre standardverdien til 1 i Teller.svelte. Da skal du få opp en melding om at du har 9 varer i handlelisten i stedet for 8, og verdien for løk vil være 1. Når en egenskap verken har en standardverdi eller får en verdi i komponentens tagg, får den verdien undefined. Hvis vi bare skriver <Teller /> og glemmer å sende inn en verdi for navnet, ser det altså slik ut:

LAGRE VARENE I ET ARRAY OG VISE DEM MED EN EACH-LØKKE I kapittel 1 lærte du å iterere over et array med en each-blokk. I handlelisten vi holder på å utvikle, er det en fordel at varene er lagret i et array. Da kan brukeren legge til så mange varer hun vil, og ikke et forhåndsbestemt antall. Vi venter litt med å legge til muligheten for å legge til flere varer og begynner med å flytte det vi allerede har, over i et array. 1 <!-- handleliste.svelte --> 2 <script> 3

import Teller from "./_komponenter/Teller.svelte"

4 5

let varer = [

6 { navn: "Løk", antall: 0 }, 7 { navn: "Epler", antall: 5 }, 8 { navn: "Melk", antall: 3 }, 9

]

10 11

const summerAntall = (varer) => {

12 let sum = 0 13 for (let vare of varer) { 14 sum = sum + vare.antall 15 } 16 return sum 17 } 18 19

$: totaltAntall = summerAntall(varer)

20 </script> 21 22 <h1>Handleliste</h1> 23 <ul> 24

{#each varer as vare}

25 <li><Teller navn={vare.navn} bind:verdi={vare.antall} /></li> 26 {/each} 27 </ul> 28 29 <p>Du har {totaltAntall} varer i handlelisten.</p>

Det første vi gjorde, var å legge varene til som objekter i arrayet varer. Siden arrayet kan inneholde mange verdier, er vi nødt til å bruke en løkke for å regne ut summen. Derfor har vi laget funksjonen summerAntall, som tar inn antall varer som input og bruker en løkke til å summere alle varene. Linjen $: totaltAntall = summerAntall(varer) gjør at verdien til variabelen totaltAntall regnes ut på nytt hver gang brukeren endrer verdien til varer

23


24

Kapittel 2

ved å klikke på pluss- og minusknappene. Du husker kanskje fra tidligere at $: markerer at linjen skal reagere på endringer i stedet for å kjøres bare én gang når vi kjører et program. Hvis vi hadde startet linjen med let i stedet for $:, hadde programmet bare regnet ut verdien når appen startet, og ikke oppdatert den ved senere endringer. I HTML-delen av dokumentet har vi skrevet en each-blokk som oppretter en Teller-komponent for hver verdi i varer-arrayet. Ved å skrive as vare sier vi at en verdi i arrayet midlertidig skal få navnet vare inni denne iterasjonen av blokken.

LEGGE TIL VARER I HANDLELISTEN For å gjøre handlelisten litt mer spennende vil vi gi brukerne muligheten til å legge til varer. Vi velger å lage en knapp og definere en funksjon som legger til en vare i listen. Nå som brukerne kan legge til varer selv, fjerner vi de tre varene fra handlelisten og lar varer være et tomt array. 1 <!-- handleliste.svelte --> 2 <script> 3

// Likt før dette, men varer er et tomt array når vi starter let varer = [] 5 // Resten av script-delen er lik 6 </script> 4

7 8 <h1>Handleliste</h1> 9 <ul> 10

{#each varer as vare}

11 <li><Teller navn={vare.navn} bind:verdi={vare.antall} /></li> 12 {/each} 13

Etter at vi har klikket på knappen tre ganger, ser det slik ut.

<li>

14 <button 15 on:click={() => { 16 varer = [...varer, { navn: "Ny vare", antall: 0 }] 17 }} 18 > 19 Legg til vare 20 </button> 21

</li>

22 </ul> 23 24 <p>Du har {totaltAntall} varer i handlelisten.</p>

ENDRE NAVN PÅ VARER Teller har en åpenbar mangel: Når brukerne legger til nye varer, ønsker de å sette egne navn på dem. Kanskje vil de også redigere navnet på varer de allerede har lagt til. Vi bør legge til en knapp i Teller.svelte som gjør at brukerne kan endre navnet på telleren. Når brukerne trykker på denne knappen, bør hele telleren bli til et input-felt, slik at de bare har muligheten til å redigere navnet, ikke antallet. Brukerne bør få to knapper å trykke på: én for å lagre resultatet og en annen for å avbryte og gå tilbake til navnet slik det var før de redigerte det. Dette løser vi i koden nedenfor.


Gjenbruk og testing

1 <!-- _komponenter/Teller.svelte --> 2 <script> 3 4

export let navn export let verdi = 0

5 6

// Variabler for å styre endringen let endrerNavn = false 8 let endringsforslag = "" 9 </script> 7

10 11 {#if endrerNavn} 12 <!-- Dette vises hvis man trykker på endre-knappen --> 13 <label>Nytt navn: <input bind:value={endringsforslag} /></label> 14

<button

15 on:click={() => { 16 navn = endringsforslag 17 endrerNavn = false 18 }} 19

>

20 Lagre 21 </button> 22

<button on:click={() => (endrerNavn = false)}> Avbryt </button>

23 {:else} 24 <span> 25 <!-26 Vi har lagt navn i en span-tag med litt rom til høyre 27 og kuttet ut kolonet 28 --> 29 <span style="margin-right: 0.5rem"> 30 {navn} 31 <!-- Knapp for å aktivere endring --> 32 <button 33 on:click={() => { 34 endringsforslag = navn 35 endrerNavn = true 36 }} 37 > 38 39 </button> 40 </span> 41 <button on:click={() => (verdi = verdi - 1)}>-</button> 42 {verdi} 43 <button on:click={() => (verdi = verdi + 1)}>+</button> 44 </span> 45 {/if}

Merk: Hvis du tester denne handlelisten, vil du kanskje oppdage at den oppfører seg litt rart i noen situasjoner. Dette er en feil vi har beholdt med vilje, slik at vi kan se på den i neste delkapittel.

25


26

Kapittel 2

2.4 Teste et helt dataprogram

I dataprogrammering er en test en kontroll av om det vi har utviklet, oppfører seg som forventet. Det vi tester, kan være en funksjon, en komponent eller et helt ­program. 1. Definer testen. 2. Gjennomfør testen.

Tester vi gjennomfører er en prosess i to steg. Først definerer vi testen ved å gi en tydelig beskrivelse av hvordan testen skal utføres, og hvilket resultat vi forventer. Deretter gjennomfører vi testen ved å følge beskrivelsene og sjekke om resultatet blir som forventet. Hvis resultatet blir som forventet, sier vi at testen passerte. Hvis resultatet ikke blir som forventet, sier vi at testen feilet. Når en test feiler, er det én eller flere feil i programmet. Da må vi finne ut hva som er årsaken til feilene, og fikse dem hvis det er mulig. I dette delkapittelet skal vi se nærmere på hvordan vi lager tester for et helt dataprogram. Disse testene utfører vi selv. Vi skriver dem med vanlig skriftspråk, og av og til supplerer vi med bilder eller illustrasjoner. I delkapittel 3.6 skal vi se nærmere på hvordan vi skriver enhetstester, som tester at utdataene er som forventet når vi sender visse inndata til funksjoner. Da skriver vi testene som kode, og det er datamaskinen som utfører dem for oss.


Gjenbruk og testing

TESTTILFELLER, TESTSAMLINGER OG DEKKENDE TESTER Et testtilfelle er en beskrivelse av hvordan vi skal utføre en test, og testens forventede resultater. Når vi skal teste et helt dataprogram, har vi som regel flere testtilfeller. Alle disse testtilfellene kaller vi for en testsamling. Målet er å ende opp med en testsamling som gjør at vi kan være rimelig trygge på at programmet oppfører seg som det skal Testsamlingen bør være satt sammen slik at testtilfellene dekker alt som kan skje når brukerne tar programmet i bruk. Da sier vi at testsamlingen er dekkende. Det er vanskelig å teste alt som kan skje med et dataprogram, uansett hvor lite det er. Målet er å være borti all kode som er utviklet, minst én gang i løpet av testen. Vi vil med andre ord at datamaskinen skal innom alle kodelinjer minst én gang, for eksempel alle deler av en if-else-setning, gjerne med litt forskjellige inndata. Når vi tester et dataprogram med brukergrensesnitt, bør den tenkte brukeren for eksempel ha trykket på alle knapper i brukergrensesnittet, gjerne i litt ulike rekkefølger.

KOMME PÅ TESTTILFELLER TIL ET DATAPROGRAM MED BRUKERGRENSESNITT Når du skal lage tester, er det lurt å gå ut fra at du er en av dem som skal bruke programmet:

• • •

Hva vil du oppnå med å bruke programmet? For hver ting du vil oppnå: Hvilke handlinger må du utføre? Hva forventer du at handlingene skal føre til? Hvordan forventer du at ting skal se ut underveis og helt til slutt?

Vi bruker det å bestige topper i et fjellandskap som et bilde på alt som er mulig å gjøre i et program. Vi tenker oss at vi står ved foten av fjelltoppen når vi åpner programmet. Hver topp representerer et mål som brukerne ønsker å oppnå med. Hvis vi

27


28

Kapittel 2

overfører dette til handleliste-nettsiden vår slik det ser ut nå, er det egentlig bare mulig å oppnå ett sluttmål, nemlig å lage en handleliste til en handletur. For illustrasjonens skyld har vi tegnet inn tre mål som brukeren kunne ha oppnådd hvis programmet hadde flere funksjoner, i figuren nedenfor.

Sende en handleliste til en venn

«Den glade sti»

Lage en handleliste til en handletur

Lage en handleliste som jeg kan hente opp senere

Faktisk bruk

Når du skal lage testtilfeller, kan du starte med å lage en test der brukerne følger den glade sti (the happy path på engelsk) til målet: De trykker på alle knapper i riktig rekkefølge, skriver alt riktig og gjør ingen feil. De oppnår sluttmålet uten problemer, som i fjellbildet vil si at de går den raskeste veien til fjelltoppene uten å ta en eneste omvei og uten å bli hindret av dårlig vær. Brukerne følger sjelden den glade sti. De trykker på knapper de ikke burde trykke på (fordi de bommer, blir distraherte eller misforstår), og de skriver ikke alltid riktig på første forsøk når de skal skrive inn noe i et felt. Etter at du har skrevet et testtilfelle der brukerne følger den glade sti, kan du skrive ett eller flere testtilfeller der brukerne tar omveier og gjør feil underveis. Slike testtilfeller kaller vi realistiske tilfeller. Når du har bestemt deg for hvilke testtilfeller du vil ha med i testsamlingen, skriver du ned hva de som tester programmet, skal gjøre, og hvilke resultater de skal forvente. Beskrivelsen må være så detaljert at andre kan utføre testene uten å spørre deg. Når andre kan utføre en test på nytt uten å spørre deg, sier vi at testen er reproduserbar. Du står selv fritt til å velge hvordan du vil beskrive testene. Avslutt hvert testtilfelle med en beskrivelse av de forventede resultatene, eller sluttresultatet, som godt kan være et bilde. Du kan godt beskrive forventede delresultater underveis når det er nyttig.


Gjenbruk og testing

EKSEMPEL Testsamling for handlelisten Nå skal vi teste handlelistekoden fra forrige delkapittel. Vi ser for oss at brukeren ønsker å lage en handleliste med 1 kaffe, 3 epler og 4 brød. I den glade sti-testtilfellet klarer brukeren å legge til de tre varene, redigere navnene og angi antallet uten å trykke feil en eneste gang. I et annet mer kaotisk testtilfelle ønsker vi at testen skal omfatte følgende handlinger:

• • • • • •

endre antall før man skriver inn navn skrive inn feil navn på en av varene skrive inn et navn som ikke har bokstaver skrive inn et navn som inneholder rare bokstaver (vi velger «crème fraîche») avbryte redigeringen av et varenavn bruke minusknappen for å redusere antallet av en vare fordi man har trykket for mange ganger

Vi har skrevet de to testene slik:

#2 Lage handleliste med forviklinger I dette tilfellet skal du gjøre akkurat som i testtilfelle #1, men når du har lagt til epler, gjør du følgende før du fortsetter med brød: 1. Klikk på «Legg til vare». 2. Klikk på plussknappen tre ganger og deretter på minusknappen én gang. Nå skal varen ha verdien 2. 3. Klikk på blyantknappen ved siden av den siste varen i listen. 4. Slett innholdet i feltet og klikk på «Lagre». Nå skal varen vises uten navn. 5. Klikk på blyantknappen ved siden av den siste varen i listen. 6. Skriv inn «krem fresj» og klikk på knappen «Avbryt». Varen skal gå tilbake til forrige verdi, som var uten navn. 7. Skriv eller lim inn teksten «crème fraîche» og klikk på knappen «Lagre». Nå skal navnet være «crème fraîche».

#1 Lage handleliste, den glade sti Dersom du følger de fire stegene nedenfor, skal det ligge ett punkt i handlelisten: «kaffe» med verdien 1.

Da skal

1. Klikk på «Legg til vare». 2. Klikk på blyantknappen ved siden av den siste varen i listen. 3. Skriv inn «kaffe» og klikk på «Lagre». 4. Klikk på plussknappen én gang.

et nytt punkt ha dukket opp til slutt i listen: «crème fraîche», med verdien 2 de tidligere punktene være uendrede

Fortsett som i testtilfelle #1 fra steget der du legger til brød. Sluttresultatet skal se slik ut:

Gjenta deretter steg 1 til 4, men skriv inn «epler» og klikk på plussknappen tre ganger. Da skal

• •

et nytt punkt ha dukket opp til slutt i listen: «epler», med verdien 3 de tidligere punktene være uendrede

Gjenta til slutt steg 1 til 4, men skriv inn «brød» og klikk på plussknappen fire ganger. Da skal

• •

et nytt punkt ha dukket opp til slutt i listen: «brød», med verdien 4 de tidligere punktene være uendrede

Sluttresultatet skal se slik ut:

Her har vi prøvd så godt vi kan å få med alt brukerne kan finne på. Vi har klikket på alle knappene, og takket være testtilfelle #2 har vi også klikket på dem i litt ulik rekkefølge. Derfor vil vi si at disse testene er dekkende. Hvis vi utfører testene med et vellykket resultat, kan vi lansere nettsiden med handlelisten med visshet om at de fleste brukerne vil få en god opplevelse.

29


30

Kapittel 2

Som sagt står vi fritt til å velge hvordan vi skriver ned testene. Her er noe av det vi liker med formatet vi valgte i testsamlingen for handlelisten:

• • • •

Testene har en ID. Dette gjør det lett å snakke om testene med andre, for eksempel kan vi si at «Testtilfelle #1 feilet». Testene har navn som beskriver hva målet med akkurat denne testen er. Da blir det lett for folk å forstå hva vi ønsker å teste. Jo større testsamlingen er, desto nyttigere er det å ha gode navn på testene. Det er tydelig hva som er instrukser, og hva som er forventede resultater. Det er tydelig hva som skal gjøres, og hva som er de forventede mellom- og sluttresultatene. Vi har lagt ved et bilde av sluttresultatet. Dette burde bidra til å avklare eventuelle utydelige instruksjoner.

EKSTRASTOFF

Tenke som en motstander Når du skal skrive tester for realistiske tilfeller, kan det være nyttig å tenke som en motstander som prøver så godt han kan å ødelegge programmet ditt. En slik motstander kunne for eksempel ha funnet på å gjøre følgende:

• • • • • •

skrive inn en veldig lang streng i input-feltet, for eksempel lime inn en hel avisartikkel la strengen inneholde forskjellige tegn, som norske bokstaver, bokstaver med aksenter (for eksempel é, à eller ö) eller emojier la være å skrive inn noe klikke på helt andre knapper enn «Lagre»- og «Avbryt»-knappen etter at han har redigert innholdet lime inn tekst på andre skriftspråk, for eksempel arabisk, kinesisk eller gresk prøve å lime inn et bilde i input-feltet

Når du har skrevet ned alt en motstander kunne ha gjort for å ødelegge programmet, tar du stilling til om du synes det er verdt å lage tester som prøver ut disse punktene. Mye av det som står på listen ovenfor, er rimelig å teste:

• • • •

Noen matvarer har rare tegn i navnet, som crème fraîche. Brukere gjør feil, som å lagre tomme verdier. Folk liker å bruke emojier. Hvis brukerne blir distrahert, kan de fort glemme at de holdt på å redigere navnet på én vare og klikke på pluss- og minusknappen for en annen vare.

Når det gjelder handlelisten, har vi allerede tatt hensyn til de to første punktene ovenfor i testtilfelle #2. De to siste punktene har vi ikke tatt hensyn til. Selv forslag som kan virke urimelige ved første øyekast, kan være verdifulle. Det virker for eksempel urimelig at programmet skal tåle at brukerne limer inn en hel avisartikkel. Men brukere kan jo lime inn lange strenger uten at de har til hensikt å ødelegge noe. En bruker kan for eksempel ha prøvd å kopiere én ingrediens fra en lengre matoppskrift og ved en feil ha kopiert hele matoppskriften. Dermed kan det være lurt å teste om programmet tåler det, slik at brukeren kan rette opp i feilen etterpå. Noen forslag kan vi overse med god samvittighet. De fleste nettlesere nekter å lime inn bilder i tekstfelter, så det er unødvendig å teste dette. Hvis vi lager en handleliste med norsk brukergrensesnitt, klarer brukerne seg sannsynligvis uten arabiske og kinesiske tegn.


Gjenbruk og testing

DISKUTER

Synes du at disse testene dekker alt det er naturlig å bruke handlelisten til? Er det noe du burde ha lagt til? Er det for eksempel noen knapper som det ikke er klikket på?

EKSTRASTOFF

Det er lurt å skrive testene før koden I dette delkapittelet har vi testet kode som vi allerede har skrevet. Det er god praksis å skrive testene før vi skriver koden. Når vi skriver testene etter at koden ferdig, er det flere menneskelige faktorer som gjør at kvaliteten på testene blir dårligere, blant annet:

• • •

Vi har ikke lyst til å finne feil i noe vi allerede har lagt mye arbeid i. Derfor har vi lett for å forhaste oss, og vi tar ikke oppgaven med å tenke som en motstander på alvor. Testingen virker som unødvendig ekstraarbeid: Vi eksperimenterte jo mens vi laget programmet, og det må da være godt nok. Andre oppgaver haster, og det er fristende å droppe testingen, slik at vi kan begynne på dem. Vi er sliten og lei av oppgaven som vi har holdt på med en stund nå. Vi har ikke overskudd til å skrive like gode tester som vi hadde gjort dersom vi hadde skrevet dem før vi begynte på oppgaven.

Hvis vi skriver testene, eller et utkast til dem, på forhånd, klarer vi sannsynligvis å komme på flere av forventningene en ny bruker møter nettsiden eller programmet med, og vi tar arbeidet mer alvorlig. Å skrive testene trenger ikke å ta mer enn noen få minutter. Det er ikke nødvendig å skrive ned alle stegene og få med alle kravene med en gang. Det er heller ikke sikkert at alle kravene til programmet er klare før vi har jobbet litt med det. Det viktigste er at vi gjør så mye vi kan, slik at vi kan forbedre testene etter hvert. Det er ingen som forventer at du som er elev alltid skal skrive tester på forhånd. Det er helt greit at du skriver tester etter at du har skrevet koden. Når du har fått mer erfaring, kan du prøve deg litt frem. Du kan for eksempel skrive en test før du legger til en ny knapp i et eksisterende program.

2.5 Feilsøking Feilsøking handler om å finne ut hvorfor et resultat ikke er som forventet. Med mindre du har rettet opp i handleliste-koden, feilet begge testene i forrige delkapittel. Når vi klikker på «Legg til vare» og pluss- og minusknappene, tilbakestilles navnene på de tidligere varene vi har lagt til, men ikke antallet. Dette er en utilsiktet feil som oppsto da vi skrev koden, men som vi har beholdt, slik at vi kunne bruke den til å demonstrere feilsøking.

NOTER DET DU TROR ER FEIL, OG UNDERSØK DET Det første du bør gjøre når du skal feilsøke, er å skrive ned alt du tror kan være opphav til feilen, i en tekstfil. Da unngår du å miste tråden underveis eller glemme gode ideer. Hvis du må be andre om hjelp, er det veldig nyttig for dem å vite hva du har sett på, og da kan du vise dem tekstfilen. Når du har funnet feilen og løst den, kan du ta vare på filen slik at du kan lære av det du har gjort.

31


32

Kapittel 2

Da testene fra forrige kapittel feilet, skrev han som hadde skrevet koden, følgende notater:

• • • •

Har jeg misforstått hvordan bind: virker? Fungerer bind: bare på attributter i HTML-elementer, og ikke på egenskaper i Svelte-komponenter? Fungerer det bare på tall? (Jeg har jo klart å binde til antall, og det ser ut til å virke selv om vi legger til varer.) Har jeg skrevet feil noe sted?

Når du har funnet feil bør du notere deg hva feilen er og hva du tror er årsaken til den. Hvis du ikke finner ut hva feilen skyldes med én gang, kan du

• lese dokumentasjonen for språket eller verktøyet du bruker • logge kjørende kode med funksjonen console.log • bruke utviklerverktøyet i nettleseren LESE DOKUMENTASJON OG HJELPERESSURSER PÅ NETTET For å sjekke om vi har forstått det kodetekniske riktig, kan vi lese dokumentasjonen for programmeringsspråket eller kodeverktøyet vi jobber med. Dokumentasjonen er bruksanvisningen til det vi bruker. Den forklarer hva vi kan gjøre, og er ofte supplert med interaktive kodeeksempler og bilder av forventede resultater. Hvis du har lastet ned et kodeverktøy fra en nettside, finner du ofte dokumentasjonen ved å se etter en lenke på siden med teksten «Getting started», «Documentation», «Docs», «API», «Readme» og lignende. Du kan også søke på nettet etter verktøyets navn etterfulgt av «documentation».


Gjenbruk og testing

Noen verktøy har ikke en egen hjemmeside. Det finnes for eksempel ingen nettside der du laster ned HTML, CSS og JavaScript. Disse språkene er bygget inn i nettleseren. Det betyr at nettleseren kan lese koden og vise frem resultatet. Hvis du åpner «Hjelp»-menyen i nettleseren, får du bare hjelp til å bruke nettleseren, ikke til å lage dine egne nettsider. Det betyr at du må finne dokumentasjon for språkene andre steder.

Dokumentasjon for Svelte, Javascript, HTML og CSS Du finner dokumentasjon om hvordan du lager Svelte-komponenter, på https://svelte.dev/docs. Hvis du bruker Svelte Kit, som brukes for å sette sammen flere filer til større nettsider, finner du dokumentasjonen på https://kit.svelte.dev/docs. www.w3schools.com er et begynnervennlig nettsted der du kan finne

dokumentasjon for HTML, CSS og JavaScript. Mer viderekomne programmerere pleier som regel å bruke Mozilla Developer Network (MDN), https://developer.mozilla.org/. Sidene til MDN er ofte mer oppdatert og innholdsrike, men bruker språk og eksempler som er mer teknisk avanserte enn sidene til W3Schools. For å få svar på spørsmålet «Har jeg misforstått hvordan bind: virker?» og de to tilhørende spørsmålene gikk vi til Svelte-dokumentasjonen og søkte på teksten «bind:». I hvert av de nesten 70 treffene på siden skumleste vi mens vi lette etter relevante opplysninger. Følgende setning ga et ganske tydelig svar på det ene spørsmålet: «You can bind to component props using the same syntax as for elements.» Det betyr at vi kan skrive bind: både i HTML-elementer og Svelte-komponenter og forvente at det fungerer likt. Vi fant heller ingenting i dokumentasjonen som sa at vi bare kan binde til tall og ikke strenger. Med andre ord hadde vi forstått hvordan bind: fungerer.

FEILSØKING I KJØRENDE PROGRAMKODE Vi kan se på hendelsesforløpet fra programmet starter til det uønskede resultatet oppstår, som en tidslinje som beveger seg mot høyre etter hvert som brukeren trykker på knapper og angir informasjon i grensesnittet, og linjene i programmet kjører. Feilsøking handler om å finne ut akkurat hvor på tidslinjen programmet begynner å oppføre seg annerledes enn forventet. Det enkleste – og kanskje mest brukte – verktøyet for å få oversikt over tidslinjen, er funksjonen console.log. Denne funksjonen henter inn data og viser dem i den såkalte konsollen, slik at vi kan se hva de underliggende verdiene er. Vi setter inn console.log før og etter en kodelinje for å undersøke hva verdien var før og etter at denne linjen ble kjørt. Det er også nyttig å bruke console.log til å forstå når og i hvilken rekkefølge koden kjøres. Hvis meldingene aldri dukker opp i konsollen når vi forventer dem, eller hvis de dukker opp i en uventet rekkefølge, har vi sannsynligvis skrevet noe feil.

33


34

Kapittel 2

Se konsollen på Svelte.dev/repl På https://svelte.dev/repl kan du trykke på «Console» nederst til høyre for å se hva som har blitt skrevet til konsollen. Nederst til høyre ser vi at strengen "Hei" har blitt lagret i konsollen.

ÅPNE UTVIKLERVERKTØYET PÅ EN HVILKEN SOM HELST NETTSIDE De færreste nettsider har et sted i dokumentet der vi kan klikke for å se hva som er skrevet til konsollen. Derfor må vi åpne utviklerverktøyet (developer tool på engelsk) til nettleseren, som er et eget vindu der vi kan finne informasjon som er interessant for utviklere, men ikke for vanlige brukere.

Når du er inne i en spesifikk fane i nettleseren og vil åpne utviklerverktøyet for å se på innholdet i denne fanen, kan du bruke følgende tastesnarveier: • Windows og Linux: Ctrl + Shift + I • Mac: Command + Option + I

For å beskytte brukere mot ondsinnede angrep har Apple valgt å deaktivere utviklerverktøyet i Safari som standard. Hvis du bruker Safari, må du derfor slå på utviklerverktøyet ved å åpne innstillingene (trykk på Cmd + ,), gå til fanen «Avansert» og klikke på «Vis Utvikle-menyen i menylinjen».


Gjenbruk og testing

Når du har åpnet utviklerverktøyet, klikker du på fanen «Console» (eller «Konsoll» i Safari), hvis den ikke allerede er åpen.

EKSEMPEL Konsollen som angrepsflate Folk med ondsinnede hensikter kan lure intetanende ofre til å lime skadelig kode inn i konsollen og kjøre den. Ofte dukker slik kode opp i kjedebrev med løfter om at koden for eksempel skal hjelpe mottakerne med å se hvem som er inne på profilen deres, eller med å få flere «likes». Slik ondsinnet kode klikker på knapper og sender forespørsler på brukerens vegne. Resultatet er som regel at angriperen får tilgang til offerets konto og sensitiv informasjon. Noen ganger sprer slik kode seg til offerets omgangskrets ved at den sender meldinger til offerets venner eller legger ut innlegg på offerets vegne med de samme oppfordringene og løftene. For å stoppe slike angrep viser noen populære nettsteder en advarsel når noen åpner konsollen på det aktuelle nettstedet. Du kan trygt bruke konsollen på nettsider du selv har skrevet.

Nå skal vi se hvordan vi brukte console.log for å finne feilen i handleliste-programmet. Det første vi ville gjøre, var å forsikre oss om at navn faktisk ble endret inni Teller når koden kjører. Da endret vi funksjonen som lå i «Lagre»-knappen ved å legge inn to console.log-utsagn.

35


36

Kapittel 2

Prøv å legge inn de tilsvarende endringene i koden på datamaskinen din, og gjennom­før testtilfelle #1 fra kapittel 2.3. 1 <button 2 on:click={() => { 3 console.log("Navn var", navn) 4 navn = endringsforslag 5 6

console.log("Navn endret til", navn) endrerNavn = false

7

}}

8 > 9

Lagre

10 </button>

Når du har utført testen, ser konsollen slik ut:

Linjen oppfører seg med andre ord som forventet. Variabelen navn som inneholdt verdien "Ny vare", får den nye verdien etter at du har klikket på «Lagre». Her begynte vi å mistenke at det var noe galt i måten handleliste.svelte reagerte på endringer fra Teller på. Derfor la vi inn følgende linje i slutten av script-blokken i handleliste.svelte: $: console.table(varer)

Prøv å legge til denne linjen i handleliste.svelte på datamaskinen din, og gjennomfør testtilfelle #1 fra 2.4 på side XXX igjen. Noter det du observerer.

console.table er en variant av console.log. Forskjellen er at console. table viser dataene vi gir den, som en tabell. Det er praktisk når vi jobber med arrayer. Fordi linjen starter med $:, kjører den når en av verdiene i varer endrer seg. Altså burde vi se en melding i konsollen hver gang vi endrer antall eller navn for en av varene.


Gjenbruk og testing

Da vi utførte testen, så loggen slik ut etter at vi hadde lagt til kaffe og epler:

Hvis vi følger med på loggen mens vi utfører testen, er det lett å se når ting går galt. Når vi har gitt et nytt navn til det første objektet i arrayet, bør vi for eksempel se en melding i konsollen, men den dukker aldri opp. Når vi deretter legger til en ny vare i listen, dukker det opp en melding som viser at det første objektet i arrayet aldri fikk en ny verdi. DISKUTER

Hvor tror du det er sannsynlig at feilen ligger med utgangspunkt i det vi har gjort av logging og feilsøking til nå? Hva ville ha vært ditt neste steg i feilsøkingsprosessen hvis du ikke har noen mistanke?

37


38

Kapittel 2

På dette tidspunktet bestemte vi oss for å lese gjennom all koden som hadde med sammenkobling av listen og Teller-komponenten å gjøre, og da så vi fort hva feilen var. Vi hadde glemt å sette inn bind: foran egenskapen navn i Teller-komponenten. Koden var altså: 1 <Teller 2 navn={vare.navn} 3

bind:verdi={vare.antall}

4 />

Vi oppdaterte koden slik: 1 <Teller 2 bind:navn={vare.navn} 3

bind:verdi={vare.antall}

4 />

Hvis vi retter denne feilen og kjører testene på nytt, blir resultatet som forventet både for testtilfelle #1 og #2.

FEILSØKING ER SJELDEN GØY, MEN FEILEN ER SOM REGEL ENKEL Når koding og feilsøking forklares i undervisningssammenheng, er det lett å få inntrykk av at det er en enkel og følelsesløs prosess: «Hvis X, gjør Y og Z, og vips så har du ønsket resultat.» I virkeligheten er feilsøking forbundet med en del frustrasjon. Selv de som har mange års erfaring som programmerere, blir frustrert når koden ikke virker som forventet. Når vi oppdager årsaken til en feil, virker den ofte veldig åpenbar. Slike feil er både en lettelse og en skuffelse. Det tar kort tid å rette dem opp, men de burde jo ikke ha


Gjenbruk og testing

skjedd i utgangspunktet. Det er imidlertid umulig å ikke gjøre feil i ny og ne, men en god strategi kan være som følger: Fiks, le og lær! Først retter du opp det som gikk galt, så ler du litt, og så gjør du det du kan for å unngå at det skjer igjen. I vårt tilfelle lærte vi at vi alltid må dobbeltsjekke bindinger i koden. Bonusen ble at vi fikk et eksempel vi kunne bruke til å demonstrere feilsøking og testing. Kanskje skjønte du hva feilen var, allerede før vi begynte å feilsøke. Det er i så fall kjempebra! Det er ofte lettere å se feil i koden som andre har skrevet, enn i koden man skriver selv. Dette er en av grunnene til at programmerere ofte ser over hverandres kode, gjerne mens de skriver den. Vi snakker mer om verdien av kodesamarbeid i kapittel 6.

2.6 Enhetstester

En enhetstest er en test som sjekker et lite stykke kode, altså en enhet. Det vanligste formålet med en enhetstest er å sjekke at dataene vi får tilbake fra en funksjon, stemmer overens med det vi forventer. I motsetning til testene vi lærte om i delkapittel 2.4, utfører vi ikke disse enhetstestene selv. I stedet skriver vi kode og lar datamaskinen utfører testene for oss. Slik ser src/routes ut når vi jobber med dette delkapittelet.

En samling med funksjoner som kan være til nytte flere steder i et dataprogram, kalles for et bibliotek (library på engelsk).

I dette delkapittelet jobber vi med disse filene:

• • •

_bibliotek/hjelpemidler-strenger.js er et bibliotek vi holder på å skrive. _komponenter/ErLikTest.svelte er en komponent som utfører én enhetstest. streng-testsamling.svelte er en testsamling med hjelpemiddelstrenger som bruker komponenten ErLikTest til å utføre tester.

Vi begynner med å lage ErLikTest.svelte som skal regne ut utfallet av én test og si om resultatet er som forventet eller ikke. 1 <!-- _komponenter/ErLikTest.svelte --> 2 <script> 3 4 5 6 7 8

/** Funksjonen vi ønsker å teste. */ export let funksjon /** Argumentene funksjonen skal få. Oppgis som et array. */ export let inndata /** Resultatet vi forventer at funksjonen skal returnere. */ export let forventetUtdata

9 10 11 12

let feil let erLike let resultat

13 14

try {

15 /* 16 * Når vi skriver "enFunksjon(...etArray)", 17 * legger datamaskinen inn verdiene i "etArray" 18 * som argumenter til funksjonen: 19 * Første verdi blir første argument, andre verdi

39


40

Kapittel 2

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

* blir andre argument og så videre. */ resultat = funksjon(...inndata) erLike = resultat === forventetUtdata } catch (error) { /* * Fordi "funksjon" er under utvikling mens vi tester, * kan den krasje. I så fall lagrer vi feilen * til variabelen "feil" og logger den til konsollen. */ feil = error console.log(error) } </script>

35 <!-36 Emojier som gjør det lett å se om testene 37 passerte, feilet eller krasjet. 38 39

Vi har valgt emojier med forskjellig form i tillegg til farge for å hjelpe fargeblinde.

40 --> 41 {#if feil}💥{:else if erLike}✅{:else}❌{/if} 42 <!-43 44 45

Viser forventet inndata og utdata. Bruker JSON.stringify for å vise kodedetaljer, som hakeparenteser og hermetegn, rundt dataene.

46 --> 47 <span class="data">{JSON.stringify(inndata)}</span> 48 ->

49 <span class="data">{JSON.stringify(forventetUtdata)}</span> 50 <!-- Hvis testen feilet eller krasjet, vil vi vise mer --> 51 {#if feil || !erLike} 52 <ul> 53 <li> 54 {#if feil} 55 <!-56 57 58 59 60 61

62 63 64 65 66

Begrenser oss til "feil.message", som er en oppsummering. Brukeren kan åpne konsollen for å se hele feilmeldingen.

--> KRASJET: <span class="data">"{feil.message}"</span> {:else} Fikk -> <span class="data">{JSON.stringify(resultat)}</span> {/if} </li> </ul> {/if}

67 <style> 68 .data { 69 font-family: monospace; /* Kodeskrift */ 70 white-space: pre; /* La alle mellomrom stå */ 71 } 72 </style>

Øverst i komponenten har vi angitt følgende egenskaper: funksjon, inndata og forventetUtdata. Ovenfor disse har vi skrevet kommentarer som forklarer hva slags data de skal motta. Like nedenfor har vi angitt de tre variablene feil, erLike og resultat, som vi skal bruke til å lagre utfallet av testen.


Gjenbruk og testing

I de siste linjene i script-blokken kaller vi på funksjonen. Vi har lagt dette funksjonskallet inni en try-catch-blokk. En try-catch-blokk prøver å gjøre det som står mellom krøllparentesene etter try. Hvis det oppstår en feil som ville fått programmet til å krasje, angrer datamaskinen på alt den har gjort hittil, og gjør i stedet det som står mellom krøllparentesene etter catch. Du får se nytten av dette ganske snart. I HTML-delen av filen viser vi informasjon om hvordan testen gikk. Aller først viser vi en emoji som varierer etter om testen passerte, feilet eller krasjet. Etter emojien viser vi forventede inndata og utdata. Hvis funksjonen krasjet eller testen feilet, viser vi hva som var galt som et kulepunkt under testen.

Try-catch-blokker En try-catch-blokk ser slik ut: 1 try { 2 /* 3

* Datamaskinen prøver å gjøre det * som står mellom krøllparentesene, 5 * men hvis en feil som ville fått 6 * programmet til å krasje oppstår … 7 */ 8 } catch (error) { 9 /* 10 * … angrer datamaskinen på alt den gjorde i try 11 * og gjør det som står mellom 12 * krøllparentesene etter catch. 13 * Feilen som oppsto, er lagret i parameteren error. 14 */ 15 } 4

Du kan tenke på en try-catch-blokk som en variant av en if-else-setning: if (alt kjører uten å krasje) { /* gjør det vanlige */ } else { /* gjør noe annet */ }. Forskjellen på en try-catch-blokk og en if-else-setning er at datamaskinen ikke vet hvilken vei den skal velge, før den starter, og derfor må prøve seg frem.

UNNGÅ Å BRUKE TRY-CATCH MED MINDRE DU ABSOLUTT MÅ Try-catch-blokker er en nødløsning. Vi tyr til dem når det viser seg å være umulig å skrive en if-else-setning som forhindrer feilen. I ErLikTest.svelte tar vi inn en hvilken som helst funksjon. Siden det finnes uendelig mange måter en ukjent funksjon kan krasje på, kan vi umulig skrive en if-else-setning som beskytter oss mot alle disse krasjene. Derfor tillater vi oss å bruke try-catch i ErLikTest.svelte.

41


42

Kapittel 2

I oppgavedelen skal du legge inn flere funksjoner i hjelpemidler-strenger.js og lage tester for dem.

LAGE FUNKSJONENE VI SKAL TESTE Før vi begynner med å lage tester, må vi ha et utkast til det vi skal teste. Vi åpner filen hjelpemidler-strenger.js og lager funksjonen gjoerForbokstavStor. Som tidligere nevnt, er det som regel en fordel å skrive testene før man skriver koden. Derfor setter vi inn en funksjon som bare inneholder return. Da gjør ikke funksjonen noe. 1 // _bibliotek/hjelpemidler-strenger.js 2 export let gjoerForbokstavStor = (streng) => { 3 return 4 }

BEGYNNE PÅ TESTSAMLINGEN Vi begynner på testsamlingen ved å lage én enkelt test for funksjonen ­gjoerForbokstavStor. Vi åpner filen streng-testsamling.svelte. Vi importerer funksjonen gjoerForbokstavStor og komponenten ErLikTest til denne filen og skriver én test. 1 <!-- streng-testsamling.svelte --> 2 <script> 3

import { gjoerForbokstavStor } from "./_bibliotek/hjelpemidler-strenger.js" import ErLikTest from "./_komponenter/ErLikTest.svelte" 5 </script> 4 6 7 <h1>Enhetstester for hjelpemidler-strenger.js</h1> 8 9 <h2>gjoerForbokstavStor</h2> 10 <!-- Legg merke til arrayet i inndataene --> 11 <ErLikTest 12 13 14

inndata={["liten"]} forventetUtdata={"Liten"} funksjon={gjoerForbokstavStor}

15 />

Når vi åpner nettsiden /streng-testsamling, skal den vise én test som feilet. Dette er et godt tegn, for nå vet vi at ErLikTest-komponenten faktisk kommer til å si fra hvis dataene ikke er som forventet.

LAGE EN DEKKENDE TESTSAMLING FOR GJOERFORBOKSTAVSTOR Som nevnt i delkapittel 2.4 bør en testsamling være dekkende. Det vil si at den ­kontrollerer det meste som brukerne kan finne på å gjøre når de bruker programmet. Vi ser for oss at funksjonene vi lager i hjelpemidler-strenger.js, først og fremst skal brukes av norskspråklige programmerere. Vi kan blant annet sette sammen funksjonene slik at de gjør om en streng som en bruker har skrevet inn, til et egnet variabelnavn, for eksempel «hent2SoekeresultaterFraNorge». Derfor kan vi begrense oss til å teste tilfeller som består av de 29 bokstavene i det norske alfabetet, tall og mellomrom.


Gjenbruk og testing

Nedenfor ser du testtilfellene vi har laget. Vi har lagt testtilfellene i arrayet storForbokstavTilfeller, slik at vi kan iterere over dem. I linjen før hver verdi i arrayet har vi skrevet en kommentar om hvorfor vi har med akkurat den verdien. Nederst har vi laget en each-løkke som lager en ErLikTest-komponent for hvert testtilfelle. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

<!-- streng-testsamling.svelte --> <script> import { gjoerForbokstavStor } from "./_bibliotek/hjelpemidler-strenger.js" import ErLikTest from "./_komponenter/ErLikTest.svelte" // Første verdi er inndataene, andre er resultatet let storForbokstavTilfeller = [ // Tom streng, fordi man ofte glemmer denne [[""], ""], // Grunntilfelle: én bokstav [["i"], "I"], // Flere bokstaver, men ikke norske [["liten"], "Liten"], // Starter med norsk bokstav [["år"], "År"], // Starter allerede med stor bokstav [["Stor"], "Stor"], // Starter med tall [["1-er"], "1-er"], // Hvis strengen starter med mellomrom, skal ingenting skje [[" fortsatt liten"], " fortsatt liten"], // Streng som inneholder komma [["ja, vi elsker"], "Ja, vi elsker"], // Ikke lag store bokstaver etter punktum [["bare. første"], "Bare. første"], ] </script> <h1>Enhetstester for hjelpemidler-strenger.js</h1> <h2>gjoerForbokstavStor</h2> <ul> {#each storForbokstavTilfeller as data} <li> <ErLikTest inndata={data[0]} forventetUtdata={data[1]} funksjon={gjoerForbokstavStor} /> </li> {/each} </ul>

43


44

Kapittel 2

Vi har valgt disse for å ha testtilfeller som starter både med internasjonale bokstaver, særnorske bokstaver, mellomrom og tall. Det er fordi vi ser for oss at dette representerer de strengene brukerne våre kan tenkes å sende inn til funksjonen. Vi ville også ha med punktum, bindestrek og kommaer i noen av strengene (men ikke som første tegn) fordi dette er vanlige tegn å ha med i en norsk tekststreng. Vi har også tatt med et testtilfelle med en tom streng. Mange funksjoner som fungerer på strenger med ett eller flere tegn, kan krasje hvis strengen er tom. Vi glemmer ofte å tenke på tomme strenger når vi skriver kode, enda det er vanlig når brukeren ikke har skrevet noe i et input-felt.

Tomme strenger og arrayer Det er vanlig at funksjoner må håndtere tomme strenger, for eksempel når et input-felt er tomt. Husk å ta med testdata med tomme strenger for funksjoner som behandler strenger, med mindre du vet at funksjonen aldri kommer til å motta tomme strenger. Hvis du skriver tester der inndataene er et array, bør du også lage testtilfeller der arrayet er tomt: [].

EKSTRASTOFF

Testtilfeller vi har utelatt En gang i fremtiden kan det hende vi skal bruke gjoerForbokstavStor på en måte som gir oss større variasjon i inndataene. Vi kunne for eksempel ha brukt funksjonen til å gi brukerne av en chattetjeneste muligheten til å rette meldingene sine automatisk. I så fall burde vi lage testtilfeller der strengene starter med emojier og annet som chattemeldinger typisk starter med. Men siden funksjonen bare skal brukes av programmerere foreløpig, får vi lite igjen for å skrive slike testtilfeller nå. Programmererne skriver sannsynligvis ikke inn slike tegn. Vi kan heller legge til slike testtilfeller etter hvert. For å si det billedlig: En fabrikk som lager bokseåpnere, tester bare bokseåpnerne på hermetikkbokser, ikke på tankskip og pengeskap.

FÅ FUNKSJONEN TIL Å GJØRE SOM TESTENE VIL Når vi har skrevet testsamlingen, går det neste steget ut på å få funksjonen til å gjøre det testene ber om. Derfor skal vi fylle ut gjoerForbokstavStor slik at testene lykkes. Gjør gjerne et forsøk selv før du leser løsningsforslaget vårt. Ha testsamlingen åpen i nettleseren og se hva som skjer underveis. Vi begynner med å returnere strengen akkurat som vi fikk den, og allerede da blir nesten halvparten av tilfellene grønne, det vil si de tilfellene der teksten skal være uendret, men det er i alle fall en start.


Gjenbruk og testing

1 // _bibliotek/hjelpemidler-strenger.js 2 export let gjoerForbokstavStor = (streng) => { 3 return streng 4 }

Nå gjør vi et forsøk på å gjøre første bokstav stor. Etter litt prøving og feiling har vi følgende kode: // hjelpemidler-strenger.js export let gjoerForbokstavStor = (streng) => { let foersteBokstav = streng[0] let resten = streng.slice(1) return foersteBokstav.toUpperCase() + resten }

Dette forsøket var stort sett vellykket, men funksjonen krasjer på den første testen i samlingen. For å se hele feilen åpner vi konsollen. Nedenfor ser du hvordan det ser ut i Chrome.

45


46

Kapittel 2

Opplistingen ovenfor viser hvor datamaskinen befant seg i koden da krasjet som vi avverget, oppsto. Når vi skal løse en slik feil, starter vi øverst i listen og jobber oss nedover for å se om vi finner årsaken. I starten av hver linje står navnet på funksjonen eller komponenten datamaskinen var inni da krasjet oppsto. I slutten av hver linje er det en lenke med formatet filnavn:linjenummer:tegnnummer. Denne angir filen og nummeret på linjen og tegnet der datamaskinen befant seg. Vi kan klikke på lenken for å se filen i utviklerverktøyet, eller vi kan finne den i utviklerverktøyet. Nedenfor ser du hvor vi havner når vi klikker på det første resultatet.

Hvis inndataene er "", altså en helt tom streng, vet vi at det skaper trøbbel fordi variabelen foersteBokstav har verdien undefined. Som feilmeldingen sa, kan vi ikke skrive .toUpperCase() i en variabel som inneholder undefined. For å unngå krasjen kan vi sjekke om strengen har lengde 0 og i så fall returnere en tom streng og dermed avslutte kjøringen av koden i funksjonen. 1 2 3 4 5 6 7 8 9

// hjelpemidler-strenger.js export let gjoerForbokstavStor = (streng) => { if (streng.length === 0) { return "" } let foersteBokstav = streng[0] let resten = streng.slice(1) return foersteBokstav.toUpperCase() + resten }

Og da går alt bra!


Gjenbruk og testing

REFAKTORERE – GJØRE KODEN MER ELEGANT Når funksjonen har passert alle testene våre, lagrer vi funksjonen. Deretter refaktorerer vi, det vil si vi prøver å finne ut om det er mulig å gjøre funksjonen kortere og mer elegant. I vårt tilfelle har vi jo måttet legge til en if-setning for å ta høyde for tomme strenger. Finnes det en måte å hente ut første bokstav på der vi ikke ender opp med undefined, slik at vi kan kutte ut if-setningen? Hvis du ikke kjenner til alle muligheter, kan det være nyttig å lese dokumentasjonen for datatypen du jobber med. I dette tilfellet kan du søke på «w3schools string documentation» på nettet. Da vil «JavaScript String Reference - W3Schools» være et av de øverste resultatene: https://www.w3schools.com/jsref/jsref_obj_string.asp

Hvis du klikker på lenken til dokumentasjonen for charAt, får du vite litt om hvordan funksjonen fungerer. Under overskriften «Return value» står det at resultatet blir en tom treng hvis indeksen er ugyldig, altså hvis det ikke finnes noen bokstav på den nevnte plassen. Da blir "".charAt(0) === "" i stedet for undefined. Dette kan vi teste i konsollen.

47


48

Kapittel 2

Dersom vi bruker streng.charAt(0) i stedet for streng[0] til å hente ut første bokstav, bør det med andre ord være mulig å kutte ut lengdesjekken. Vi endrer koden slik: 1 [Kodeboks, start] 2 // _bibliotek/hjelpemidler-strenger.js 3 export let gjoerForbokstavStor = (streng) => { 4 let foersteBokstav = streng.charAt(0) 5 6

let resten = streng.slice(1) return foersteBokstav.toUpperCase() + resten

7 }

Når vi har lagret filen, ser vi en hake på grønn bakgrunn foran alle testene i listen. Det betyr at de fortsatt lykkes. Nå ser vi ikke noe mer vi kan forbedre, og vi sier oss fornøyd med funksjonen.

Enhetstester En enhetstest er en test som sjekker om koden oppfører seg som forventet. Når vi skriver enhetstester, lager vi en samling testtilfeller. Målet er at disse skal være dekkende, altså at de representerer den typen data som vi forventer å få fra brukere. Vi kan skrive enhetstester for en funksjon etter at vi har laget den, men det beste er å skrive testene før vi skriver funksjonen. Dette kalles testdrevet utvikling og omfatter disse stegene: 1. Skriv en funksjon som ikke returnerer noe. 2. Lag en testsamling med dekkende testtilfeller. 3. Gjennomfør ett og ett testtilfelle og sørg for at alle testtilfellene lykkes. Ofte lykkes flere enn ett samtidig. 4. Når alle testtilfellene passerer, refaktorerer du.

2.7 Vanlige problemer når du tester med tall På grunn av måten tall lagres på på datamaskinen, blir ikke alltid resultatet slik vi forventer. Her er en testsamling vi har laget for funksjonen leggSammen fra matematikk.js. 1 // _bibliotek/matematikk.js 2 export let leggSammen = (x, y) => { 3 return x + y 4 }


Gjenbruk og testing

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

<!-- matematikk-testsamling.svelte --> <script> import ErLikTest from "./_komponenter/ErLikTest.svelte" import { leggSammen } from "./_bibliotek/matematikk.js" let leggSammenTilfeller = [ // Noen enkle heltall [[0, 0], 0], [[0, 1], 1], [[1, 1], 2], // Desimaltall [[0.5, 0.5], 1], [[0.1, 0.2], 0.3], /* * Store tall * * I Javascript kan man bruke understrekingstegn * for å gjøre lange tall lettere å lese */ [[9_007_199_254_740_991, 1], 9_007_199_254_740_992], [[9_007_199_254_740_991, 2], 9_007_199_254_740_993], [[9_007_199_254_740_991, 3], 9_007_199_254_740_994], [[9_007_199_254_740_991, 4], 9_007_199_254_740_995], ] </script> <h1>Enhetstester for matematikk.js</h1> <h2>leggSammen</h2> <ul> {#each leggSammenTilfeller as data} <li> <ErLikTest inndata={data[0]} forventetUtdata={data[1]} funksjon={leggSammen} /> </li> {/each} </ul>

Utfallet av det femte testtilfellet er uventet: I stedet for 0,3 får vi et langt tall. Og selv om de påfølgende testtilfellene passerer, har vi fått et annet resultatet enn vi oppga, i to av dem, nærmere bestemt i de tilfellene der resultatet skulle ha blitt et oddetall.

49


50

Kapittel 2

DATAMASKINEN LAGRER IKKE ALLE TALL SLIK VI SKRIVER DEM Årsaken til begge disse feilene er at datamaskinen bruker totallssystemet, der tall bare består av tallene 0 og 1. Derfor er det mange desimaltall datamaskinen ikke klarer å representere nøyaktig. Store heltall er også et problem fordi datamaskinen ikke har plass nok til å lagre dem. EKSTRASTOFF

Totallssystemet og flyttall I totallssystemet er det slik at den plassen som står til venstre for desimaltegnet, er verdt 1, og derfra dobler verdien seg for hver plass man går mot venstre: 2, 4, 8, 16, 32 og så videre. Titallssystemet følger et lignende mønster der verdien tidobler seg for hver plass man går mot venstre: 1, 10, 100, 1000 og så videre. Så lenge vi jobber med heltall, kan vi oversette nøyaktig mellom alle tall i begge systemer. Når vi passerer kommaet, blir ting vanskeligere. Siden vi går mot høyre i titallssystemet, får hver plass verdiene

1 1 1 , , og så videre, mens plassene i totallsystemet har verdiene 10 100 1000

1 1 1 , , og så videre. Det går dessverre ikke an å legge sammen brøkene som står til høyre for 2 4 8 1 2 3 kommaet i totallsystemet, og ende opp med en brøk som kan forkortes til , eller , 10 10 10 altså 0,1, 0,2 og 0,3. Når vi skriver desimaltall, lagrer datamaskinen disse tallene med så mange sifre som det er plass til. For å lagre både store og små tall og spare plass lagrer datamaskinen tall på en normalform som kalles for flyttall (floating point numbers på engelsk). Hvis vi skal skrive veldig store eller veldig små tall i titallssystemet, kan vi for eksempel skrive én million som 1 · 106 eller én milliontedel som 1 · 10–6. Systemet datamaskinen bruker for å lagre tall, er basert på samme mønster, bortsett fra at den ikke bruker en tierpotens for å forstørre eller forminske verdien, men en toerpotens. For eksempel vil verdien

1 bli lagret som 1 · 2–1. 2

Fordi datamaskinen har en bestemt mengde bits, altså plasser til 1- og 0-tegn, som den kan bruke på eksponenten og på tallet som står foran gangetegnet, klarer den ikke å lagre oddetall nøyaktig hvis tallet er større enn 253 eller mindre enn –253.

Hvis du vil se den verdien som datamaskinen egentlig lagrer når du skriver inn et tall, kan du skrive (tallet).toPrecision(100) i konsollen i nettleseren. Da får du tilbake en streng med den verdien som faktisk er lagret (med mange overflødige nuller på slutten). Hvis du bruker toPrecision med tallene 0,1, 0,2 og 0,3, får du se at datamaskinen egentlig lagrer følgende verdier (her har vi kuttet bort de bakerste ­nullene):

• • •

0,1000000000000000055511151231257827021181583404541015625 0,200000000000000011102230246251565404236316680908203125 0,299999999999999988897769753748434595763683319091796875

Disse verdiene returneres som strenger, men hvis vi kopierer tallverdien som står mellom hermetegnene, og limer den inn igjen i konsollen, får vi verdien vi startet


Gjenbruk og testing

med: 0,1, 0,2 og 0,3. Det er fordi datamaskinen bruker avrundingsregler når den viser tallverdier. Disse reglene er bestemt slik at overraskende mange tall vises som den verdien vi skrev inn. Disse avrundingsreglene er ikke magiske. Selv om vi legger sammen to tall som avrundes riktig, blir ikke resultatet alltid som vi forventer. Regnestykket 0,1 + 0,2 har for eksempel resultatet 0,30000000000000004440892098500626161694526672363, som datamaskinen runder av til 0.30000000000000004 når den viser tallet. Denne verdien er ikke lik verdien som 0,3 blir konvertert til, og derfor gir 0.1 + 0.2 === 0.3 resultatet false. Som en parallell kan vi forestille oss at vi 1 1 2 ble bedt om å vurdere om , men måtte regne med desimaltall i stedet for 3 3 3 brøker. Da ville vi ha skrevet om til 0,333 + 0,333 = 0,666 ≈ 0,67 og funnet at 0,66 ≠ 0,67. Utfallet er feil, men vi kunne ikke ha gjort det noe bedre med de begrensningene vi hadde.

LAGE EN TEST-KOMPONENT SOM SAMMENLIGNER ET VISST ANTALL DESIMALER For å løse problemet med desimaler som rundes av, i testene våre, kan vi lage en ny komponent, ErLikTestMedPresisjon, som sjekker om resultatet er likt innenfor et visst antall desimaler. Den er stort sett identisk med ErLikTest, men har egenskapen antallDesimaler, som vi bruker for å bestemme hvor mange desimaler tallene skal ha når vi sammenligner dem. Vi velger at denne egenskapen skal ha standardverdien 2. For å runde av tallene bruker vi toPrecision. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

<!-- _komponenter/ErLikTestMedPresisjon.svelte --> <script> export let funksjon export let inndata export let forventetUtdata // Ny egenskap export let antallDesimaler = 2 let feil let erLike let resultat try { resultat = funksjon(...inndata) // Runder av før sammenligning erLike = resultat.toPrecision(antallDesimaler) === forventetUtdata.toPrecision(antallDesimaler) } catch (error) { feil = error console.log(error) } </script> {#if feil}💥{:else if erLike}✅{:else}❌{/if} <!-- Lik visning, bortsett fra to detaljer: --> <span class="data">{JSON.stringify(inndata)}</span> -> <span class="data">{JSON.stringify(forventetUtdata)}</span> <!-- Opplys om antall desimaler etter testen … --> (innenfor {antallDesimaler} desimaler)

51


52

Kapittel 2

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

{#if feil || !erLike} <ul> <li> {#if feil} KRASJET: <span class="data">"{feil.message}"</span> {:else} Fikk -> <span class="data">{JSON.stringify(resultat)}</span> <!-- … og hva det avrundede resultatet var i feilvisningen --> , som avrundes til <span class="data">{resultat.toPrecision(antallDesimaler)}</span> {/if} </li> </ul> {/if} <style> .data { font-family: monospace; /* Kodeskrift */ white-space: pre; /* La alle mellomrom stå */ } </style>

I matematikk-testsamling.svelte bytter vi til ErLikTestMedPresisjon. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 21 22 23

<!-- matematikk-testsamling.svelte --> <script> import ErLikTestMedPresisjon from "./_komponenter/ErLikTestMedPresisjon.svelte" // ellers lik script-del </script> <h1>Enhetstester for matematikk.js</h1> <h2>leggSammen</h2> <ul> {#each leggSammenTilfeller as data} <li> <!-- ... og her bruker vi den nye komponenten --> <ErLikTestMedPresisjon inndata={data[0]} forventetUtdata={data[1]} funksjon={leggSammen} antallDesimaler={2} /> </li> {/each} </ul>

Da blir resultatet som følger:


Gjenbruk og testing

Hvis vi sender inn en høyere verdi til egenskapen antallDesimaler, for eksempel 20 med antallDesimaler={20}, vil testen feile igjen, for da er ikke lenger de avrundede verdiene like.

HVA VI SKAL GJØRE MED STORE TALL, AVHENGER AV SITUASJONEN Du kan se det høyeste heltallet som datamaskinen klarer å lagre nøyaktig, ved å skrive Number.MAX_SAFE_INTEGER i konsollen. Da får du ut verdien 9007199254740991. Når et heltall er over denne grensen, er det ikke sikkert at datamaskinen klarer å lagre det nøyaktig. Hvis datamaskinen ikke klarer å lagre den nøyaktige verdien, runder den det av til den nærmeste verdien den klarer å representere. Burde vi gjøre noe med leggSammen for å hanskes med tall som er større enn dette? Hvis vi kan akseptere litt unøyaktighet, kan vi godt la være å gjøre noe. Det er ikke sikkert at funksjonen leggSammen noensinne skal brukes til å legge sammen tall som er så store som dem vi brukte i denne testen. Number.MAX_SAFE_INTEGER er et veldig stort tall, over 9 billiarder. Når vi jobber med så store tall, blir et avvik på 1 forsvinnende lite – under én billiarddel. I slike situasjoner klarer vi oss som regel fint med et avrundet, litt unøyaktig svar. I visse sammenhenger er det ikke akseptabelt med unøyaktige tall. Hvis vi for eksempel leter etter store primtall, nytter det ikke å jobbe med en avrundet verdi – da må vi jobbe med det nøyaktige tallet. Hvis leggSammen skal brukes i en slik sammenheng, er det bedre at funksjonen sier fra til brukeren. Nedenfor ser du hvordan vi kan få funksjonen leggSammen til å gi en feilmelding dersom resultatet er for høyt til å være nøyaktig. Da vil testtilfellene si fra når tallet er for høyt. 1 // matematikk.js 2 export let leggSammen = (x, y) => { 3 let resultat = x + y 4

if (resultat > Number.MAX_SAFE_INTEGER) {

5 throw new Error( 6 "Resultatet var høyere enn Number.MAX_SAFE_INTEGER" 7 ) 8 9 10 }

} return resultat

53


54

Kapittel 2

EKSTRASTOFF

Noen tall bør lagres som strenger En av bokas forfattere hadde en gang et oppdrag for et firma som laget et salgssystem. Dette salgssystemet hadde bestillings-ID-er som besto av tallrekker på 20 sifre. Den dagen firmaet lanserte hjemmesiden sin, fikk de feilmeldinger fra kunder som fikk opp andre bestillinger enn den de hadde søkt på. Feilen viste seg å ligge på nettsiden der kundene skulle taste inn bestillings-ID-en. Etter at kunden hadde tastet bestillings-ID-en inn i et input-felt, ble den gjort om til et tall. Fordi tall på 20 sifre ligger langt over Number.MAX_SAFE_INTEGER, ble mange tall rundet av til et nærliggende tall som tilsvarte en annen bestilling. De klarte heldigvis å fikse feilen samme dag. I hverdagen bruker vi ofte tallrekker av ulik lengde for å identifisere personer, steder, bestillinger og lignende. Noen eksempler er fødselsnumre, telefonnumre og postnumre. Disse tallene er godt under Number.MAX_SAFE_INTEGER, men de starter ofte med 0-tegn. JavaScript fjerner overflødige 0-tegn fra starten av et tall, så slike 0-tegn vil forsvinne dersom disse numrene lagres som tall. Skatteetaten.no oppgir «011299 55131» som et eksempel på et fødselsnummer. Postnumrene i Oslo starter på 0, for eksempel «0010» (postnummeret til Det kongelige slott). Prøver vi å lagre disse verdiene som tall, kan det skape små og store problemer. Når vi bruker en tallrekke til å identifisere noe, er det altså best å lagre den som en streng. Da slipper vi å få problemer hvis tallet starter på 0 eller blir veldig langt.

Viktige grenser for tall Number.MAX_SAFE_INTEGER og Number.MIN_SAFE_INTEGER er det høy-

este heltallet og det laveste heltallet datamaskinen klarer å representere nøy­ aktig. Når et heltall befinner seg innenfor dette området, klarer datamaskinen alltid å lagre tallverdien nøyaktig. Når et heltall befinner seg utenfor dette området, runder datamaskinen av til den nærmeste verdien den klarer å lagre. Jo lenger man beveger seg utenfor grensene, desto større blir unøyaktigheten i absoluttverdi, men den er som regel bitte liten i forhold til hvor stort tallet er. Number.MAX_VALUE er det største tallet datamaskinen i det hele tatt klarer å

lagre. Denne verdien er større enn 10300, altså et titall med 300 nuller bak. Hvis du skriver inn tall som er større enn dette, klarer ikke datamaskinen engang å runde det av.


Gjenbruk og testing

в I programmering handler gjenbruk om å eksportere og importere for eksempel

SAMMENDRAG

funksjoner og variabler fra én fil til én eller flere andre filer. Det er vanlig å lagre funksjoner og variabler som skal eksporteres, i en JavaScript-fil. в Vi eksporterer en funksjon ved å skrive nøkkelordet export foran klassedefinisjo-

nen, slik: export let funksjonsnavn = ... For å importere en funksjon til en fil skriver vi import { funksjonsnavn } from "./filnavn.js". в Vi eksporterer en variabel ved å sette inn nøkkelordet export foran definisjonen

av variabelen, slik: export let variabelnavn = ... Vi importerer variabelen ved å skrive import { variabelnavn } from "./filnavn.js". в Hvis en funksjon eller en variabel har en tydelig hovedrolle i en fil, kan vi gjøre

dette til filens standardeksport. Når vi skal definere en standardeksport, må vi først definere funksjonen eller variabelen uten å eksportere den: let etEllerAnnet = ... Senere i samme fil skriver vi en linje der det står export default etEllerAnnet. Da er etEllerAnnet blitt filens standardeksport. в Når vi skal importere en standardeksport, setter vi ikke krøllparenteser rundt nav-

net når vi importerer den. Dersom standardeksporten ligger i en fil som heter foo. js, og denne filen ligger i samme mappe som filen vi importerer til, skriver vi import etEllerAnnet from "./foo.js". в Når vi importerer en standardeksport, står vi fritt til å velge et annet navn på

eksporten enn navnet den hadde da den ble eksportert. Vi kunne altså også ha skrevet import nyttNavnSelvOmDetErDaarligSkikk from "./foo.js". в En komponent er en liten bit av en nettside som brukes til å bygge opp en større

nettside. En komponent kan brukes i flere filer. Den kan også brukes gjentatte ganger på samme side. в Alle Svelte-filer eksporterer automatisk seg selv som komponenter, slik at vi kan

bruke dem som nettopp det. Vi importerer en Svelte-fil som en komponent med koden import Komponentnavn from "./eventuelle-mappenavn/Filnavn. svelte". Når vi skal bruke komponenter, setter vi vinkelparenteser rundt navnet vi ga komponenten da vi importerte den: <Komponentnavn />. в Vi bruker understrekingstegn foran navnet til mapper og filer som inneholder

komponenter, for eksempel _Komponent.svelte og _mappenavn, for å skille dem fra filer og mapper som skal gjøres om til nettsider.

55


56

Kapittel 2

в Vi bruker skrivestilen camelCase når vi navngir variabler og funksjoner:

navnetTilVariabelEllerFunksjon в Vi bruker skrivestilen PascalCase for eksempel når vi navngir komponenter:

Komponentnavn в Vi bruker skrivestilen kebab-case i filnavnet til filer som skal gjøres om til nettsi-

der: navnet-til-filen.svelte в Egenskaper er verdier som angis i komponentens tagg. For å si at vi vil ha en

verdi fra taggen i en Svelte-komponent, skriver vi en linje som begynner med export let egenskapensNavn. For å angi verdien til en egenskap skriver vi <Komponenten egenskapensNavn={verdien} /> в Vi angir standardverdien til en egenskap slik:

export let egenskapensNavn = standardverdi.

Egenskapen får denne verdien dersom vi ikke angir en annen verdi i komponentens tagg. в Et testtilfelle er en beskrivelse av hvordan en test skal gjennomføres, og testens

forventede resultater. En samling av testtilfeller kalles en testsamling. En testsamling er dekkende dersom testtilfellene er innom alt som kan skje når programmet tas i bruk. в En enhetstest er en test som sjekker en funksjon for å se om den oppfører seg

som forventet. Når vi skriver enhetstester, lager vi en samling testtilfeller. Målet er at disse skal være dekkende, altså at de representerer den typen data som brukerne kan finne på å sende inn til funksjonen. в Når vi skriver testene før vi skriver programkoden, praktiserer vi testdrevet utvik-

ling. в Number.MAX_SAFE_INTEGER gir det høyeste heltallet som en datamaskin kla-

rer å representere nøyaktig, mens Number.MIN_SAFE_INTEGER gir det laveste. Number.MAX_VALUE er det største tallet datamaskinen klarer å lagre.


Gjenbruk og testing

Oppgaver

57


4 Objektorientert programmering KOMPETANSEMÅL I dette kapittelet skal du jobbe for å nå disse målene: в utvikle objektorienterte programmer med klasser, objekter, metoder og arv в anvende objektorientert modellering til å beskrive et programs struktur

4.1 Klasser og objekter

I programmeringen vi har drevet med hittil, har vi behandlet data og funksjoner som to atskilte konsepter: Variabler inneholder data, og funksjoner tar inn data og gir tilbake et svar. I dette kapittelet skal vi lære å lage egne datatyper som lagrer verdier, og som har innebygde funksjoner. Dette kalles for objektorientert programmering. En klasse er en oppskrift, eller mal, som beskriver hvordan datamaskinen skal lage en spesifikk datatype. En variabel som er opprettet med utgangspunkt i en klasse, kaller vi for et objekt. Nedenfor ser du filen _hund.js. Den inneholder en klasse vi har gitt navnet Hund. Fordi det står export foran den, kan den importeres til andre filer. 1 // _hund.js 2 export class Hund { 3 constructor(navn) { 4 this.navn = navn 5

}

6 }

Vi importerer klassen Hund til filen hund-test.svelte og oppretter et objekt av Hund som lagrer navnet til hunden Fido.


Objektorientert programmering

Tegning av et hus og et ferdigbygd hus. En klasse kan sammenlignes med tegningen, og objektet med huset.

1 <!-- hund-test.svelte --> 2 <script> 3 import { Hund } from "./_hund.js" 4 5 let hundeobjekt = new Hund("Fido") 6 </script> 7 8 <h1>Flink bisk, {hundeobjekt.navn}!</h1>

Når vi starter en linje med class, forteller vi datamaskinen at det som kommer mellom de påfølgende krøllparentesene, er en klassedefinisjon. Før krøllparentesene kommer klassens navn, i dette tilfellet Hund. Vi skriver klassens navn med stor forbokstav for å skille klasser fra vanlige variabler og funksjoner.

Klasser En klasse er oppskriften på et objekt. Vi definerer en klasse slik: 1 class Klassenavn { 2 constructor(parameter1, parameter2) { 3 this.felt1 = parameter1 4 this.felt2 = parameter2 5 } 6 }

Et objekt som tilhører en viss klasse, kalles for et objekt av klassen Klassenavn.

I klassedefinisjonen oppretter vi metoder, som er objektenes innebygde funksjoner. I klassedefinisjonen for klassen Hund hadde vi bare én metode, og det var den såkalte konstruktøren, som alltid heter constructor.

59


60

Kapittel 4

Jobben til konstruktøren er å lagre de dataene som metoden får gjennom parameterne sine, på riktig sted i objektet. Disse stedene kalles felter (fields på engelsk). Vi legger en verdi i et felt ved å skrive this.feltnavn = verdi. Konstruktøren til Hund har én parameter, navn. Konstruktøren plasserer verdien i et felt som også heter navn. I linje 5 i programmet på forrige side oppretter vi et nytt objekt. I denne linja får vi datamaskinen til å gjøre tre ting:

• • • De spesifikke verdiene vi gir til parametere når vi kaller på en klasse, kalles argumenter: Parameterne er hullene, mens argumentene er klossene som går gjennom hullene.

let hundeobjekt får datamaskinen til å opprette variabelen hundeobjekt. new Hund("Fido") får datamaskinen til å kalle på konstruktøren som ligger i klassen Hund med argumentet "Fido" for parameteren navn. Når vi kaller på en klasses konstruktør, er vi nødt til å skrive new og deretter navnet til klassen.

Likhetstegnet = får datamaskinen til å lagre resultatet av koden på høyre side i en variabel med navnet hundeobjekt.

Metoder og konstruktøren Alle klasser inneholder metoden som kalles konstruktøren. Konstruktørmetoden heter alltid constructor. Vi bruker konstruktøren når vi oppretter et objekt av en klasse. Jobben til konstruktøren er å lagre dataene vi gir den, i riktige felter. For å kalle på konstruktøren skriver vi new fulgt av klassenavnet til konstruktøren og parenteser. Mellom parentesene skriver vi verdiene vi vil kalle på klassen med: new Klassenavn(argument1, argument2, …)

I HTML-delen av koden kan vi hente ut og vise frem verdien til feltet navn i ­hundeobjekt ved å skrive hundeobjekt.navn.

Felter Et felt er et sted i et objekt der data lagres. Vi oppretter feltene i konstruktørmetoden. For å opprette et felt skriver vi this.feltnavn = verdi. Når vi har opprettet et objekt og lagret det i en variabel, får vi tilgang til dataene som er lagret i et av objektets felter, ved å skrive variabelnavn.feltnavn.


Objektorientert programmering

EKSEMPEL Objekt med flere felter Nedenfor har vi definert en klasse som heter Sirkel. Konstruktørmetoden tar inn radiusen som parameter. Konstruktøren bruker radiusen til å regne ut arealet og omkretsen til sirkelen før den lagrer disse verdiene i felter. 1 <script> 2 class Sirkel { 3 constructor(radius) { 4 this.radius = radius 5 this.omkrets = 2 * Math.PI * radius 6 this.areal = Math.PI * radius * radius 7 } 8 } 9 10 let minSirkel = new Sirkel(10) 11 </script> 12 13

<div>

14 15

Vi har en sirkel med radius {minSirkel.radius}. <br /> Arealet av sirkelen er {minSirkel.areal}. <br /> 16 Omkretsen av sirkelen er {minSirkel.omkrets}. 17 </div>

O = 2πr A=

πr2 r

61


62

Kapittel 4

EKSEMPEL Endre verdien til et felt Vi kan endre verdiene i objektets felter etter at vi har opprettet objektet. Dette gjør vi ved å skrive objektnavn.feltnavn = nyVerdi. I følgende kodesnutt vil Fido bli omdøpt til Pluto. 1 <!-- hund-test.svelte --> 2 <script> 3 import { Hund } from "./_hund.js" 4 5 let hundeobjekt = new Hund("Fido") 6 hundeobjekt.navn = "Pluto" 7 </script> 8 9 <h1>Flink bisk, {hundeobjekt.navn}!</h1>

I Svelte går det an å binde verdiene i HTML-attributter sammen med verdiene som ligger lagret i variabler, ved å skrive bind:attributt={variabelnavn}. På samme måte kan vi også binde et attributt sammen med verdien til et felt ved å skrive bind:attributt={variabelnavn.feltnavn}. I koden nedenfor har vi lagt til et input-felt og bundet det til navnet til hundeobjekt. Når vi skriver inn et annet navn, for eksempel Nala, endres samtidig verdien til navn-feltet i hundeobjekt. 1 <!-- hund-test.svelte --> 2 <script> 3 import { Hund } from "./_hund.js" 4 5 let hundeobjekt = new Hund("Fido") 6 </script> 7 8 <h1>Flink bisk, {hundeobjekt.navn}!</h1> 9 <label>Hundenavn: <input bind:value={hundeobjekt.navn} /></label>

4.2 Metoder

I forrige delkapittel brukte vi bare klasser og objekter til å lagre data. Dette kunne vi fått til med færre kodelinjer dersom vi brukte teknikker vi allerede kjenner til, slik som JavaScript-objektene vi skriver med krøllparenteser: { foo: 1, bar: "en streng" }. DISKUTER

Hvordan ville du ha programmert eksemplene med klassene Hund og Sirkel for å oppnå samme resultat uten å bruke objektorientert programmering?


Objektorientert programmering

Forskjellen på objektene i objektorientert programmering og JavaScript-objektene er at vi kan bygge inn funksjoner i objektorientert-programmering-objektene. Disse funksjonene kalles metoder. Vi kan blant annet bruke metoder til å styre hvordan vi oppdaterer data i objektene, slik at feltene ikke inneholder data som er i uoverensstemmelse med hverandre.

EKSEMPEL Felter kan komme i uoverensstemmelse med hverandre Hvis feltene i en klasse inneholder data som avhenger av hverandre, kan disse komme i uoverensstemmelse med hverandre dersom vi oppdaterer ett felt og ikke et annet. Hvis vi ser på sirkeleksemplet, er det lett å havne i situasjoner der arealet, omkretsen og radiusen ikke stemmer overens hvis vi bare oppdaterer ett av feltene, slik vi gjør i koden nedenfor. 1 <script> 2 class Sirkel { 3 constructor(radius) { 4 this.radius = radius 5 this.omkrets = 2 * Math.PI * radius 6 this.areal = Math.PI * radius * radius 7 } 8 } 9 10 let minSirkel = new Sirkel(10) 11 // Oppdaterer radius, men glemmer areal og omkrets. 12 minSirkel.radius = 1 13

</script>

14 15 <div> 16 Vi har en sirkel med radius {minSirkel.radius}. <br /> 17 <!-- Areal og omkrets stemmer ikke med radius --> 18 19

Arealet av sirkelen er {minSirkel.areal}. <br /> Omkretsen av sirkelen er {minSirkel.omkrets}.

20 </div>

I koden ser vi at radiusen først ble satt til å være 10 og deretter endret til 1. Nå stemmer ikke lenger radiusen overens med arealet og omkretsen til sirkelen.

SØRGE FOR AT DATA IKKE KOMMER I UOVERENSSTEMMELSE MED SETT-METODER Som nevnt er en av fordelene med objektorientert programmering muligheten til å ha såkalte metoder i objektene. Den viktigste forskjellen mellom metoder og frittstående funksjoner er at en metode har tilgang til å endre feltene i et objekt, og hente data fra dem. Metoder får tilgang til disse dataene ved å bruke nøkkelordet this. På neste side ser vi hvordan vi kan bruke en såkalt sett-metode for å sette radiusen til en ny verdi. Sett-metoder oppdaterer verdier og gjør alt annet «opprydningsarbeid» som må gjøres samtidig. Dette arbeidet går som regel ut på å endre verdien til andre felter.

63


64

Kapittel 4

I koden nedenfor har vi lagt til metoden settRadius i klassen Sirkel. Denne metoden endrer radiusen og regner ut verdien til de to andre feltene. Legg merke til at metoden starter med et verb. Det er en utbredt skikk å starte metodenavn med et verb, mens feltnavn bør starte med et substantiv. Dette hjelper leseren til å forstå at metoden er en metode og ikke et felt. Sannsynligvis er verbet nok til at leseren skjønner alt metoden gjør, uten å lese kodelinjene i den. I den siste linjen i script-blokka kaller vi på metoden ved å skrive minSirkel.settRadius(1). 1 <!-- sirkel-med-setter.svelte --> 2 <script> 3 class Sirkel { 4 constructor(radius) { 5 this.radius = radius 6 this.omkrets = 2 * Math.PI * radius 7 this.areal = Math.PI * radius * radius 8 } 9 10 settRadius(radius) { 11 this.radius = radius 12 this.omkrets = 2 * Math.PI * radius 13 this.areal = Math.PI * radius * radius 14 } 15 } 16 17 let minSirkel = new Sirkel(10) 18 minSirkel.settRadius(1) 19 </script> 20 21 <div> 22 Vi har en sirkel med radius {minSirkel.radius}. <br /> 23 Arealet av sirkelen er {minSirkel.areal}. <br /> 24 Omkretsen av sirkelen er {minSirkel.omkrets}. 25 </div>

Nå stemmer arealet og omkretsen med radiusen.

Kalle på metoder En metode er en funksjon som er bygd inn i et objekt. Vi kaller på en metode ved å skrive objektnavn.metodenavn(argumenter). Vi setter punktum etter objektnavnet for å hente ut metoden, slik som når vi henter ut verdien fra et felt. For å kalle på metoden må vi alltid ha med parentesene til slutt, slik som med funksjoner. Eventuelle argumenter skriver vi mellom parentesene.


Objektorientert programmering

Metodenavn bør starte med et verb og felter med et substantiv Metodenavn bør starte med et verb. Felter bør starte med et substantiv. Det er fordi metoder gjør noe, mens felter er noe. Dette hjelper leserne med å se forskjellen på metoder og felter når de leser koden. Er du flink, klarer du å gi et så godt navn til metodene og feltene at leserne ikke trenger å lese mer enn metode- eller feltnavnet og eventuelle parameternavn når de skal bruke klassen.

BRUKE METODER TIL Å GJØRE UTREGNINGER MED DATAENE I OBJEKTET Metoder kan ikke bare endre innholdet i felter. De kan også brukes til å gjøre utregninger, slik at vi muligens kan klare oss med færre felter. Når vi snakker om utregninger, viser vi ikke bare til matematiske utregninger, men også til alt mulig annet som vi kan gjøre med frittstående funksjoner, som å sette sammen strenger. I Sirkel kan vi for eksempel regne ut arealet og omkretsen når vi trenger dem, i stedet for å lagre verdiene i felter. Da slipper vi alt ekstraarbeidet med å sørge for at de tre feltene stemmer overens, og vi trenger bare å lagre den ene verdien, som bestemmer de to andre: radius. I kodesnutten nedenfor har vi gjort nettopp dette. I stedet for å regne ut areal og omkrets i konstruktøren har vi laget to metoder som regner ut arealet og omkretsen når de trengs: regnUtAreal og regnUtOmkrets. Vi har kvittet oss med settOmkrets, for nå er det ikke lenger behov for noe ekstraarbeid når vi endrer radiusen, så i stedet kan vi bare endre den direkte. For moro skyld har vi lagt til et input-felt som er bundet sammen med verdien til feltet radius i minSirkel. Når vi redigerer verdien i feltet, oppdateres det utregnede arealet og den utregnede omkretsen automatisk på nettsiden. 1 <!-- sirkel-med-automatisk-utregning.svelte --> 2 <script> 3 class Sirkel { 4 constructor(radius) { 5 this.radius = radius 6 } 7 8 regnUtAreal() { 9 return Math.PI * this.radius * this.radius 10 } 11 12 regnUtOmkrets() { 13 return 2 * Math.PI * this.radius 14 } 15 } 16 17 let minSirkel = new Sirkel(10) 18 </script> 19

65


66

Kapittel 4

20 <div> 21 Vi har en sirkel med radius {minSirkel.radius}. <br /> 22 Arealet av sirkelen er {minSirkel.regnUtAreal()}. <br /> 23 Omkretsen av sirkelen er {minSirkel.regnUtOmkrets()}. 24 </div> 25 <div> 26 <label> 27 Radius: 28 <input type="number" bind:value={minSirkel.radius} /> 29 </label> 30 </div>

Vi kunne ha laget frittstående funksjoner som gjorde den samme jobben som metodene vi akkurat la til, altså let regnUtAreal = (radius) => { ... } og let regnUtOmkrets = (radius) => { ... }. Fordelen med en metode er at vi ofte kan kutte ut én eller flere parametere som vi hadde trengt dersom den var en frittstående funksjon. Metoden kan nemlig hente inn data fra felter ved å bruke nøkkelordet this.


Objektorientert programmering

EKSEMPEL Sette sammen strenger i en metode Nå skal vi legge til en metode i Hund, slik at alle hunder kan presentere seg på en nettside. Vi kaller metoden for formulerPresentasjon. 1 // _hund.js 2 export class Hund { 3 constructor(navn) { 4 this.navn = navn 5

}

6 7

formulerPresentasjon() {

8 return 9 }

`Jeg

heter ${this.navn}. Voff!`

10 }

I formulerPresentasjon har vi brukt en malstreng (template string på engelsk) for å sette sammen en streng. Du finner en nærmere forklaring i faktaboksen om malstrenger. I visningen kaller vi på metoden mellom krøllparenteser for å vise frem resultatet. Vi har lagt til et input-felt der vi kan endre navnet på hunden, slik at vi kan se at metoden kjøres automatisk idet vi endrer hundens navn. 1 <!-- bundet-hund-med-metode.svelte --> 2 <script> 3 import { Hund } from "./_hund.js" 4 5 let hundeobjekt = new Hund("Fido") 6 </script> 7 8 <h1>Hei, hund!</h1> 9 <div>{hundeobjekt.formulerPresentasjon()}</div> 10 <label>Hundenavn: <input bind:value={hundeobjekt.navn} /></label>

Malstrenger En malstreng har gravistegn``(backticks på engelsk) i starten og slutten i stedet for vanlige anførselstegn (" "). Hvis vi skriver ${variabelnavn} i løpet av strengen, setter vi inn variabler: `Jeg heter ${variabelnavn}. Voff!`. Dette ligner på måten vi kan sette inn variabler på i HTML-delen av en Svelte-fil. Det er bare i script-delen av koden og i JavaScript-filer vi må bruke dollartegnet foran krøllparentesen.

Vi kan oppnå samme resultat dersom vi slår sammen strenger med plusstegnet: "Jeg heter " + this.navn + ". Voff!". I moderne JavaScript foretrekker vi å bruke malstrenger foran plusstegnet fordi de krever mindre plass i kildekoden, og fordi det er lettere å se om vi har husket mellomrommene som skal med.

67


68

Kapittel 4

EKSEMPEL Metoder som kaller på andre metoder Metoder kan kalle på andre metoder i klassen i tillegg til å hente ut verdier fra felter. Nedenfor definerer vi klassen Kontakt, som brukes til å lagre virtuelle visittkort, altså kort med navnet og kontaktopplysningene til en person. Klassen definerer to metoder i tillegg til konstruktøren: settSammenFulltNavn, som returnerer en streng med personens fulle navn, og lagVisittkort, som formaterer det som skal stå på visittkortet. Fordi personens fulle navn skal stå på visittkortet, kaller lagVisittKort på metoden settSammenFulltNavn for å gjøre jobben sin. 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 }

Et typisk visittkort inneholder navn, firma og kontaktopplysninger til en person.

12 13

lagVisittkort() {

14 return `${this.settSammenFulltNavn()}, e-post:

${this.epostadresse}` } 16 } 15

1 <!-- kontaktliste.svelte --> 2 <script> 3 import { Kontakt } from "./_kontakt.js" 4 5 let kontakter = [ 6 /* 7 8 9 10 11 12 13

* Vi oppretter kontaktene rett i arrayet, * uten å legge dem i en variabel. */ new Kontakt("Ole", "Olsen", "ole.olsen@example.no"), new Kontakt("Kari", "Nordmann", "kari.nordmann@ example.no"), ] </script>

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

Kontaktinformasjonen til to personer vises i en punktliste.


Objektorientert programmering

NØKKELORDET THIS Som vi har sett, bruker vi ofte nøkkelordet this når vi definerer en klasse – både når vi oppretter felter og metoder, og når vi refererer til dem inne i en klassedefinisjon. Nøkkelordet this finnes fordi et objekt trenger en måte å vise til seg selv på, enda det ikke vet hvilket variabelnavn det kommer til å få i fremtiden. En metode vet ikke engang om objektet den hører til, skal få et navn i det hele tatt. For eksempel fikk ikke kontaktene i kontaktlisten i forrige eksempel egne variabelnavn.

Objekter bruker nøkkelordet this for å vise til seg selv i metoder, slik at metodene kan hente ut data fra felter, endre dataene i felter og kalle på andre metoder i samme objekt. For å hente ut verdien fra et felt i en metode skriver vi this.feltnavn. For å gi en ny verdi til et felt i en metode skriver vi this.feltnavn = nyVerdi. For å kalle på en metode skriver vi this.metodenavn(argumenter).

TVINGE FREM OPPDATERINGER ETTER METODEKALL Hittil har vi sett mange eksempler på at innholdet på nettsiden oppdateres automatisk hvis vi endrer verdien til et felt ved å gi det en ny verdi, enten ved å skrive objektnavn.feltnavn = nyVerdi eller ved å binde sammen input-felter og feltnavn ved å skrive bind:value={objektnavn.feltnavn}. Hvis vi bruker metoder for å oppdatere innholdet i objektet, kan det oppstå situasjoner der de oppdaterte verdiene ikke kommer på skjermen. Heldigvis lar dette seg løse, som vi skal se i det følgende eksemplet. Vi har en klasse Teller som er lagret i filen _teller.js. Denne klassen representerer en slik telledings som dørvakter på for eksempel fotballstadioner og utesteder har i hånda: Trykker du på knappen, øker verdien med én. 1 // _teller.js 2 export class Teller { 3 constructor() { 4 this.antall = 0 5

}

6 7

tell() {

8 this.antall = this.antall + 1 9 } 10 11

beskrivTelling() {

12 return `Du har talt ${this.antall}` 13 } 14 }

69


70

Kapittel 4

Vi skal lage en nettside som tester denne telleren. Du får se første-, andre- og tredjeutkastet til nettsiden, der andreutkastet inneholder en programfeil som vi fikser i tredjeutkastet. Nedenfor ser du førsteutkastet. 1 2 3 4 5 6 7 8 9 10 11 12

<!-- teller-test.svelte --> <script> import { Teller } from "./_teller.js" let minTeller = new Teller() </script> <h1>Teller</h1> <p>{minTeller.beskrivTelling()}</p> <button on:click={() => (minTeller.antall = minTeller.antall + 1)}> Tell 1 til </button>

Her viser vi frem resultatet av å kalle på beskrivTelling. Vi har også en knapp som øker verdien i feltet minTeller.antall med én når vi klikker på den, ved å skrive rett til feltet. Etter at vi har klikket på knappen seks ganger, står det at vi har talt til 6. Altså fungerer programmet som forventet. Da vi skrev førsteutkastet, overså vi metoden tell. Fordi vi har lyst til å ta imot all hjelpen som klassen tilbyr oss (og teste at alle metodene virker), prøver vi å bruke metoden tell i stedet for å oppdatere feltet selv. Vi endrer slik at knappen kaller på metoden tell. 1 <button on:click={() => minTeller.tell()}>Tell 1 til</button>

Når vi nå tester koden, hender det noe rart: Antallet ser ikke ut til å øke! Det skjer i alle fall ikke noe der vi kaller på minTeller.beskrivTelling(). Hvis vi legger til koden console.log("Antall: " + minTeller.antall) i tell-metoden, klikker fire ganger på knappen og åpner utviklerkonsollen i nettleseren, ser vi derimot at det har blitt skrevet ting til konsollen hver gang vi klikket på knappen. tell-metoden kalles på, men visningen oppdateres ikke.

Av disse meldingene kan vi lese at metoden har blitt kalt på. Hvorfor oppdateres da ikke utseendet? For å få Svelte til å reagere på at vi har endret innholdet i et objekt med en metode, må vi tilegne objektet til seg selv i linjen etter at vi har kalt på metoden ved å skrive: minTeller = minTeller. Nedenfor ser du hvordan vi til slutt endrer knappen i teller-test.svelte slik at visningen oppdateres enda vi bruker metoden tell for å oppdatere innholdet i minTeller. 1 <button 2 on:click={() => { 3 minTeller.tell() 4 // Sender signal om å oppdatere visningen 5 minTeller = minTeller

Nå oppdateres visningen når vi trykker på knappen.

6 }} 7 > 8 Tell 1 til 9 </button>


Objektorientert programmering

FORTELLE SVELTE AT VI HAR OPPDATERT INNHOLDET I ET OBJEKT MED EN METODE Når en variabel står mellom krøllparenteser i HTML-delen av dokumentet i Svelte, for eksempel <div>{variabelnavn}</div>, styrer variabelen denne delen av visningen. Svelte oppdaterer dette området automatisk når variabelen variabelnavn står på venstre side av et likhetstegn i en kodelinje den kjører, for eksempel variabelnavn = nyVerdi eller variabelnavn.feltnavn = nyVerdi. Svelte oppdaterer også visningen automatisk dersom variabelen er bundet sammen med et HTML-attributt ved hjelp av bind:. Når vi oppdaterer innholdet i et objekt som styrer visningen, med en metode, for eksempel objektnavn.metodenavn(), må vi skrive objektnavn = objektnavn i den påfølgende linjen for at Svelte skal forstå at det har skjedd en oppdatering. EKSTRASTOFF

Svelte er nødt til å overse metoder for å unngå uendelige løkker Linjen minTeller = minTeller trengs på grunn av måten Svelte holder visningen oppdatert på. Som du sikkert vet, betyr krøllparenteser i HTML-koden, slik som {minTeller.beskrivTelling()} eller bare {enHvilkenSomHelstVerdi}, at vi setter inn verdien i dokumentet. Svelte setter ikke bare inn verdien når vi skriver krøllparenteser. Når Svelte vet at variabelen som krøllparentesen starter med (altså minTeller og enHvilkenSomHelstVerdi), har fått en ny verdi, er Svelte så tjenestevillig at det oppdaterer det som står mellom krøllparentesene. Problemet vi støter på her, er at Svelte ikke kan vite når det kommer til en linje med et metodekall dersom objektet som metoden kalles på, endrer seg. Svelte vet altså ikke om minTeller har fått nytt innhold i feltene når vi kaller på minTeller.tell(). Svelte reagerer på endringer i variabler hvis og bare hvis

• •

variabelen står på venstre side av et enkelt likhetstegn i en kodelinje som kjøres variabelen får ny verdi fordi den er bundet med bind:

Førsteutkastet fungerte altså fordi kodelinjen minTeller.antall = minTeller.antall + 1 kjøres når vi klikker på knappen, og der står minTeller på venstre side av et likhetstegn. I andreutkastet var det verken likhetstegn eller binding, og derfor visste ikke Svelte at minTeller hadde fått en ny verdi. I tredjeutkastet satte vi inn minTeller = minTeller for å fortelle Svelte at «nå vet jeg, programmereren, at minTeller har fått ny verdi», og derfor virket programmet som forventet igjen. Av ulike grunner er det ikke praktisk mulig for en datamaskin å analysere et stykke kode (for eksempel en metode) og ha full oversikt over konsekvensene av å kjøre den. Dermed er det ikke mulig for Svelte å analysere en metode for å finne ut om den endrer noe eller ikke. Den er nødt til å holde seg til overfladiske, sikre tegn, som = og bind:. Men hvorfor er det så viktig å vite om det skjer endringer eller ikke? Kan ikke Svelte bare oppdatere, uavhengig av om det skjedde en endring eller ikke? Metoder brukes ganske ofte når man viser frem data, som vi prøvde med {minTeller.beskrivTelling()}. Slike metodekall hadde forårsaket en uendelig løkke som hadde endt opp med å krasje nettsiden om siden skulle blitt oppdatert for hvert metodekall. Da hadde vi fått ett metodekall for å finne verdien idet siden lastes inn, fulgt av en oppdatering av nettsiden fordi det ble kalt på metoden, fulgt av et metodekall for å finne verdien som skal oppdateres, fulgt av en oppdatering, og så videre). Derfor kan ikke Svelte oppdatere idet vi kaller på metoder.

71


72

Kapittel 4

4.3 Bruksområder for klasser og metoder

I dette delkapittelet ser vi på noen bruksområder for klasser og metoder. Vi tar ikke for oss nye teknikker, men viser hvordan du kan sette sammen teknikkene du har lært hittil. Du vil se at vi kan gjøre mye med et lite verktøyskrin. Du må selvsagt ha litt erfaring med verktøyene før du klarer å komme frem til slike løsninger selv, så ta dette delkapittelet som inspirasjon heller enn som en målestokk på hva du skal kunne nå.

Med kullsyre Varmt vann

Full ½ Tom

Tapp 0,5 l 1 glass

Tanken er tømt

Tanken er fylt

BRUKE METODER FOR Å IVARETA REGLER FRA DEN VIRKELIGE VERDEN Selv om det vanligvis går bra når vi endrer innholdet i et felt direkte, er det ikke alltid lurt. Vi kan ende opp med å gi feltet verdier som det ikke går an for objektet å ha i virkeligheten. Et typisk eksempel er at vi setter et tall til en negativ verdi som det ikke kan ha i virkeligheten. En sirkel kan for eksempel ikke ha en negativ radius, og en kopp kan ikke inneholde en negativ mengde kaffe. Metoder kan forhindre at felter får verdier de ikke skal ha.

5 liter

I starten fokuserer programmererne på funksjonaliteten bak knappene med teksten «Tapp 0,5 l», «Tanken er tømt» og «Tanken er fylt».

Noen programmerere i et vannautomatfirma jobber med å utvikle en kode til en ny elektronisk vannautomat med vanntank og en enkel LCD-skjerm. De er i startfasen og ønsker å lage en enkel variant til å begynne med. I koden nedenfor har de definert en klasse som styrer vannautomaten. Automaten skal slippe ut 0,5 liter vann fra en kran når brukeren trykker på en knapp. Automaten skal også følge med på hvor mye det er igjen på tanken. Maskinen har ingen sensor som måler hvor mye vann det er igjen på tanken, så for å vite hvor mye som er igjen, må maskinen regne ut innholdet basert på hvor mye kranen har sluppet ut siden sist noen fylte på tanken. Det viktigste i dette eksemplet er at metodene i klassen passer på at verdien i innhold aldri går under 0. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// _vannautomat.js export class Vannautomat { constructor(kapasitetILiter) { this.kapasitet = kapasitetILiter this.innhold = kapasitetILiter } tappVann(liter) { let innholdFoer = this.innhold this.innhold = this.innhold - liter if (this.innhold < 0) { this.innhold = 0 } return innholdFoer - this.innhold } beskrivVanninnhold() { return `Det er ${this.innhold} av ${this.kapasitet} liter på tanken` } lagBeskjedTilSkjerm() { if (this.innhold <= 0) { return "Tanken er tom. Be drift om påfyll." } else { return this.beskrivVanninnhold() } } }


Objektorientert programmering

Nedenfor ser vi et testgrensesnitt som programmererne har laget for å teste en lignende vannautomat uten å måtte laste programvaren over på den fysiske automaten. Testgrensesnittet simulerer skjermen og knappene på automaten. Vanlige brukere har én knapp de kan trykke på. De er ennå tidlig i utviklingen av produktet, så automaten kan bare slippe ut vann i en forhåndsbestemt dose på 0,5 liter. Teknikere kan låse opp et kontrollpanel og trykke på to knapper. Den ene trykker de på når de har tømt tanken, den andre når de har fylt den helt opp. 1 <!-- vannautomat-testgrensesnitt.svelte --> 2 <script> 3 import { Vannautomat } from "./_vannautomat.js" 4 5 let minVannautomat = new Vannautomat(5) 6 let hvorMyeVannSomKomUtSist = 0 7 let standardVannmengde = 0.5 8 </script> 9 10 <h1>Vannautomat</h1> 11 12 <h2>Synlig for alle</h2> 13 <p>{minVannautomat.lagBeskjedTilSkjerm()}</p> 14 15 <p>Det kom ut {hvorMyeVannSomKomUtSist} liter sist</p> 16 17 <button 18 on:click={() => { 19 hvorMyeVannSomKomUtSist = minVannautomat.tappVann(standardVannmengde) 20 // Sender signal om å oppdatere visningen 21 minVannautomat = minVannautomat 22 }} 23 > 24

Tapp {standardVannmengde} liter

25 </button> 26 27 <h2>Synlig for teknikere</h2> 28 <button on:click={() => (minVannautomat.innhold = 0)}>Tanken er tømt</button> 29 <button on:click={() => (minVannautomat.innhold = minVannautomat.kapasitet)}> 30 Tanken er fylt 31 </button>

Legg merke til at vi bruker minVannautomat.tappVann for å tappe vann og oppdatere innholdet. Derfor måtte vi skrive minVannautomat = minVannautomat i den påfølgende linjen for å sørge for at Svelte oppdaterer visningen, slik vi lærte i delkapittel 4.2. Vi trenger derimot ikke å si fra om at hvorMyeVannSomKomUtSist har fått ny verdi, for den sto på venstre side av likhetstegnet i den foregående linjen. I metodene som er bundet til knappene for å tømme og fylle tanken har vi derimot oppdatert feltet minVannautomat.innhold direkte for å demonstrere forskjellen på å oppdatere med metoder og å skrive direkte til feltet. Vi kunne også ha laget metoder for å tømme og fylle, men da måtte vi også ha skrevet minVannautomat = minVannautomat etter å ha kalt på disse metodene.

73


74

Kapittel 4

METODER KAN HUSKE FORHOLDSREGLER OG TILBY EKSTRAFUNKSJONER En viktig fordel med å overlate oppdateringer til metoder er at de kan huske på forholdsregler for programmereren: Uten metoden tappVann ville det ha vært fort gjort å skrive minVannautomat.innhold = minVannautomat.innhold 0.5 et sted i koden uten å tenke på at innholdet kan gå under null. Etter at tanken hadde vært tømt og deretter fylt på igjen, kunne brukerne da ha fått se tekst som «–0,5 av 5 liter på tanken» på skjermen. Den ekstra koden inni en metode kan i tillegg til å forhindre feil legge til rette for forbedringer som programmereren ikke hadde tenkt på. Metoden tappVann kunne for eksempel ha inneholdt noen kodelinjer som sendte en tekstmelding til driftsavdelingen dersom vanninnholdet gikk under et visst nivå, slik at de visste at det var på tide å fylle på tanken. DISKUTER

Hva er fordelene med å oppdatere med likhetstegnet? Hva er fordelen med å oppdatere med metoder fulgt av variabelnavn = variabelnavn? Kan det være en fordel å oppdatere med metoder hver gang, selv om det krever en ekstra kodelinje?

DISKUTER

Sett at du skulle legge til støtte som gjorde at det ble sendt en tekstmelding til et visst nummer når maskinen var tom. Hvor synes du det hadde vært best å oppgi telefonnummeret meldingen skulle sendes til? Som en parameter til tappVann, for eksempel tappVann(0.5, "12345678"), som en parameter til konstruktøren, for eksempel new Vannautomat(5, "12345678"), eller et helt annet sted?

V=G·h G

h

FELTER SOM INNEHOLDER ANDRE OBJEKTER Det er ofte nyttig at et objekt kan inneholde felter med et objekt av en annen klasse. Hvis vi for eksempel allerede har laget en klasse som gjør deler av jobben en annen klasse skal gjøre, kan vi la denne andre klassen gjøre deler av den eksisterende klassens jobb. En sylinder er en romfigur. Den har en sirkel som grunnflate og en høyde. I følgende fil definerer vi klassen Sylinder i tillegg til å lime inn klassen Sirkel som vi brukte i delkapittel 4.2. Første parameter i konstruktøren til Sylinder er grunnflaten, altså et sirkel-objekt, som lagres i feltet grunnflate. Høyden er bare et vanlig tall.


Objektorientert programmering

1 // _klasser.js 2 export class Sirkel { 3 constructor(radius) { 4 this.radius = radius 5

}

6 7

regnUtAreal() {

8 return Math.PI * this.radius * this.radius 9 } 10 11

regnUtOmkrets() {

12 return 2 * Math.PI * this.radius 13 } 14 } 15 16 export class Sylinder { 17 constructor(grunnflate, hoeyde) { 18 this.grunnflate = grunnflate 19 this.hoeyde = hoeyde 20 } 21 22

regnUtVolum() {

23 return this.grunnflate.regnUtAreal() * this.hoeyde 24 } 25 26

regnUtAreal() {

27 let grunnflateareal = this.grunnflate.regnUtAreal() 28 /**

29 * Det er en "grunnflate" både oppe og nede på sylinderen, 30 * derfor må vi ta med begge i arealet 31 */

32 let antallGrunnflater = 2 33 // Dette er arealet som går langs siden av sylinderen 34 let arealUtenomGrunnflate = this.grunnflate.regnUtOmkrets() * this.hoeyde 35 return arealUtenomGrunnflate + grunnflateareal * antallGrunnflater 36

}

37 }

Som vi kan se av koden, er Sylinder-klassen ganske lat: Den lar Sirkel regne ut sitt eget areal og sin egen omkrets og bidrar selv bare med høyden i ny og ne. Nedenfor ser du et grensesnitt der vi kan leke oss litt med ulik høyde og radius for sylinderen. 1 <!-- sylinder-test.svelte --> 2 <script> 3 import { Sirkel, Sylinder } from

"./_klasser.js"

4 5 let minSylinder = 6 /* 7 * Her oppretter vi sirkelen og angir den som argument. 8 * Vi lagrer den ikke til noen variabel. 9 * Det går greit ettersom vi ikke trenger å vise til den senere. 10 */ 11 new Sylinder(new Sirkel(2), 3) 12 </script> 13 14 <div>

75


76

Kapittel 4

15 <label> 16 Radius:

17 <input type="number" bind:value={minSylinder.grunnflate.radius}

/>

18 </label> 19 <label> 20 Høyde: 21 <input type="number" bind:value={minSylinder.hoeyde} /> 22 </label> 23 </div> 24 25 <p> 26 27 28 </p>

Overflatearealet til sylinderen er {minSylinder.regnUtAreal()}. <br /> Volumet er {minSylinder.regnUtVolum()}.

Legg merke til at vi henter ut grunnflatens radius med dot-notasjon, slik: minSylinder.grunnflate.radius. EKSTRASTOFF

Polymorfisme Vi kunne ha brukt et objekt av en hvilken som helst klasse som grunnflate i Sylinder, så lenge klassen vi brukte, inneholdt metodene regnUtAreal og regnUtVolum. Metodene regnUtVolum og regnUtAreal i Sylinder ville ha kjørt og gitt riktige resultater fordi formlene for volum og overflateareal ikke er avhengige av formen på grunnflaten, bare omkrets og areal. Brukergrensesnittet i sylinder-test.svelte er riktignok tett knyttet til akkurat sirkler, så vi hadde nok måttet lage et nytt brukergrensesnitt om vi ønsket å eksperimentere med andre grunnflater. Muligheten til å bruke objekter av ulike klasser så lenge de definerer de forventede metodene, kalles for polymorfisme (som betyr at noe kan opptre i forskjellige former). Vi går ikke videre inn på polymorfisme i denne boka, men dersom du fortsetter med objektorientert programmering senere, vil du sannsynligvis komme borti ordet igjen.

HOLDE OVERSIKTEN OVER ET BIBLIOTEK MED ET OBJEKTORIENTERT PROGRAM Mange dataprogrammer har som jobb å holde styr på samlinger, deriblant bøker. I denne seksjonen skal vi lage et enkelt program for å holde styr på et bibliotek. I følgende kodesnutt lager vi de to klassene Forfatter og Bok. Forfatter-objektene skal lagre fornavnet og etternavnet til en forfatter i to separate felter. Et Bok-objekt skal lagre tittelen til en bok i feltet tittel, og i feltet forfattere skal det ligge et array med objekter av klassen Forfatter som representerer de forfatterne som var med på å skrive boka.


Objektorientert programmering

1 // _klasser.js 2 export class Bok { 3 constructor(tittel, forfattere) { 4 this.tittel = tittel 5 this.forfattere = forfattere 6 } 7 } 8 9 export class Forfatter { 10

constructor(fornavn, etternavn) {

11 this.fornavn = fornavn 12 this.etternavn = etternavn 13

}

14 }

Vi vil lage et brukergrensesnitt som viser frem bøkene vi har lagret, som en kulepunktliste, med bøkenes forfattere som underpunkter. I biblioteket vårt har vi de to bøkene Mamma Mø av Jukka Wieslander, Tomas Wieslander og Sven Nordquist og Gubben og katten av Sven Nordquist i en punktliste på en nettside. Listen skal inneholde tittelen på de to bøkene mellom to apostrofer etterfulgt av ordet «av». Forfatternes fulle navn skal presenteres i underpunkter. 1 <!-- bibliotek-grensesnitt.svelte --> 2 <script> 3 import { Bok, Forfatter } from "./_klasser.js" 4 5 // Oppretter et objekt for hver forfatter 6 let jukka = new Forfatter("Jukka", "Wieslander") 7 let tomas = new Forfatter("Tomas", "Wieslander") 8 let sven = new Forfatter("Sven", "Nordquist") 9 10 // Oppretter et array som holder på bøkene 11 let boksamling = [ 12 new Bok("Mamma Mø", [jukka, tomas, sven]), 13 new Bok("Gubben og katten", [sven]), 14 ] 15 </script> 16 17 <h1>Biblioteket mitt</h1> 18 <ul> 19

{#each boksamling as bok}

20 <li> 21 "{bok.tittel}" av 22 <ul> 23 {#each bok.forfattere as forfatter} 24 <li>{forfatter.fornavn} {forfatter.etternavn}</li> 25 {/each} 26 </ul> 27 </li> 28 {/each} 29 </ul>

77


78

Kapittel 4

FORBEDRE BIBLIOTEKET VED Å LAGE EN KOBLING FREM OG TILBAKE MELLOM FORFATTERE OG BØKER I biblioteket vi nettopp laget, var det bare mulig å finne ut hvilke forfattere som hadde skrevet en bok. Hvis vi likte en forfatter godt, var det ikke mulig å finne ut hvilke bøker vedkommende hadde skrevet. Vi kan bruke metoder som kan hjelpe oss med dette. Vi legger til et felt i Forfatter som heter verker. Dette er et array med Bok-objekter, altså de bøkene som forfatteren har vært med på å skrive. Vi ønsker å oppnå følgende: Hvis et gitt Forfatter-objekt er å finne i feltet forfattere i et Bok-objekt, skal Bok-objektet også være å finne i feltet verker i Forfatter-objektet (og omvendt). Dette krever at vi legger til metoder som legger til forfatteren. Nedenfor ser du hvordan vi kan endre klassene for å løse oppgaven. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

// _klasser.js export class Bok { constructor(tittel) { this.tittel = tittel this.forfattere = [] } leggTilForfatter(forfatter) { let finnesAlleredeIForfattere = this.forfattere.includes(forfatter) // Ikke legg til forfatter hvis vedkommende allerede er i listen if (!finnesAlleredeIForfattere) { this.forfattere.push(forfatter) /* * Legg til boka i forfatterens verker. * * Forfatter-klassens "leggTilVerk"-metode * kommer til å forhindre at vi legger til boka dobbelt, * så vi trenger ikke å sjekke om den allerede finnes */ forfatter.leggTilVerk(this) } } } export class Forfatter { constructor(fornavn, etternavn) { this.fornavn = fornavn this.etternavn = etternavn this.verker = [] } leggTilVerk(verk) { let erIVerker = this.verker.includes(verk) // Ikke legg til verk hvis det allerede er i listen if (!erIVerker) { this.verker.push(verk) /* * Legg til forfatteren i bokas forfatterliste. * * Bok-klassens "leggTilForfatter"-metode * kommer til å forhindre at vi legger til forfatteren dobbelt, * så vi trenger ikke å sjekke om forfatteren allerede finnes */ verk.leggTilForfatter(this) } } }


Objektorientert programmering

Her har det dukket opp to nye metoder: leggTilForfatter, som legger til en forfatter i en boks forfatterliste leggTilVerk, som legger til en bok i forfatterens verksliste

• •

Begge to sjekker først at det de skal legge til, ikke allerede ligger i listen. Hvis det allerede ligger i listen, avsluttes metoden. Hvis ikke, legger de til det de skal, samtidig som de kaller på den tilsvarende metoden til motparten, slik at en tilbakekobling opprettes hvis den ikke allerede finnes. Konstruktøren til Bok tar ikke lenger forfattere som parameter, og derfor må vi legge til alle verker og forfattere ved hjelp av disse to metodene. I disse metodene bruker vi this på en måte vi ikke har sett hittil. Vi bruker ordet for å sende objektet i sin helhet som et argument til en metode i den andre klassen. Det ser litt snålt ut, men når vi tenker over det, har ikke objektet noe annet alternativ hvis det skal vise til hele seg: Det vet jo ikke hvilket variabelnavn det skal få når det brukes. Nedenfor ser du et oppdatert grensesnitt for biblioteket. Der tester vi både leggTilForfatter og leggTilVerk. I slutten av visningen har vi lagt til en liste over forfattere og bøkene de har vært med på, for å sjekke at koblingene går begge veier. Vi kan se for oss at det som skjer i script-delen, simulerer bibliotekarer som fyller inn litt og litt informasjon etter hvert som de får den. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

<!-- bibliotek-grensesnitt.svelte --> <script> import { Bok, Forfatter } from "./_klasser.js" // Bibliotekarene legger inn Mamma mø let mammaMoe = new Bok("Mamma Mø") let jukka = new Forfatter("Jukka", "Wieslander") let tomas = new Forfatter("Tomas", "Wieslander") mammaMoe.leggTilForfatter(jukka) mammaMoe.leggTilForfatter(tomas) // Så legger de inn Gubben og katten let sven = new Forfatter("Sven", "Nordquist") let gubbenOgKatten = new Bok("Gubben og katten") gubbenOgKatten.leggTilForfatter(sven) // Så oppdager de at Sven også var med på Mamma Mø sven.leggTilVerk(mammaMoe) let boksamling = [mammaMoe, gubbenOgKatten] let forfattere = [jukka, tomas, sven] </script> <h1>Biblioteket mitt</h1> <h2>Bøker</h2> <ul> {#each boksamling as bok} <li> "{bok.tittel}" av <ul> {#each bok.forfattere as forfatter} <li>{forfatter.fornavn} {forfatter.etternavn}</li> {/each} </ul> </li> {/each} </ul> <h2>Forfattere</h2> <ul> {#each forfattere as forfatter}

79


80

Kapittel 4

43 44 45 46 47 48 49 50

<li>{forfatter.fornavn} {forfatter.etternavn} har skrevet:</li> <ul> {#each forfatter.verker as verk} <li>"{verk.tittel}"</li> {/each} </ul> {/each} </ul>

4.4 Arv

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 bruker vi 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// _hunder.js export class Hund { constructor(navn) { this.navn = navn } formulerPresentasjon() { return `Jeg heter ${this.navn}. Voff!` } } export class HundSomKanEngelskOgNorsk extends Hund { formulerPresentasjonPaaEngelsk() { return `My name is ${this.navn}. Woof!` } }

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.


Objektorientert programmering

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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

<!-- hund-med-arv.svelte --> <script> import { Hund, HundSomKanEngelskOgNorsk } from "./_hunder.js" let tospraakligHund = new HundSomKanEngelskOgNorsk("Rover") let enspraakligHund = new Hund("Passop") </script> <h1>Hei, hund!</h1> <div> {tospraakligHund.formulerPresentasjon()} </div> <h2>Hello, dog!</h2> <div> {tospraakligHund.formulerPresentasjonPaaEngelsk()} </div> <h1>Hei, mindre verdensvante hund</h1> <div>{enspraakligHund.formulerPresentasjon()}</div>

81


82

Kapittel 4

Når en 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, kaller vi for superklassen til den nye klassen, og derfor 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-». Altså er subklassene i den nedre delen av slektstreet, mens superklassene er høyere oppe. Til forskjell fra menneskene har en subklasse bare én forelder: En subklasse kan bare arve fra én superklasse. En superklasse kan derimot ha flere subklasser. Slektstreet til det norske kongehuset.

Maud

Haakon VII Olav V

Märtha

Ragnhild

Harald V

Sonja

Astrid

Mette-Marit

Haakon

Märtha Louise

Ari Behn

Marius

Ingrid Alexandra

Maud Angelica

Sverre Magnus

Leah Isadora Emma Tallulah

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.

Arv, subklasser og superklasser For at en klasse Ny skal arve en annen klasse Gammel, skriver vi: class Ny extends Gammel når vi oppretter klassen Ny. Når klassen Ny arver en 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. For eksempel finnes det hunder som bare kan snakke engelsk, og vi vil representere dem med klassen HundSomBareKanEngelsk. For hunder av denne klassen skal det


Objektorientert programmering

være slik at metoden formulerPresentasjon fører til at hunden 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 formulerPresentasjon() { 7 return `My name is ${this.navn}. Woof!` 8 } 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, slik vi kan 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 naturlig å redefinere konstruktøren. Kanskje legger vi til flere felter, eller kanskje vi bare ønsker å sette de eksisterende feltene i en annen rekkefølge. Vi går tilbake til kontaktlisten vi jobbet med tidligere, for å vise hvordan en redefinert konstruktør kan være nyttig. I mange kulturer er det slik at slektsnavnet sies før ­fornavnet. For eksempel sier koreanere og japanere slektsnavnet sitt først når de sier

83


84

Kapittel 4

hele navnet sitt. Vi kan lage en subklasse for folk fra kulturer der dette er standarden, KontaktMedSlektsnavnFoerst. 1 2 3 4 5 6 7 8 9 10

// _kontakt.js export class Kontakt { constructor(fornavn, etternavn, epostadresse) { this.fornavn = fornavn this.etternavn = etternavn this.epostadresse = epostadresse }

settSammenFulltNavn() { return `${this.fornavn} ${this.etternavn}` 11 } 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

lagVisittkort() { return `${this.settSammenFulltNavn()}, e-post: ${this.epostadresse}` } } export class KontaktMedSlektsnavnFoerst extends Kontakt { constructor(slektsnavn, gittNavn, epostadresse) { super(gittNavn, slektsnavn, epostadresse) } settSammenFulltNavn() { return `${this.etternavn} ${this.fornavn}` } }

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 2 3 4 5 6 7 8

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

let kontakter = [ new Kontakt("Ole", "Olsen", "ole.olsen@example.no"), new Kontakt("Kari", "Nordmann", "kari.nordmann@example.no"), new KontaktMedSlektsnavnFoerst("Hong", "Gil-Dong", "gildong@example.kr"), 9 new KontaktMedSlektsnavnFoerst("Yamada", "Taro", "yamada.taro@example.jp"), 10 ] 11 </script> 12 13 14 15 16 17 18

<h1>Mine kontakter</h1> <ul> {#each kontakter as kontakt} <li>{kontakt.lagVisittkort()}</li> {/each} </ul>


Objektorientert programmering

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.

85


86

Kapittel 4

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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

// _luksusautomat.js import { Vannautomat } from "./_vannautomat.js" export let DOSER_I_EN_NY_PATRON = 8 export class Luksusautomat extends Vannautomat { constructor(kapasitetILiter) { super(kapasitetILiter) this.kullsyredoser = DOSER_I_EN_NY_PATRON } beskrivVanninnhold() { return `${super.beskrivVanninnhold()} med ${ this.kullsyredoser } kullsyredoser igjen` } tappVannMedKullsyre(liter) { this.kullsyredoser = this.kullsyredoser - 1 if (this.kullsyredoser < 0) { this.kullsyredoser = 0 } return this.tappVann(liter) } }

Filen importerer klassen Vannautomat fra filen der den opprinnelig var 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 trenger å 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.


Objektorientert programmering

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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

<script> import { Luksusautomat, DOSER_I_EN_NY_PATRON } from "./_luksusautomat.js" let automat = new Luksusautomat(5) let sisteVannmengde = 0 let standardVannmengde = 0.5 </script> <h1>Vannautomat</h1> <h2>Synlig for alle</h2> <p>{automat.lagBeskjedTilSkjerm()}</p> <p>Det kom ut {sisteVannmengde} liter sist</p> <button on:click={() => { sisteVannmengde = automat.tappVann(standardVannmengde) automat = automat }} > Tapp {standardVannmengde} liter </button> <button on:click={() => { sisteVannmengde = automat.tappVannMedKullsyre(standardVannmengde) automat = automat }} > Tapp {standardVannmengde} liter vann med kullsyre </button> <h2>Synlig for teknikere</h2> <button on:click={() => (automat.innhold = 0)}> Tøm tank </button> <button on:click={() => (automat.innhold = automat.kapasitet)}> Fyll tank </button> <button on:click={() => (automat.kullsyredoser = DOSER_I_EN_NY_PATRON)}> Bytt kullsyrepatron </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.

87


88

Kapittel 4

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 dyrets art, lyden det lager, samt individets navn (altså navnet på den enkelte kua eller frosken). Dyrene skal kunne presentere seg 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 fargen på kuas pels. Siden frosker ikke har pels, skal ikke Frosk 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.


Objektorientert programmering

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 uansett hva vi gjør, 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 2 3 4 5 6 7 8 9 10

// _dyr.js class Dyr { constructor(individnavn, artsnavn, lyd) { this.individnavn = individnavn this.artsnavn = artsnavn this.lyd = lyd }

formulerPresentasjon() { return `Jeg heter ${this.individnavn} og er en ${this.artsnavn}. ${this.lyd}!` 11 } 12 } 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

export class Frosk extends Dyr { constructor(individnavn, hoppehoeyde) { super(individnavn, "frosk", "Rrrbit") this.hoppehoeyde = hoppehoeyde } formulerPresentasjon() { return `${super.formulerPresentasjon()} Jeg kan hoppe ${ this.hoppehoeyde } meter høyt.` } } export class Ku extends Dyr { constructor(individnavn, pelsfarge) { super(individnavn, "ku", "Mø") this.pelsfarge = pelsfarge } formulerPresentasjon() { return `${super.formulerPresentasjon()} Jeg har ${this.pelsfarge} pels.` } }

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

<!-- dyr-grensesnitt.svelte --> <script> import { Frosk, Ku } from "./_dyr.js" let dyr = [ new Ku("Dagros", "hvit og brunflekkete"), new Frosk("Franklin", 1.5), ] </script> {#each dyr as enkeltdyr} <div> {enkeltdyr.formulerPresentasjon()} </div> {/each}

89


90

Kapittel 4

Det gir ikke mening å opprette et objekt av klassen Dyr fordi vi ikke kan finne et standard-dyr 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. DISKUTER

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? 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.


Objektorientert programmering

Arverekkefølge og kunstige samleklasser En subklasse bør være et spesialtilfelle av superklassen, slik at subklassen er superklassen med litt ekstrafunksjonalitet. Hvis vi føler 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 vi ikke har muligheten til å si fra til programmeringsspråket at det ikke skal opprettes et objekt av klassen. Det kan kanskje være nyttig å huske at disse klassene kalles for abstrakte klasser, men det er viktigere å forstå teknikken enn å huske akkurat hva vi kaller dem.

91


92

Kapittel 4

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 burde 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// _bil.js export class Bil { constructor(registreringsnummer, kilometerpris, timepris) { this.registreringsnummer = registreringsnummer this.kilometerpris = kilometerpris this.timepris = timepris } /** * Regner ut prisen for en tur og oppdaterer * hvor mye det er i bilens energilager etter * turen. * * @param kilometer turlengden i kilometer * @param timer leievarigheten i timer * @returns prisen i kroner for turen */ utfoerTur(kilometer, timer) { this.reduserEnergilager(kilometer) return this.regnUtTurpris(kilometer, timer) } regnUtTurpris(kilometer, timer) { return kilometer * this.kilometerpris + timer * this.timepris } }

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 det sammen med antall kjørte timer ganger timeprisen, så 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


Objektorientert programmering

ikke hva slags energilager subklassene har, og 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

// _elbil.js import { Bil } from "./_bil.js" export class Elbil extends Bil { constructor(bildata) { super(bildata.registreringsnummer, bildata.timepris, bildata.kilometerpris) this.batterikapasitetIWattTimer = bildata.batterikapasitetIWattTimer this.wattTimerPerKilometer = bildata.wattTimerPerKilometer this.energinivaaIWattTimer = 0 } ladFullt() { this.energinivaaIWattTimer = this.batterikapasitetIWattTimer } reduserEnergilager(kilometer) { let stroemBruktPaaTuren = this.regnUtStroemforbruk(kilometer) this.reduserLadning(stroemBruktPaaTuren) } regnUtStroemforbruk(kilometer) { return this.wattTimerPerKilometer * kilometer } reduserLadning(wattTimer) { this.energinivaaIWattTimer = this.energinivaaIWattTimer - wattTimer } }

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 2 3 4 5 6 7 8 9 10 11

// Kortfattet, men vanskelig å se om tallene står på rett plass let bilenMin = new Elbil("EV73889", 1.9, 58, 58000, 173) // Mange bokstaver, men nå trenger man ikke å huske rekkefølgen, bare rett navn let bilenMin = new Elbil({ registreringsnummer: "EV73889", kilometerpris: 1.9, timepris: 58, batterikapasitetIWattTimer: 58000, wattTimerPerKilometer: 173, })

Tommelfingerregelen er at vi bruker dette objekt-trikset når vi har flere enn tre–fire parametere. Konstruktøren til Elbil forventer at objektet den får, skal inneholde to biter informasjon utenom de tre den sender til superklassens konstruktør, batterikapasitetIWattTimer, som oppgir størrelsen på batteriet i wattimer (batterikapasiteten til elbiler er ofte oppgitt i kilowattimer, kWh;

93


94

Kapittel 4

→ 1 kilowattime = 1000 wattimer). wattTimerPerKilometer er enheten for batteriforbruk hos 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 på 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 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 registreringsnummer: "EV73889", 7 kilometerpris: 1.9, 8 timepris: 58, 9 batterikapasitetIWattTimer: 58000, 10 wattTimerPerKilometer: 173, 11 }) 12 13 let ladningFraFabrikk = bilenMin.energinivaaIWattTimer 14 bilenMin.ladFullt() 15 let ladningEtterFulladning = bilenMin.

energinivaaIWattTimer 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

let kilometer = 30 let timer = 2 let kostnadForTur = bilenMin.utfoerTur(kilometer, timer) </script> <h1>Bilen</h1> <p> Vi har en bil med registreringsnummer {bilenMin.registreringsnummer}. Hvis du vil kjøre den, koster det {bilenMin.kilometerpris} kroner per kilometer og {bilenMin.timepris} kroner per time. </p> <p> Da vi fikk bilen, hadde den {ladningFraFabrikk} Wh på batteriet. Etter at vi hadde ladet den, hadde den {ladningEtterFulladning} Wh på batteriet. </p> <p> Så kjørte vi en tur som tok {timer} timer og var {kilometer} kilometer lang. Det kostet {kostnadForTur} kroner. Etter turen var ladningen {bilenMin.energinivaaIWattTimer} Wh. </p>


Objektorientert programmering

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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

// _fossilbil.js import { Bil } from "./_bil.js" class Fossilbil extends Bil { constructor(bildata) { super(bildata.registreringsnummer, bildata.timepris, bildata.kilometerpris) this.bensintankkapasitet = bildata.bensintankkapasitet this.literDrivstoffPerKilometer = bildata.literDrivstoffPerKilometer this.drivstofftype = bildata.drivstofftype this.drivstoffmengde = 0 } fyllTankenFull() { this.drivstoffmengde = this.bensintankkapasitet } reduserEnergilager(kilometer) { this.reduserDrivstoff(this.regnUtDrivstofforbruk(kilometer)) }

regnUtDrivstofforbruk(kilometer) { 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)

95


96

Kapittel 4

Bruke ett 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 situasjoner 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 7 8 9 10

registreringsnummer: "EV73889", kilometerpris: 1.9, timepris: 58, batterikapasitetIWattTimer: 58000, 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 dokstrengen 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.


Objektorientert programmering

EKSTRASTOFF

Ulike programmeringsparadigmer Det er ikke slik at vi alltid må bruke objektorientert programmering når vi skal lage dataprogrammer. Objektorientert programmering er bare ett av mange programmeringsparadigmer. Et programmeringsparadigme er en overordnet filosofi og metodikk for å beskrive hva datamaskiner skal gjøre. Det mest utbredte alternativet til objektorientert programmering er funksjonell programmering. I funksjonelle programmeringsspråk er det ikke tillatt å blande data og funksjoner, slik vi gjør i objektorientert programmering. Det er ikke engang lov til å gi nye verdier til variabler etter at de er opprettet. Når vi bruker funksjonell programmering, må vi arbeide mer mens vi skriver og leser koden, men til gjengjeld kan vi ofte bevise matematisk at funksjonell kode er uten uforutsette feil. Objektorienterte språk tillater mye mer. Programmererne kan ta snarveier når de (tror de) vet at ting går bra, men de må også ta større ansvar for at koden er uten feil. For de fleste er det mer motiverende å starte med objektorientert programmering fordi det er romsligere og gir programmererne større frihet enn funksjonell programmering. For datamaskinen har det til syvende og sist lite å si. Koden kan være like rask uansett hvilket paradigme den følger, så lenge den er uten feil. Både objektorientert programmering og funksjonell programmering er utbredt i Norge og i resten av verden i dag. Det er vanlig at programmerere behersker begge stiler, men foretrekker en av dem. JavaScript støtter objektorientert programmering, men det er også mulig å legge seg på en funksjonell linje ved å begrense hvilke virkemidler man bruker (blant annet ved ikke å bruke objekter). Dermed kan man eksperimentere med hvilken stil man foretrekker uten å måtte lære et nytt programmeringsspråk. Vi tar ikke for oss funksjonell programmering i denne boka, men du kan søke etter «functional programming JavaScript» på nettet dersom du skulle være interessert.

4.5 Objektorientert modellering

Dersom vi skal beskrive strukturen til et objektorientert dataprogram med tekst, blir det mange ord, og vi må ofte henvise frem og tilbake. For å unngå dette kan vi i stedet tegne klassene og koblingene mellom dem. Denne tegnemåten kalles for objekt­ orientert modellering. I dette delkapittelet ser vi nærmere på noe som heter klassediagrammer og hvordan vi kan tegne disse. I disse diagrammene representeres klassene av bokser, og inni boksene står klassenavn, felter og metoder. Mellom boksene går det piler og streker, som viser hvordan klassene forholder seg til hverandre. Klassediagrammer er en del av et tegnespråk som kalles for Unified Modeling Language (UML), som også inneholder oppskrifter på hvordan vi kan tegne andre typer diagrammer. Det finnes tegneprogrammer som er laget for å tegne UML-diagrammer, deriblant klassediagrammer. Et gratis og mye brukt alternativ er https://app.diagrams. net/, som du kan bruke i nettleseren eller laste ned til datamaskinen.

97


98

Kapittel 4

EKSTRASTOFF

Definisjonen av objektorientert modellering er romslig Definisjonen av objektorientert modellering er at man bruker klassediagrammer til å planlegge dataprogrammet før man skriver det. Derfor kan vi strengt ikke kalle det objektorientert modellering dersom vi tegner klassediagrammer for kode som allerede er skrevet. Hvis vi lager tegningene etter at vi har begynt å lage koden, sier vi bare at vi lager et klassediagram. Teknikkene vi bruker for å tegne, er de samme, forskjellen ligger i om systemet finnes i koden eller i hodet vårt. Før i tiden var det vanlig å planlegge datasystemet i detalj før man begynte å kode, men dette har man av ulike årsaker gått bort fra. Dette kan du lese mer om i kapittel 6. Programmerere bruker likevel klassediagrammer for å forklare og diskutere koden de har laget med brukere og medarbeidere, og derfor er det nyttig å lære seg å tegne klassediagrammer.

TEGNE EN KLASSE Den grunnleggende formen innenfor klassediagrammer er et rektangel som ligner en tabell med én kolonne og tre rader. Et slikt rektangel representerer en klasse. Nedenfor ser du klassediagrammet for klassen Hund etter at den fikk metoden formulerPresentasjon (se koden på side XXX). Hund navn: string Hund(navn: string) formulerPresentasjon(): string

Den øverste raden inneholder alltid klassenavnet, midtstilt. Den midterste raden viser alle feltene som objektet inneholder. Siden Hund bare har ett felt – navn – skriver vi opp dette. Etter navnet til feltet skriver vi et kolon fulgt av feltets datatype. Fordi det er meningen at feltet navn skal inneholde en streng, skriver vi «string». Den nederste raden lister opp metodene som klassen inneholder. Hund inneholder to metoder: konstruktøren og formulerPresentasjon. Selv om konstruktøren har navnet constructor i koden, skal vi ifølge reglene i UML skrive klassenavnet fordi vi skriver klassenavnet og ikke «constructor» når vi kaller på konstruktøren. Mellom metodenes parenteser oppgir vi parameternavnene. Akkurat som med feltene skriver vi et kolon og parameterens datatype. Konstruktøren til Hund tar én parameter, navn, som skal være en streng. For alle metoder – utenom konstruktøren – skal vi også oppgi datatypen til returverdien, altså hvilken datatype den verdien som står etter return i koden, har. Siden formulerPresentasjon returnerer en streng, skriver vi kolon fulgt av «string». Vi skal ikke oppgi datatype for konstruktøren, men vi angir datatypen til parameterene.


Objektorientert programmering

EKSTRASTOFF

Klassediagram for klassen Sirkel Her ser du et klassediagram for Sirkel-klassen som vi laget tidligere i kapittelet (se koden på side XXX). Fordi datatypene som Sirkel opererer med, er tall, har feltene og returverdiene datatypen «number». Sirkel radius: number Sirkel(omkrets: number) regnUtAreal(): number regnUtOmkrets(): number

Dersom datatypen til et felt er et objekt av en annen klasse, skriver vi navnet på denne klassen. Nedenfor ser du hvordan klassen Sylinder (se koden på side XXX) bruker Sirkel som datatype der den forventer et objekt av klassen Sirkel. Sylinder grunnflate: Sirkel hoeyde: number Sylinder(grunnflate: Sirkel, hoeyde: number) regnUtAreal(): number regnUtVolum(): number

Dersom feltet inneholder et array av en datatype, skriver vi klammeparenteser etter datatypen. Nedenfor ser du et klassediagram for klassen Bok slik den fremsto i delkapittel 4.2. Feltet forfattere inneholder et array med objekter av klassen Forfatter, og derfor skriver vi at feltets datatype er «Forfatter[]». Siden metoden leggTilForfatter bare forventer én forfatter, som ikke er pakket inn i et array, har «Forfatter» ikke hakeparenteser etter seg i dette tilfellet. Bok tittel: string forfattere: Forfatter[] Bok(tittel: string) leggTilForfatter(forfatter: Forfatter): void

Du legger kanskje også merke til at funksjonen leggTilForfatter returnerer datatypen «void» (void er engelsk for «tomrom»), som er det vi skriver når funksjonen ikke returnerer noe i det hele tatt. Grunnen til at vi har skrevet dette for leggTilForfatter, er at det ikke står return noe sted i metoden, og derfor returnerer den heller ingen data. Hadde vi skrevet let a = bokenMin.leggTilForfatter(forfatterenMin), hadde variabelen a fortsatt hatt verdien undefined etter denne kodelinjen.

99


Kapittel 4

TEGNE EN ASSOSIASJON MELLOM KLASSER En assosiasjon er en rett strek mellom to klasser og betyr at det er et felt i den ene klassen som skal inneholde objekter av den andre klassen. Nedenfor ser du et klassediagram for Sylinder og Sirkel som viser at Sylinder og Sirkel er assosierte. Grunnen til at de er assosierte, er at feltet grunnflate i Sylinder skal inneholde en Sirkel som grunnflate. Sylinder

Sirkel grunnflate: Sirkel hoeyde: number

radius: number Sirkel(omkrets: number) regnUtAreal(): number regnUtOmkrets(): number

Sylinder(grunnflate: Sirkel, hoeyde: number) regnUtAreal(): number regnUtVolum(): number

For at leseren raskere skal kunne forstå hva assosiasjonen betyr, kan vi skrive et navn på assosiasjonen midtveis på streken. Dette navnet har ikke noe motsvar i koden, det bare forklarer hva forholdet mellom de to er. Nedenfor har vi skrevet inn «har grunnflate» på assosiasjonen. For å hjelpe leseren til å forstå hvilken retning assosiasjonen skal leses i, kan vi sette inn et svart pilhode som peker i den retningen. Her viser vi at Sylinder har grunnflate Sirkel og ikke omvendt. Sylinder

Sirkel radius: number Sirkel(omkrets: number) regnUtAreal(): number regnUtOmkrets(): number

har grunnflate grunnflate: Sirkel 1..1 0..* hoeyde: number ▲

100

Sylinder(grunnflate: Sirkel, hoeyde: number) regnUtAreal(): number regnUtVolum(): number

Vi kan også skrive inn antallsområdet (multiplicity på engelsk) på enden av assosiasjonen. I klassediagrammet ovenfor har vi satt inn antallsområde på koblingen mellom Sirkel og Sylinder. Grunnen til at vi har skrevet «1..1» ved enden nærmest Sirkel, er at en Sylinder skal ha minst én Sirkel som grunnflate, og den kan også ha høyst én Sirkel. I den andre enden har vi derimot skrevet «0..*». Minsteverdien er 0 fordi en sirkel ikke nødvendigvis må være koblet til en Sylinder – vi kan bruke klassen Sirkel på nyttige måter uten at den inngår i en Sylinder. Den høyeste verdien er *, og dette betyr «mange». Dette har vi skrevet fordi vi ser for oss at et Sirkel-objekt gjerne kan være grunnflaten til flere Sylinder-objekter uten at det gjør de enkle regneprogrammene vi laget med klassene, i denne omgang. Hva som er rett antallsområde, er ofte et spørsmål om intensjon, altså hvordan klassene er tenkt å brukes. Hvis Sirkel og Sylinder skulle brukes i et tegneprogram for to- og tredimensjonale figurer, hadde brukeropplevelsen kanskje blitt bedre dersom én Sirkel bare kunne være grunnflate for ett Sylinder-objekt (slik at man


Objektorientert programmering

kunne redigere hver sylinder uavhengig av de andre). I så fall hadde vi nok skrevet 0..1 som antallsområde i stedet for 0..*. Forhåpentligvis kan programmererne som bruker diagrammet, også oversette denne intensjonen til kode, for eksempel ved å sjekke om sirkelen allerede var brukt av en annen sylinder i konstruktøren.

EKSEMPEL Klassediagram for bibliotekprogrammet Nedenfor ser du et klassediagram for programmet vi laget for å holde styr på bøker og forfattere. Forfatter

Bok

Bok(tittel: string) leggTilForfatter(forfatter: Forfatter): void

har skrevet fornavn: string 1..* 0..* verker: Bok[]

tittel: string forfattere: Forfatter[]

Forfatter(fornavn: string, etternavn: string) leggTilVerk(verk: Bok): void

Vi har valgt å skrive at en forfatter må ha minst én bok i feltet verker, selv om det er teknisk mulig å opprette en forfatter uten bøker slik koden ser ut nå. Når vi skal skrive antallsområde, må vi tenke på (1) hva som er teknisk mulig, og (2) hvordan systemet helst bør virke. Tegningen her stemmer med alternativ 2: Forfattere bør ikke ligge inne i systemet dersom de ikke har skrevet en eneste bok, selv om de kan. Når vi er i tvil om hvilken verdi vi skal skrive fordi tiltenkt virkemåte og kode ikke stemmer overens, bør diagrammet stemme med intensjonen. Hvis koden vi har laget, ikke stemmer overens med diagrammet, bør vi skrive en kommentar til diagrammet og koden, eller aller helst skrive om koden, slik at den faktisk stemmer overens med diagrammet. Vi har skrevet at en bok må ha minst 0 forfattere, og vi mener også at dette er en riktig minsteverdi fordi det finnes bøker som ikke har en navngitt forfatter, for eksempel Bibelen. Det er ikke alle som lager bibliotekprogramvare, som har tenkt slik. Bærum kommunes bibliotek oppgir for eksempel at en forfatter som heter «Bibelen» har skrevet Bibelen, sannsynligvis fordi datasystemet deres krever at en bok har minst én forfatter når man registrerer den. Oslo kommunes bibliotek oppgir derimot ingen forfattere for Bibelen, sannsynligvis fordi datasystemet deres tillater at bøker har 0 forfattere.

Søkeresultater for «bibelen» på nettsidene til Bærum kommunes bibliotek. Legg merke til at «Bibelen» er oppgitt som forfatter for Bibelen og Apokryfe evangelier, akkurat som Are Kalvø og Kari Veiteberg er oppgitt som forfattere for andre bøker.

Resultatene for det samme søket på nettsidene til Oslo kommunes bibliotek, Deichman. Bare boka Bibelen: boken som formet vår kultur har en oppgitt forfatter.

101


102

Kapittel 4

TEGNE ARV For å vise at en klasse arver en annen, tegner vi en pil med et hult pilhode. Pilen skal peke på superklassen, altså den klassen som subklassen arver fra. Her ser du et klassediagram som viser Hund og subklassen HundSomKanEngelskOgNorsk, slik de fremsto i starten av delkapittel 4.3. Vi skriver bare inn de metodene og feltene som er nye. Når metode- eller felt-området står tomt, betyr det at subklassen ikke definerer noen nye felter eller metoder. Hund navn: string Hund(navn: string) formulerPresentasjon(): string

HundSomKanEngelskOgNorsk formulerPresentasjonPaaEngelsk(): string

Når vi redefinerer en metode, setter vi en hatt (^) foran navnet til metoden. Nedenfor har vi tegnet et diagram som viser forholdet mellom Hund, HundSomKanEngelskOgNorsk og HundSomBareKanEngelsk. Fordi HundSomBareKanEngelsk redefinerer metoden formulerPresentasjon, har vi satt en hatt foran navnet på denne metoden. Når en klasse har flere subklasser, er det vanlig å tegne pilene slik at alle ender opp i samme pilhode. Hund navn: string Hund(navn: string) formulerPresentasjon(): string

HundSomKanEngelskOgNorsk formulerPresentasjonPaaEngelsk(): string

HundSomBareKanEngelsk ^formulerPresentasjon(): string


Objektorientert programmering

Når superklassen er en abstrakt klasse, altså en klasse som vi ikke skal opprette objekter av, setter vi klassenavnet i kursiv. Her er klassediagrammet til klassene Dyr, Ku og Frosk som vi jobbet med i delkapittel 4.3. Dyr individnavn: string artsnavn: string lyd: string Dyr(individnavn: string, artsnavn: string, lyd: string) formulerPresentasjon(): string

Ku

Frosk

pelsfarge: string

hoppehoeyde: number

Ku(individnavn: string, pelsfarge: string) ^formulerPresentasjon(): string

Frosk(individnavn: string, hoppeheyde: number) ^formulerPresentasjon(): string

DISKUTER

Hvordan ville du tegnet klassediagrammet hvis du skulle laget flere klasser som skal arve fra den kunstige klassen Dyr?

103


104

Kapittel 4

в En klasse er oppskriften på en spesifikk datatype. En variabel som er opprettet

SAMMENDRAG

med utgangspunkt i en klasse, kalles et objekt. Funksjonene som ligger i en klasse, kalles metoder. Alle klasser må bruke konstruktør-metoden, som sørger for at dataene vi sender inn, blir lagret i riktig felt. Vi definerer en klasse med to felter slik: 1 class Klassenavn { 2 constructor(parameter1, parameter2) { 3 this.felt1 = parameter1 4 this.felt2 = parameter2 5 } 6 }

в Vi oppretter et objekt av en klasse slik:

let variabelnavn = new Klassenavn(argument1, argument2, …) в Vi henter ut en verdi fra et felt slik: variabelnavn.feltnavn. Det er god

skikk å starte navnet til et felt med et substantiv. в Vi kan endre verdien i et felt slik: objektnavn.feltnavn = nyVerdi. в En metode defineres inni et objekt med koden metodenavn (parametere)

{…}. Vi kaller på en metode ved å skrive objektnavn.metodenavn(argumenter). Det er god skikk å starte navnet til en metode med et verb. в Objekter bruker nøkkelordet this for å vise til seg selv i metoder, slik at meto-

den kan hente ut data fra felter, endre dataene i felter og kalle på andre metoder i samme objekt. For å hente ut verdien fra et felt i en metode skriver vi this. feltnavn. For å gi en ny verdi til et felt i en metode skriver vi this.feltnavn = nyVerdi. For å kalle på en metode i klassedefinisjonen skriver vi this. metodenavn(...argumenter). в Når vi oppdaterer innholdet i et objekt som styrer visningen, med en metode, for

eksempel objektnavn.metodenavn(), må vi skrive objektnavn = objektnavn i den påfølgende linjen for at Svelte skal forstå at det har skjedd en oppdatering. в For at en klasse Ny skal arve en annen klasse Gammel, skriver vi: class Ny

extends Gammel når vi oppretter klassen Ny. 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. I en subklasse kan vi

redefinere konstruktøren og andre metoder.


Objektorientert programmering

в Hvis subklassen trenger å kalle på en metode fra superklassen som den har rede-

finert, kan vi skrive super.metodenavn(...argumenter). в Dokumentasjonsstrenger (dokstrenger) er kommentarer vi skriver inn i koden for

å forklare en metode eller variabel. I kodeverktøy vises disse kommentarene når vi lar musepekeren hvile over navnet. Vi skriver dokumentasjonsstrenger slik: /** … */ в Objektorientert modellering er en måte å tegne klasser og koblingen mellom

dem på. Klassene tegnes som bokser. Hver boks inneholder navnet på klassen, felter og metoder. Etter feltenes navn skriver vi kolon etterfulgt av datatypen. Klassenavn feltnavn: datatype feltnavn: datatype … Klassenavn(feltnavn: datatype, ...) metodeMedParametre(parameter: datatype) metodeMedReturverdi(): datatypen til returverdien

в En assosiasjon er en rett strek mellom to klasser og betyr at et felt i den ene klas-

sen inneholder objekter av den andre klassen. Vi kan markere streken med et forklarende navn og gjerne med en pil som viser i hvilken retning vi skal lese assosiasjonen. в Vi tegner arv som en pil med et hult pilhode. Pilen skal gå fra subklassen og peke

mot superklassen.

105


6 Jobbe med IT-prosjekter KOMPETANSEMÅL I dette kapittelet skal du jobbe for å nå disse målene: в velge og bruke relevante systemutviklingsmetoder og -verktøy for samarbeid med andre в utforske og vurdere alternative løsninger for design og implementering av et program

Sett at læreren din deler dere inn i grupper på fire og gir dere følgende oppdrag uten flere instrukser: Lag en enkel webapplikasjon sammen. Da er det stor sannsynlighet for at dere må ta stilling til ett eller flere av spørsmålene nedenfor, enten med en gang eller underveis.

• • • • •

Hva skal dere lage, og hvorfor synes dere at dette er en god idé? Hvordan skal dere bli enige om et felles alternativ hvis dere har to eller flere konkurrerende ideer, både til hva dere skal lage, og til hvordan dere skal lage det? Hvordan skal dere sørge for at arbeidet blir jevnt fordelt? Hvordan skal dere holde oversikt over hva dere skal gjøre, hvem som skal gjøre hva, og hva som må være på plass før dere kan si dere ferdige med produktet? Hvordan skal dere jobbe med samme kode samtidig uten å gå i veien for hverandre?

I dette kapittelet lærer du hvordan du kan samarbeide med andre – både medarbeidere og oppdragsgivere – om å lage et dataprogram. Målet er at du på slutten av kapittelet skal kunne nok til å gjennomføre et lite prosjekt sammen med andre som også tar faget. Samtidig får du et innblikk i hvordan det er å jobbe med prosjekter i IT-bransjen.


Jobbe med IT-prosjekter

6.1 Produkter og prosjekter PRODUKTER Den som får en jobb (eller skaper sin egen arbeidsplass) innenfor IT, skal som regel lage ett eller annet. Dette ett eller annet kaller vi for produkt i dette kapittelet. Som du ser i listen nedenfor, viser ordet til det samme som det gjorde i kapittel 3: nettsider, apper, digitale tjenester og annen programvare. Eksempler på produkter:

• •

Et produkt kan være et dataprogram eller en digital tjeneste som man selger til en kunde (skoler, privatpersoner, bedrifter, offentlige etater og så videre). Dette programmet hjelper kunden med å utføre én eller flere oppgaver. Eksempler på slike programmer er Microsoft Word, Adobe Photoshop, pasientjournalsystemer til fastleger, timeføringssystemer på en arbeidsplass og bookingsystemer til restauranter. Et produkt kan være en applikasjon med abonnement eller reklame som inntektskilde, for eksempel Instagram, Finn.no, Spotify og Netflix. Et produkt kan være en del av et annet produkt: • Det kan være en mindre del av et av de produktene vi allerede har nevnt. De ulike delene av Spotify behandles for eksempel som om de var enkeltstående produkter. Noen har ansvar for å lage avspilleren, noen spillelistene, noen avspillingskøen og så videre. Til slutt sys disse produktene sammen til én stor pakke: Spotify-appen. • Det kan være programvare for fysiske produkter med elektronikk, for eksempel TV-er, biler, panteautomater, jagerfly og røntgenmaskiner.

PROSJEKTER Prosessen med å lage et produkt kalles et prosjekt. Når man bruker ordet prosjekt utenfor IT-bransjen, viser det som regel til en arbeidsprosess med en forhåndsbestemt tidsfrist og et tenkt sluttresultat. Prosjektdeltakerne skal for eksempel lage eller finne ut noe. Når deltakerne har oppnådd sluttresultatet, er prosjektet over. I IT-bransjen, derimot, har prosjekter sjelden en forhåndsbestemt tidsfrist der arbeidet slutter – produktet blir ofte utviklet videre etter at det er lansert. Det merker vi i hverdagen når vi for eksempel må oppdatere appene på mobilen. Prosjekter i IT-bransjen har sjelden en konkret sluttdato. Ofte fortsetter produktutviklingen etter lanseringen.

Start

Start

Slutt

Lansering

Oppdateringer

119


120

Kapittel 6

I IT-bransjen har man ofte tidsfrister underveis i et prosjekt. De som jobber på prosjektet, kan for eksempel ha blitt enige om tidspunkt der visse funksjoner skal være ferdige. Og når de har nådd et mål de har satt seg, oppstår det som regel behov for mer arbeid. Det kan være aktuelt å legge til flere funksjoner, ta bort gamle funksjoner, forbedre designet og brukervennligheten, fikse bugger og mye annet. Prosjektet kan sies å leve så lenge noen utvikler det videre. Kanskje deles prosjektet opp i flere prosjekter over tid. Et produkt kan leve i flere tiår etter at det først ble tatt i bruk, slik som Microsoft Windows og Google-søk. De fleste forbinder ordet bugg med insekter. I dataverden brukes ordet om feil i dataprogrammer.

Mappen som inneholder koden man jobber med i et prosjekt, kalles kodebasen.

EKSTRASTOFF

IT-prosjekter med fastsatt sluttresultat og sluttdato En sjelden gang har IT-prosjekter en bestemt sluttdato og et bestemt sluttresultat. Da vet de som står bak prosjektet, akkurat hva de trenger, slik at de kan sette en tidsfrist. Når et fysisk produkt som ikke har Internett-tilkobling, forlater fabrikken, er det som regel for sent å oppdatere programvaren. Programvaren må derfor være ferdig og feilfri før produktet skal lanseres. Det er for eksempel ikke mulig å oppdatere programvaren i en datastyrt komfyr etter at den har forlatt fabrikken. Fra 1950- til 1980-årene sparte mange programmerere på lagringsplassen i dataprogrammer ved å sette av bare to sifre til å lagre årstall (de to siste sifrene i stedet for alle fire). Da årsskiftet mellom 1999 og 2000 nærmet seg, måtte mange bedrifter ha ett eller flere såkalte Y2K-prosjekter for å sikre at dataprogrammene deres ikke krasjet fordi de plutselig trodde at de var i år 1900 igjen. I disse prosjektene visste bedriftene hva de trengte (sikring mot krasj), og når de måtte ha det (senest ved inngangen til 1. januar 2000).

TEAM De som utfører et prosjekt, kalles for et team (eller arbeidslag, lag eller gruppe). Et team er med andre ord to eller flere personer som samarbeider om å lage et produkt. I en bedrift er det vanlig å øke antall teammedlemmer hvis man over lang tid har for mye å gjøre. Når det er mindre å gjøre, tar man personer ut av teamet, og da flytter man dem som regel til andre team i bedriften. Et team har sjelden flere enn seks–sju medlemmer. Et team som har flere medlemmer enn det, må ofte bruke så mye tid på å koordinere og kommunisere at vinningen går opp i spinningen. Som regel er det én eller flere programmerere i teamet. Disse kalles utviklere. Dersom det teamet lager, har et brukergrensesnitt, trengs det interaksjonsdesignere som har spesialkompetanse på brukeropplevelse. Teamet har kanskje også en egen tester som kan spesielt mye om hvordan digitale produkter skal testes.


Jobbe med IT-prosjekter

I IT-bransjen jobber man med prosjekter i ett eller flere team. Et team kan ha ansvaret for hele eller deler av et prosjekt.

Det er vanlig at teammedlemmene veksler litt på oppgavene. Når det for eksempel er få oppgaver som har med interaksjonsdesign å gjøre, kan det være en fordel at interaksjonsdesigneren tar noen kodeoppgaver. Arbeidsdeling gjør teamet mer effektivt, for da kan alle bidra – også når det ikke er noe å gjøre innenfor akkurat deres spesialfelt. Arbeidsdeling forhindrer også at arbeidet med visse deler av koden stopper opp hvis et bestemt medlem skulle bli sykt eller slutte. Og selv om man liker noen oppgaver bedre enn andre, kan det være fint å holde kunnskapen ved like på flere områder.

OPPDRAGSGIVEREN Oppdragsgiveren er den eller de som har bestemt at produktet skal lages og som betaler for det.

• • •

Hvis man lager et produkt, for eksempel en nettside eller et dataprogram, på bestilling for en bedrift, er oppdragsgiveren bedriften. Hvis produktet lages innad i en bedrift, er oppdragsgiveren én eller flere i ledelsen. Hvis du skal gjennomføre prosjekt i dette faget, er oppdragsgiveren sannsynligvis læreren din eller deg selv.

121


122

Kapittel 6

6.2 Utviklingsmetoder

Sett at en oppdragsgiver har bestilt et produkt av et team. Hvordan skal teamet gå frem for å finne ut hva oppdragsgiveren vil ha, og deretter lage det? Det finnes to overordnede måter å gjennomføre et prosjekt på: fossefallsmetoder og smidige utviklingsmetoder.

FOSSEFALLMETODER I prosjekter som følger en fossefallsmetode, planlegges alle fasene på forhånd. Når en fase er over, er det for sent å gå tilbake.

Før i tiden var det vanlig at oppdragsgiveren og teamet analyserte oppdraget og la en fullstendig plan for hvordan produktet skulle produseres før teamet laget det. Først ble partene enige om hva det ferdige produktet skulle gjøre. Deretter lagde teamet skisser til brukergrensesnitt og kodebase (gjerne med objektorientert modellering). Når planen så var lagt, kodet teamet hver enkelt bit, satte bitene sammen og testet programmet. Alt ble planlagt så detaljert som det var praktisk mulig, inkludert hvor lang tid hver oppgave skulle ta. Partene ble også enige om en tidsfrist for det ferdige produktet og en totalpris.

Analyse

Design

Programmering

Testing

Tanken var at alt kom til å gå som smurt hvis man bare planla godt og fulgte planen. Slik var det imidlertid ikke i praksis. Når det ferdige produktet ble lansert, manglet det ofte funksjoner som brukerne ønsket seg, og mange ganger var det nesten umulig å bruke. Produktet ble dessuten ofte forsinket, for det viste seg at det ikke var så lett å beregne hvor lang tid det ville ta å utføre de enkelte oppgavene. Og hvis én oppgave ble forsinket, ble også oppgavene som var avhengige av denne, forsinket, og over tid hopet forsinkelsene seg opp.


Jobbe med IT-prosjekter

Slike utviklingsmetoder, det vil si der man planlegger først og utfører arbeidet senere, kalles for fossefallsmetoder. Navnet viser til at man ikke kan angre på valgene man har tatt, litt som når man rafter ned en elv og har passert en foss. Da er det umulig å snu og gå tilbake! Hvis for eksempel et team som jobbet med å kode, fant ut at det hadde vært smartere å designe brukeropplevelsen litt annerledes, var det utenkelig å gå tilbake og rette opp i dette. Siden de hadde brukt så mye tid på å planlegge, ville det ha vært for dyrt å endre planene.

Fossefallsmetoder deler arbeidet inn i oppgaver som skal gjøres i en bestemt rekkefølge: først analyse og planlegging, deretter design og programmering og til slutt testing.

SMIDIGE UTVIKLINGSMETODER Fordi prosjekter der man brukte en fossefallsmetode, ofte ble forsinket og endte med et lite brukervennlig produkt, ble det etter tusenårsskiftet vanligere å bruke såkalte smidige utviklingsmetoder (agile software development på engelsk). Disse metodene hadde vært i emning i flere tiår, men det var først på dette tidspunktet de slo gjennom. Målet med smidige utviklingsmetoder er å utvikle et lite, fungerende produkt så fort som mulig og deretter forbedre produktet litt etter litt. I stedet for å legge vekt på den første planleggingen satser oppdragsgiveren og teamet på å ha en løpende dialog. Det er ikke slik at de ikke planlegger i det hele tatt – de planlegger bare i kortere steg. I kapittel 3 lærte du om hvordan vi gradvis kan forbedre brukeropplevelsen basert på tilbakemeldinger fra brukerne. Smidige utviklingsmetoder kan sies å følge den samme prosessen, bortsett fra at de omfatter hele produktet, ikke bare brukeropp­levelsen.

I smidige utviklingsmetoder foregår arbeidet med planlegging, utvikling, testing og evaluering i kortere intervaller kalt iterasjoner eller sprinter. Med en slik metode vil en enkel utgave av produktet være klar tidlig i prosjektet. Produktet blir deretter gradvis forbedret. Teamet får hyppige tilbakemeldinger fra oppdragsgiver og brukere.

Iterasjon

Planlegge

Utvikle og teste

Evaluere

123


124

Kapittel 6

EKSTRASTOFF

Oppdragsgivere valgte fossefallsprosjekter fordi det virket lurt Det høres kanskje rart ut at fossefallsmetodene ble brukt over en så lang periode når resultatene var så dårlige. Hvis vi ser det fra oppdragsgivernes perspektiv, er det derimot ikke så rart. Oppdragsgiverne har som regel et budsjett, og da er det naturlig at de velger metoder som både gir dem en garanti om totalkostnadene og en sluttdato for når produktet skal være ferdig. Dessuten brukes fossefallsmetoder i prosjekter i andre bransjer, og det burde jo også være et tegn på at de er effektive.

DISKUTER

Fossefallsmetoder har blitt brukt til å bygge hus, sy klesplagg, bygge skip og ganske mye annet i hundrevis av år. Hvorfor tror du at slike metoder ikke passer like godt når man skal utvikle dataprogrammer? Eller tror du at det er mulig å bruke fossefallsmetoder og likevel lykkes?

FORDELENE MED SMIDIGE UTVIKLINGSMETODER For oppdragsgivere kan det være litt skummelt å si ja til smidig utvikling, der de verken får oppgitt en sluttdato eller totalkostnadene. I stedet er det som å sette på et taksameter og håpe at man får noe man ønsker seg. Men oppdragsgiverne er også garantert noen fordeler:

• • •

Brukerne får et produkt mye raskere. Produktet er ikke ferdig, men det kan brukes til noe. Produktet blir som regel mye bedre fordi utviklerne kan justere produktutviklingen basert på tilbakemeldinger fra brukerne. Hvis oppdragsgiverne ikke er fornøyd med resultatene (og kontrakten med utviklerne gir rom for det), kan de stoppe prosjektet eller ansette nye utviklere før de har brukt for mye penger.

Først etter ganske mange mislykkede fossefallsprosjekter – og en lang rekke vellykkede smidige prosjekter – var oppdragsgivere villig til å inngå kontrakter der de godtok smidig utvikling. I dag brukes smidige utviklingsmetoder i de fleste IT-prosjekter, mens fossefallsprosjekter hører til sjeldenhetene.


Jobbe med IT-prosjekter

6.3 Scrum

Den mest kjente smidige metoden heter Scrum. I dette delkapittelet forklarer vi hva en Scrum-sprint er.

SPRINT: KJERNEN I SCRUM I Scrum kalles hver iterasjon for en sprint. En sprint har en fast lengde på mellom én og fire uker. Hver sprint består av fire steg. Vi har satt noen begreper i kursiv nedenfor. Dette betyr at vi skal forklare begrepet nærmere senere i delkapittelet. 1. Sprint-planlegging: Teamet ser på innholdet i backloggen og blir enige om hva de skal ha klart i produktet når sprinten er ferdig. Det man blir enig om å ha ferdig på slutten av sprinten, kalles for sprintmålet. Teamet lager programmeringsoppgaver som de skal utføre basert på det de valgte fra backloggen. 2. Daglig arbeid (også kjent som «daglig Scrum»): Teammedlemmene tar på seg én og én oppgave og løser dem. Teamet har minst ett kort møte hver arbeidsdag, standupmøte. 3. Sprint-gjennomgang: Et fysisk eller digitalt møte der teamet demonstrerer produktet for interessentene. Teamet får tilbakemeldinger og tips til hva de bør gjøre i neste sprint. Denne gjennomgangen skjer gjerne én–to dager før sprinten er over. Ofte viser man frem backloggen, slik at interessentene vet hva de kan vente seg etter neste sprint. Man snakker løst og fast om hvorvidt interessentene har noen nye idéer til produktet. Dersom det er mulig, tas den nye versjonen av produktet i bruk. 4. Retrospektiv: Teamet har et møte der de evaluerer og reflekterer over hva som gikk bra og dårlig i sprinten som nettopp er over, og kommer med forslag til hvordan de kan bli bedre. I kapittel 6.5 får du et eksempel på hvordan man gjennomfører en sprint. Nå ser vi nærmere på noen sentrale Scrum-begreper. En sprinter står på startstreken. Han vet nøyaktig hva han skal gjøre og hva målet er. I smidige utviklingsmetoder brukes ordet sprint om en arbeidsperiode.

125


126

Kapittel 6

Alle som har noe med sluttproduktet å gjøre

Interessentene

Meningsutveksling om det som lages

Teamet Backloggen

Styrer Produkteieren

INTERESSENTENE Interessentene (stakeholders på engelsk) er en håndfull mennesker som skal representere alle som har noe å si om produktet, for eksempel de som skal bruke det og de som betaler for det. Teamet bruker interessentene til å veilede seg til å lage produktet som brukerne og oppdragsgiveren ønsker seg. Teamet og interessentene snakker sammen minst én gang hver sprint. Interessentene skal delta i sprint-gjennomgangen. Teamet kan også ta kontakt med interessentene underveis i sprinten. For eksempel kan en interessent gi teamet svar på noe de er i tvil om. Interessentene kan også bidra med å finne deltakere til en brukertest. Før teamet starter den første sprinten, bør de også ha snakket med interessentene, slik at de har noen arbeidsoppgaver til den første sprinten.

Interessentene har ulike grunner til at de vil være med i gruppa. Noen blir med fordi de skal bruke produktet og vil påvirke brukeropplevelsen. Andre er med for å passe på at produktet holder seg «innenfor», for eksempel økonomisk eller juridisk. De setter ned foten når man planlegger altfor omfattende funksjoner eller funksjoner som bryter loven. Kanskje kommer de med innspill til måter man kan tjene mer penger på. For å være en interessent må man ha tid til å delta på sprint-gjennomgangen, gjerne i flere måneder framover god kjennskap til hele eller deler av problemet som skal løses motivasjon for å hjelpe produktet til å lykkes

• • •

EKSTRASTOFF

Å finne interessenter som representerer brukerne Ofte lager man produkter som hjelper brukerne å gjøre en del av jobben deres. Slike produkter kan for eksempel være systemer for å loggføre medisiner på et sykehjem, registrere utlånte bøker i et bibliotek eller programvaren til et kassaapparat i en butikk. Når man lager et produkt som interessentene må bruke, pleier det å være lett å få folk til å stille: Brukeren og brukerens arbeidsgiver vil gjerne ha et så godt produkt som mulig, og derfor får interessenten gjerne fri fra andre forpliktelser for å delta i sprint-gjennomgangene. Mange IT-produkter har derimot brukere som ikke er nødt til å bruke produktet i jobben sin, men som selv har valgt å bruke det. Fordi slike brukere må ta seg tid til å delta, pleier man sjelden å få faktiske brukere til å stille. Du kan jo selv tenke deg at du ble bedt om å stille som interessent til en app du bruker på fritiden. Selv om du hadde hatt lyst, hadde skolen eller arbeidsgiveren din neppe gitt deg fri flere ganger i måneden for å møte teamet som utvikler appen. Når man utvikler et produkt der brukerne selv velger å bruke produktet, pleier man å velge en ansatt hos oppdragsgiveren til å representere brukerne. Dette bør være en som kjenner brukerne godt, for eksempel en kundeansvarlig eller markedsansvarlig. Det er en stor fordel om man også finner en faktisk bruker som er kunnskapsrik, motivert og har tid til å delta.


Jobbe med IT-prosjekter

EKSEMPEL Interessenter for et søknadsskjema hos Nav Vi tenker oss et prosjekt der et team skal lage et digitalt søknadsskjema for Nav. Da kan interessentene for eksempel bestå av følgende personer:

• • •

Ole, saksbehandler i hos Nav. Valgt fordi han jobber med saksbehandling til daglig og skal bruke systemet til slutt. Han er også nybakt far, så han har brukt det gamle foreldrepengeskjemaet på papir. Dermed klarer han til en viss grad å se systemet med perspektivet til en utenforstående. Nina, jurist hos Nav. Valgt fordi hun kjenner regelverket og fort kan si om det er lovlig å lage en viss funksjon eller ikke. Pekka, avdelingsdirektør for foreldrepengeavdelingen. Er blant interessentene for å representere oppdragsgiveren (administrasjonen i Nav). Pekka kan for eksempel gi innspill på om budsjettet strekker til for å gjøre alt Ole ønsker seg.

EKSEMPEL Interessenter for en app for førerkortopplæring Trafikkskolen Tut og kjør AS har lyst til å lage en app for elever som skal ta førerkortet, slik at de kan lære seg det de trenger for å bestå teoriprøven. Interessentene består av følgende personer:

• • •

Maria, forretningsansvarlig for Tut og kjør AS. Hun kommer med innspill fra bedriften om hvordan man kan tilpasse appen for å tjene penger. Gisle, trafikkskolelærer gjennom 20 år og deltidsansatt i Tut og kjør AS som kundeansvarlig. Gisle er ekspert på vegtrafikkloven. Han har også pedagogisk erfaring fra jobben sin. Peder, 17 år gammel og en av Gisles elever. Tut og kjør AS spanderer én kjøretime på ham for hver sprint-gjennomgang han deltar i – en ganske god deal. Peder representerer elevene. Han har god kjennskap til hva som er populært blant produktets målgruppe.

BACKLOGGEN I møte med interessentene pleier man å presentere ønsker og ideer for produktet. Disse ønskene skriver man ned og lagrer i den såkalte backloggen. Backloggen kan for eksempel være et regneark der ønskene sorteres i prioritert rekkefølge. Ønskene som har høyest prioritet, plasseres øverst i backloggen. Det er vanlig å skrive ned ønskene som såkalte brukerhistorier. En brukerhistorie er et ønske uttrykt som noe en spesifikk type bruker kunne ønske seg, skrevet på en fast form, for eksempel slik: «Som <type bruker> vil jeg kunne <utføre en gitt handling>, slik at <overordnet mål>.» Med et fast format blir det lettere å prioritere ønskene opp mot hverandre og å sørge for at alle brukergrupper får noe de ønsker seg med jevne mellomrom. Under sprint-planleggingen i starten av hver sprint plukker teamet så mange ønsker fra toppen av backloggen som man tror er realistisk at de får til i løpet av sprinten. Dette blir sprintmålet.

127


128

Kapittel 6

Backloggen er en prioritert liste over hva interessentene ønsker seg. Ønskene kan være formulert som brukerhistorier på formen «Som <type bruker> vil jeg kunne <utføre en gitt handling>, slik at <overordnet mål>.»

EKSEMPEL Et team skal utvikle et kassaapparat. Her er noen av brukerhistoriene som interessentene kom med i det første møtet. Teamet har ikke sortert brukerhistoriene etter prioritet enda.

• • • • • • •

Som kasseoperatør vil jeg kunne skrive ut en kopi av en kvittering, slik at kunder som vil ha kvitteringen, kan få det. Som kasseoperatør vil jeg kunne lese inn en vare, slik at den havner på kjøpslista og jeg kan ta betalt for den til slutt. Som kunde vil jeg kunne se varene som er slått inn hittil på en skjerm slik at jeg kan kontrollere at prisen er riktig før jeg har betalt. Som butikksjef vil jeg kunne se hvor mange poser en ansatt har solgt slik at vi ikke taper penger på posesalget. Som kasseoperatør vil jeg kunne ta betalt, slik at butikken ikke går konkurs. Som kasseoperatør vil jeg kunne ta vekk varer jeg har slått inn, slik at jeg kan angre på feilinntastinger og kunden kan ombestemme seg hvis totalsummen er for høy. Som kunde vil jeg kunne ta ut kontanter når jeg betaler med kort, slik at jeg kan betale med dem andre steder og gi dem bort som gave.

Av og til kan det siste leddet i en brukerhistorie høres nesten latterlig selvsagt ut, men når man skal prioritere mellom oppgaver eller komme med alternative løsningsforslag, er det verdifullt å vite hvorfor folk ønsker seg noe.

DISKUTER

Hvilke av brukerhistoriene i eksemplet med kassaapparatet synes du teamet burde prioritere høyest? Kommer du på noen brukerhistorier du ville lagt til?

PRODUKTEIEREN I Scrum-team får én person ansvaret for backloggen. Denne personen sørger for at ønskene er tydelig formulerte og får riktig prioritert. Denne personen kalles for produkteieren (product owner på engelsk). Hvis alle teammedlemmene skulle delt på ansvaret med å styre backloggen, hadde de brukt mye arbeidstid på å holde oversikt over hva interessentene mente og diskutere hva som burde ha høyest prioritet. Til daglig jobber produkteieren ofte som utvikler i teamet, men bruker litt av arbeidstiden sin på å vedlikeholde backloggen. Kanskje sier et teammedlem ifra om tekniske hindringer eller uklarheter på standupmøtet som betyr at man bør omprioritere. Da kan produkteieren måtte bruke litt tid på å snakke med teammedlemmet og ta en


Jobbe med IT-prosjekter

telefon til ulike interessenter. Etterpå oppdaterer produkteieren backloggen slik at den speiler det nye han eller hun har lært. Grunnen til at denne rollen har fått et navn som tilsier at vedkommende «eier» produktet, er at produkteieren styrer backloggen. Backloggen bestemmer hva som blir laget, og den som bestemmer over backloggen, bestemmer dermed over produktet. Produkteierens makt er ikke absolutt. Hvis teammedlemmene, interessentene eller oppdragsgiveren er misfornøyde med jobben han eller hun gjør over lengre tid, kan det være at noen andre får rollen.

STANDUPMØTET Standupmøtet er et møte som foregår på et fast tidspunkt hver arbeidsdag. I dette møtet skal hvert teammedlem svare på følgende tre spørsmål som angår sprintmålet. Sprintmålet er de ønskene fra backloggen som man har blitt enige om å oppfylle når sprinten er over: 1. Hva gjorde jeg i går som hjalp teamet med å møte sprintmålet? 2. Hva skal jeg gjøre i dag som hjelper teamet med å møte sprintmålet? 3. Ser jeg noe som hindrer meg eller teamet i å møte sprintmålet? Teamet har korte møter hver dag. Møtene skal være så korte at alle bare kan stå. Derfor kalles møtene for standupmøter.

Dette møtet har ikke fått navnet sitt fordi man er nødt til å fortelle vitser, men fordi man helst skal utføre møtet stående. Hvis man står, blir teammedlemmene slitne hvis det varer for lenge. Dermed sørger man for at folk holder seg til de tre faste spørsmålene. Teammedlemmene kan avtale å møtes for å diskutere og løse problemer sammen når det er behov for det.

129


130

Kapittel 6

Et IT-prosjekt berører mange ulike personer. I et smidig prosjekt er det vanlig å dele disse opp i følgende grupper/roller: • oppdragsgiver • brukere • interessenter • produkteier • teammedlemmer

6.4 Oppgavetavler

De fleste team bruker en tavle med lapper for å holde styr på alt de skal gjøre i en sprint. Da er det lett å se hva de ulike teammedlemmene jobber med, og hva som er ferdig. Slike tavler finnes både i fysiske og digitale utgaver.

FYSISK OPPGAVETAVLE En fysisk oppgavetavle består av en tavle, som regel en tusjtavle, av passende størrelse (gjerne én ganger to meter eller større). Tavla deles inn i tre kolonner, gjerne med farget teip eller utviskbar tusj. Kolonnen lengst til venstre får overskriften «Skal gjøres», den midtre kolonnen får overskriften «Gjøres» og kolonnen lengst til høyre «Ferdig».

En fysisk oppgavetavle er ganske enkelt en tavle med papirlapper på.


Jobbe med IT-prosjekter

Alle teammedlemmene lager en markør som skal brukes til å vise hvilken oppgave de jobber med. Hvis det er en tusjtavle, bruker de gjerne en magnet som er litt større enn en tjuekroning, slik at de kan feste et portrettbilde eller skrive navnet sitt på den. Er tavla ikke magnetisk, finner de på noe annet. Når teamet har bestemt seg for hvilke oppgaver de skal jobbe med, skriver de en kort beskrivelse av hver oppgave på atskilte lapper, eksempelvis «Designe navigasjonsmeny», «Kode innloggingssiden», «Sette opp brukerdatabase» og «Legge inn eksempelartikkel». Oppgavene på hver lapp bør være så små at ett teammedlem alene (eller to som samarbeider) kan løse dem på kort tid uten hjelp fra noen andre. Hvis oppgaven på en lapp virker for omfattende eller tidkrevende, prøver teamet å dele den opp i flere oppgaver (og lapper). Hvis et teammedlem først oppdager dette når hun begynner å jobbe med en oppgave, deler hun den opp der og da. Når teamet er ferdig med å skrive lappene, fester de dem i kolonnen «Skal gjøres». Tavla bør være plassert slik at alle teammedlemmene kan se den mens de jobber. På den måten blir den et samlingspunkt for teamet. Når et teammedlem skal ta en oppgave, flytter det lappen fra «Skal gjøres» til «Gjøres» og fester markøren sin på lappen. Når medlemmet er ferdig med oppgaven, flytter det lappen til «Ferdig» og tar en ny lapp fra «Skal gjøres»-kolonnen. Slik gjentar teammedlemmene prosessen til alle lappene er flyttet til «Ferdig»-kolonnen. Det er gøy å bruke en oppgavetavle. Alle i teamet har oversikt over fremdriften i prosjektet og det er lett å se hvordan oppgavene gradvis blir ferdige. I tillegg er det lett å se hva hver enkelt holder på med. Og siden hvert teammedlem bare har én markør, unngår man at folk holder av de morsomste oppgavene til seg selv. EKSTRASTOFF

Det enkle er ofte det beste Ofte har prosjekter flere enn én kolonne mellom «Skal gjøres»- og «Ferdig»-kolonnene. Disse kolonnene representerer gjerne flere understeg av «Gjøres», for eksempel «Kodes», «Kvalitetssikres», «Testes», «Venter på hjelp» og så videre. Hvis oppgavetavla blir så avansert at teammedlemmene glemmer å flytte lappen sin eller er usikre på hvor en lapp hører til, synker tavlas verdi. Derfor anbefaler vi at man ikke legger til flere kolonner i midten enn «Gjøres»kolonnen, med mindre det faktisk skaper problemer for teamet at tavla bare har én kolonne i midten.

DIGITAL OPPGAVETAVLE I de senere årene har det blitt stadig vanligere at teammedlemmer ikke sitter fysisk sammen i et felles arbeidsrom. Noen har kanskje hjemmekontor, og enkelte team har til og med medlemmer som befinner seg i et andre land. Da er det ikke hensiktsmessig å ha en fysisk oppgavetavle. Men det finnes digitale oppgavetavler som gjør den samme jobben som fysiske oppgavetavler. Også team der alle medlemmene sitter sammen, kan av ulike grunner foretrekke å ha en digital oppgavetavle.

131


132

Kapittel 6

En digital oppgavetavle kan brukes på samme måte som en fysisk tavle, men den byr som regel på noen ekstra muligheter. Vi kan for eksempel ofte skrive lengre kommentarer på lappene, som blir synlige hvis vi klikker på dem, og noen ganger kan vi legge ved filer. Da kunne teamet for eksempel ha lagt ved skissene til koden i «Kode innloggingssiden»-lappen. Hvis vi skriver kommentarer på lappene, kan vi for eksempel si fra om at vi står fast og trenger hjelp, eller om at vi er usikre på om vi skal velge ett eller flere alternativer. For å løse slike problemer må vi ofte ha en samtale. Det er nyttig å kunne si fra om behovet for en samtale og legge frem ­«saksmateriale» til samtalen på forhånd.

Digitale oppgavetavler: • Trello • Asana • Microsoft Planner

En oppgavetavle har kolonner for oppgaver som skal gjøres, oppgaver som pågår og oppgaver som er ferdige. Tavla kan være fysisk eller digital. Oppgavene skrives på en lapp eller i en boks som kan flyttes mellom kolonnene. På den måten får teamet oversikt over hva som skal gjøres, hva de andre jobber med, og hvilke oppgaver som er ferdige.

EKSTRASTOFF

Datasikkerhet og personvern for prosjektstyringsverktøy Når vi bruker digitale oppgavetavler og andre prosjektverktøy, lagres dataene så å si alltid på et datasenter utenfor Norge. De som har tilgang til datasenteret, har som regel mulighet til å se hva vi skriver, og regjeringen i landet der datasenteret ligger, kan kreve å få overlevert dataene uten en spesiell grunn. Selv i demokratiske land er det juridiske vernet overfor andre enn landets egne borgere ganske svakt. Som skoleelev har du ikke samme juridiske ansvar for å behandle personopplysninger trygt som du ville hatt om du jobbet for en bedrift. Du jobber heller ikke med informasjon som er forretningssensitiv. Hvis du etablerer en bedrift, for eksempel et enkeltpersonsforetak, for å tjene penger på koden din, må du derimot ta hensyn til personvernregler når det gjelder brukerne dine. I tillegg må du sørge for at oppdragsgiveren din er innforstått med konsekvensene av at du lagrer data som angår forretningen deres, i utlandet. Hvis du lager en hjemmeside for en kafé, har det neppe like mye å si som om du jobber for en advokat eller en lege, men kundene skal i alle tilfeller ha kontroll over opplysningene sine. Da må du være på den sikre siden.


Jobbe med IT-prosjekter

6.5 En sprinthistorie

I dette delkapittelet skal vi se nærmere på de ulike stegene i en sprint gjennom et tenkt prosjekt med tre teammedlemmer: Alice (produkteier), Bob og Christine (vanlige teammedlemmer). Alice, Bob og Christine har tatt IT2 sammen og har bestemt seg for å tjene penger på å lage nettsider mens de tar et friår etter videregående. De har laget en bedrift de kaller Kodemagi. Kodemagi har spredd budskapet om planene sine blant folk de kjenner. Etter noen dager tar daglig leder ved konsertscenen Dunken kontakt. Han ønsker å bestille en hjemmeside. Dermed har Kodemagi fått sitt første oppdrag.

OPPDRAGSGIVEREN Oppdragsgiveren er daglig leder ved Dunken, som er en populær konsertscene i nærområdet. Han heter Abdi og er 35 år gammel. Abdi har ikke hatt en egen hjemmeside for Dunken før. I stedet har han hatt kontakt med publikum via en sosial medieplattform. Tidligere fikk alle som hadde likt Dunkens side, opp innlegg om arrangementer i feeden sin, uten at Abdi trengte å betale for det. Nå har imidlertid selskapet bak plattformen besluttet å ta betalt for å vise disse postene, så Abdi vil prøve å ha kontakt med publikum uten et mellomledd. Han har lyst til å se om en egen hjemmeside kan ha en positiv effekt.

OPPDRAGET Kodemagi har fått i oppdrag å lage hjemmesiden til konsertscenen Dunken. Dunken, som har plass til 200 publikummere, har litt av hvert på programmet:

• • • •

På kveldene kan det være en DJ eller et band på scenen. Av og til er en standupkomiker innom. Noen ganger har det lokale koret øvinger. Det er mulig å leie lokalet til private arrangementer.

133


134

Kapittel 6

Oppdragsgiveren har ikke lyst til å bruke for mye penger på hjemmesiden – bare noen få tusen i utgangspunktet. Derfor blir oppdragsgiveren og Kodemagi enige om at Kodemagi skal eie opphavsretten til koden, slik at de kan selge produktet videre til andre konsertscener eller bedrifter senere. Teamet har gjort det klart for Abdi at de er interessert i å ta på seg andre oppdrag når de er ferdige med dette oppdraget. Dersom han vil vedlikeholde hjemmesiden, må han altså gjøre det selv. Abdi forstår HTML og JSON, så han har sannsynligvis nok kunnskap til å legge ut innhold på hjemmesiden når den er ferdig. De blir enige om en sprintlengde på én uke, slik at interessentene kan gi tilbakemelding raskt.

BRUKERE Abdi og Kodemagi er enige om at nettsiden har tre hovedbrukergrupper:

• • •

publikum – de som besøker en konsert promotører – de som booker inn artister til konserter private arrangører – de som vil leie lokalet til et privat arrangement

INTERESSENTER Abdi mener at han selv kjenner promotører og private arrangører godt nok til å vite hva de ønsker seg. Abdi foreslår at en av interessentene kan være Jenny. Jenny er dørvakt og vekter på mange av arrangementene på kveldstid og er 22 år gammel. Hun studerer på dagtid og er en ivrig konsertgjenger. Hun representerer dermed en av de viktigste brukergruppene hans: publikum.

DET FØRSTE MØTET MED INTERESSENTENE Alice møter Abdi og Jenny på dagtid torsdag før de skal begynne arbeidet. Begge har mange ideer. Her er notatene Alice tar på møtet:

• • • • • •

Forsiden skal vise hvilke arrangementer som kommer de neste ukene. Det skal være lett for promotører å finne ut hvordan de kontakter Abdi for å selge inn en artist. Brukerne bør kunne filtrere de kommende arrangementene etter type, slik at de kan se bare den typen arrangementer eller de musikksjangrene de er interesserte i. Når noen klikker på et arrangement, skal de få opp informasjon om de som spiller, og konserten, som når dørene åpnes. Kanskje siden kan inneholde lenker til strømmetjenester der man kan høre på musikken til artisten eller gruppen? Det burde være mulig å kjøpe billetter via siden. Folk som vil leie lokalet til private arrangementer, skal kunne sende inn et skjema for å booke tider.


Jobbe med IT-prosjekter

• • • •

Det bør være mulig å se hva de tilbyr av snacks i baren, på en meny. På forsiden vil de ha en boks der de viser bilder og videoer som publikummerne har tatt på de siste arrangementene og lagt ut på sosiale medier. De vil ha en e-postliste som publikummerne kan melde seg på, slik at de kan få varsel om kommende arrangementer. Forsiden bør ha lenker til sider på sosiale medier der man kan følge konsert­ scenen.

Etter møtet omformulerer Alice disse ønskene til brukerhistorier. Hun snakker med resten av teamet om hva det er realistisk at de får til, og de blir enige om hva de skal prioritere.

ALICE BLIR ENIG MED INTERESSENTENE OM SPRINTMÅLET På fredag har Alice et kort videomøte med interessentene. Hun forteller at teamet har blitt enige om å levere et produkt som oppfyller følgende brukerhistorier: 1. Som daglig leder vil jeg kunne legge inn et nytt arrangement, slik at det dukker opp på forsiden og publikum kan se det. 2. Som publikummer vil jeg kunne se hvilke arrangementer som kommer, slik at jeg kan bestemme meg for hva jeg vil gå på. 3. Som promotør vil jeg kunne se hvordan jeg tar kontakt med konsertscenen, slik at artistene mine kan spille der. 4. Som privat arrangør vil jeg kunne ta kontakt med konsertscenen, slik at jeg kan booke lokalet til et privat arrangement. Sprintmålet er altså å ha en hjemmeside som møter de grunnleggende behovene til alle brukerne av konsertscenen. Alice og interessentene blir enige om å møtes på torsdag ettermiddag uken etterpå for å overlevere produktet.

ARBEIDSUKEN Teammedlemmene jobber hjemmefra. De er enige om å ha daglige standupmøter klokken 11. Den første dagen møtes Kodemagi klokken 9 og gjør brukerhistoriene som Alice har valgt ut, om til arbeidsoppgaver som de skal ha på tavla:

• • • • • •

Lage et dataformat for arrangementer Lage utkast til design for hjemmesiden Lage en arrangementsliste-komponent som henter inn rådataene om arrangementer og viser dem på hjemmesiden Lage en navigasjonslinje Lage siden promotører skal besøke Lage siden som private arrangører skal besøke

135


136

Kapittel 6

Slik ser oppgavetavla ut når medlemmene har lagt inn oppgavene.

De har tre dager på seg før de skal møte interessentene igjen. Altså må hver person i gjennomsnitt bli ferdig med to oppgaver hver dag. Det er litt mye, men de tre siste oppgavene er heldigvis mindre enn de tre første. Alice tar ansvar for oppgaven «Lage et dataformat for arrangementer». Bob tar oppgaven «Lage utkast til design for hjemmesiden». Christine tar oppgaven «Lage en arrangementslistekomponent …». De jobber en åtte timers arbeidsdag. Slik ser oppgavetavla ut etter at medlemmene har tatt på seg oppgaver.

DAGLIGE STANDUPMØTER Hver dag klokken 11 har medlemmene et videomøte for å snakke om det de holder på med. På tirsdag sier medlemmene dette: Alice: • I går bestemte jeg meg for at arrangementene skal lagres som en stor JSON-fil som Abdi redigerer manuelt. Jeg har lagt inn noen eksempelarrangementer i kodebasen. • I dag skal jeg ta en prat med Abdi og finne ut om han forstår hvordan han kan redigere filen. • Det som kan hindre meg, er at Abdi ikke har tid til å svare. I så fall begynner jeg på en annen oppgave i mellomtiden.


Jobbe med IT-prosjekter

Bob: • I går laget jeg noen enkle skisser av hjemmesiden på papir. Jeg tok turen til kjøpesenteret og gikk bort til folk på en kafé for å utføre en geriljatest, det vil si jeg ga dem ulike oppgaver som publikummer og privat arrangør. Jeg droppet å teste promotørrollen, for kafégjestene har ikke mulighet til å sette seg inn i hva den går ut på. Det var litt kleint først, men jeg ble vant til det ganske raskt. • Jeg fikk noen tilbakemeldinger på papirtegningene som jeg skal rette på i dag. • Det er ingen hindre i sikte.

Christine: • I går laget jeg et utgangspunkt for arrangementslistekomponenten. • I dag må jeg nok endre på designet basert på tilbakemeldingene du fikk, Bob, og basert på dataformatet du har bestemt, Alice. • Ingen hindre.

Oppgavetavla er uendret fra i går. På onsdag sier medlemmene dette: Alice: • Abdi hadde ikke tid til å snakke i går. Derfor tok jeg på meg å lage siden som private arrangører skal besøke. • Jeg har avtalt et møte med Abdi i dag. Derfor regner jeg med å bli ferdig med begge oppgavene. • Ingen hindre.

Slik ser oppgavetavla ut etter standupmøtet.

Bob: • Jeg ble ferdig med skissene i går og kodet designet for hjemmesiden. • I dag skal jeg lage navigasjonslinjen. • Ingen hindre.

Christine: • I går endret jeg arrangementslistekomponenten. Jeg satset på at dataformatet var greit, Alice. Du kan endre den hvis Abdi vil endre på dataformatet. • I dag skal jeg legge inn arrangementslisten på forsiden av hjemmesiden. Etter det skal jeg lage siden som promotører havner på. • Ingen hindre.

137


138

Kapittel 6

SPRINT-GJENNOMGANGEN På torsdagen møtes medlemmene hjemme hos Christine klokken 10. Alle er heldigvis ferdige med oppgavene sine. De gjennomfører alle testene de har skrevet for å se at produktet virker som det skal. Slik ser oppgavetavla ut når alle medlemmene er ferdige med oppgavene sine.

Klokken 13 møter alle opp på Dunken. De viser frem programmet slik det ser ut nå. Det består av tre sider, som vises som følgende punkter i navigasjonsmenyen:

• • •

Forsiden: Denne siden vises når man går til Dunkens domene. Her vises arrangementslisten som en kronologisk liste. Book lokalet: Dette er siden som private arrangører trykker på. Her står det litt om hvor mye det koster å leie lokalet, hvilke tjenester de tilbyr, og hvordan man tar kontakt. Opptre hos oss? Dette er siden som promotører trykker på. Her dukker e-postadressen og telefonnummeret til daglig leder opp.

Abdi får en ekstra gjennomgang av dataformatet han må legge inn arrangementene med, og teamet demonstrerer hvordan han laster opp bilder. Dette er han sikker på at han klarer. Han skulle ønske at det var et skjema på bookingsiden i stedet for bare e-postadressen hans. Ofte oppgir ikke private arrangører alle de nødvendige opplysningene når de skriver til ham, og det blir mye frem og tilbake. Jenny er fornøyd, men hun skulle ønske at det var mulig å trykke på et arrangement for å lese mer om det. Informasjonen på forsiden er litt for intetsigende til at hun blir interessert i konsertene. Hun skulle også ønske at det var en kalendervisning av arrangementene i tillegg til listen, slik at det var lettere å huske datoen for de ulike arrangementene. Alice omprioriterer backloggen. De blir enige om at de i neste sprint bør jobbe med to brukerhistorier, en som handler om å lage en side med informasjon for hvert arrangement, og en annen som handler om å lage et skjema som private arrangører kan ta kontakt gjennom. Som kronen på verket offentliggjør de arrangementssiden på domenet dunken.no.


Jobbe med IT-prosjekter

RETROSPEKTIV Teamet møtes hjemme hos Bob. De har bestemt seg for å bruke retrospektivmetoden DAKI. DAKI er en retrospektivmetode og står for følgende kategorier • Drop – vaner eller tiltak man vil slutte med • Add – vaner eller tiltak man vil legge til i teamet • Keep – vaner eller tiltak man vil holde på • Improve – vaner eller tiltak man vil forbedre

Hvert medlem bruker ti minutter på å skrive ned tanker om hver kategori på forskjellige post-it-lapper. Etterpå går de gjennom lappene sine sammen mens de fester dem på en korktavle. Til slutt ender teamet opp med følgende lapper innenfor de ulike kategoriene:

• •

• •

Drop: «Sprintlengde på én uke. Det ble for stressende.» Add: «Hvis dette skal være verdt å drive med, bør vi ha minst én kunde til. Ellers kan vi like godt ta en vanlig jobb og tjene mer penger.» «Brukergrensesnitt for å legge inn arrangementer: De færreste kunder kan skrive kode selv. Vi kan lage vårt eget brukergrensesnitt, men vi kan sannsynligvis spare mye tid på å bruke et forhåndslaget brukergrensesnitt for å styre innholdet på nettsiden (et såkalt innholdsstyringssystem).» Keep: «Alice som produkteier. Hun gjør en kjempejobb.» Improve: «Mer brukerinnsikt. Vi bruker for mye tid på å lage produktet og for lite på å sjekke om vi faktisk lager det riktige produktet.»

Med utgangspunkt i det de har kommet frem, bestemmer de seg for at de skal gjøre følgende på fredag:

• • •

Alice skal be Abdi om til en lengre sprintlengde i neste omgang. Christine skal ringe rundt til konsertscener i nabobygdene for å høre om de er interessert i å kjøpe en tilsvarende hjemmeside. Bob skal utforske løsninger for innholdsstyringssystemer over helgen. Han har blant annet hørt mye bra om det norskproduserte Sanity.io.

På fredag ettermiddag bruker Alice det de har funnet til å prioritere backloggen. På mandag starter de neste sprint med sprintplanlegging.

139


140

Kapittel 6

6.6 Versjoner og versjonskontroll VERSJONER Som vi sa i starten av kapittelet, er det vanlig å oppdatere IT-produkter ettersom tiden går. Vi bruker ordet versjon for å vise til hvordan et produkt var på et visst tidspunkt. Når man gir ut en oppdatering, sier man at man gir ut en ny versjon av produktet. EKSTRASTOFF

Versjonsnummerering En vanlig måte å vise til en versjon på, er med tre tall adskilt av punktum, et såkalt versjonsnummer. Man starter på versjon 1.0.0 eller 0.0.0 og øker de ulike tallene ut fra hva slags endringer som kommer i den nye versjonen. Versjonsnummeret endrer seg ofte etter følgende mønster: «(økes ved store endringer).(økes ved mindre endringer).(økes ved feilrettinger)». Tallene som kommer etter det tallet man øker, stilles alltid tilbake til 0. Hvis man gjør en stor endring, øker man det første tallet og stiller de to påfølgende tallene tilbake til 0, selv om man også la til funksjoner eller rettet opp feil. Dersom vi for eksempel er på versjon 12.3.0 og skal gi ut en ny versjon, kan vi nummerere slik:

• • •

Hvis den nye versjonen bare inneholder kodeendringer som retter opp i små feil, øker vi bare det siste tallet: 12.3.1. Hvis den nye versjonen inneholder nye funksjoner, kaller vi den nye versjonen 12.4.0. Hvis den nye versjonen inneholder store endringer, som å endre det overordnede designet eller endre en eksisterende funksjon, kaller vi den nye versjonen 13.0.0.

Selv om dette er den vanligste måten å vise til versjonene på, finnes det mange alternativer. De fleste appene i listen over oppdateringer i iOS til venstre, følger dette mønsteret. Appene Google Chrome og IMDb følger et annet mønster.

Når man oppdaterer apper i operativsystemet iOS, får man se hvilken versjon man oppdaterer til og en beskrivelse av hva som er nytt.

VERSJONSKONTROLL Versjonskontroll er en type programvare som holder styr på hvordan filene i en mappe så ut på ulike tidspunkter. Man kan også bruke programmet for å dele en prosjektmappe mellom alle som jobber sammen om et prosjekt. Du har kanskje brukt fildelingstjenester som Dropbox, Onedrive eller Google disk for å samarbeide om prosjekter med andre. Fildelingstjenester er ikke å regne som versjonskontroll, men brukes av nesten samme grunner og gir omtrent de samme fordelene. Alle gruppemedlemmer har tilgang til de nyeste versjonene av prosjektfilene. Hvis et gruppemedlems datamaskin krasjer, har man en sikkerhetskopi i skyen. Hvis noen sletter en fil eller noe av innholdet i en, kan man se på og gjenopprette tidligere versjoner av filen.


Jobbe med IT-prosjekter

I fildelingstjenester som Dropbox, kan vi dele en mappe med flere personer. Da lagres en sikkerhetskopi i skyen. Vi kan også gå tilbake til tidligere versjoner av enkelte filer.

Versjonskontrollprogrammer har som sagt mange likheter med fildelingsprogrammer som brukes til arbeid utenfor IT-bransjen, og gir mange av de samme fordelene: sikkerhetskopiering, deling og historikk. Programmene er tilpasset måten man jobber sammen på innen IT. Det mest populære versjonskontrollprogrammet som brukes i dag, heter Git. To andre kjente alternativer er Mercurial og SVN.

VERSJONSKONTROLL ER ØYEBLIKKSBILDER AV KODEBASEN Den viktigste forskjellen på et versjonskontrollprogram sammenlignet med fildelingsprogrammer, er at versjonskontrollen lager et bilde av hele mappen når den sikkerhetskopierer prosjektet. Fordi filene i et IT-prosjekt nesten alltid avhenger av hverandre for å fungere, gir det ikke mening å tilbakestille bare én fil til en tidligere versjon. Man må kunne tilbakestille hele mappen til slik den var på et visst tidspunkt for å skjønne hvordan produktet virket akkurat da.

141


142

Kapittel 6

De som lager Svelte ­bruker versjonskontroll­ programmet Git. På https://github.com/ sveltejs/svelte/commits/ master kan vi se de nyeste endringene som en liste. Der ser vi blant annet når endringen skjedde, hvem som utførte den og en beskrivelse av hva som ble endret.

For at versjonshistorikken ikke skal inneholde støy (for eksempel mislykkede eksperimenter før man fant en fungerende løsning), må den enkelte utvikler si til versjonskontrollen når den skal ta et øyeblikksbilde (i motsetning til med fildelingstjenester, som tar øyeblikksbilder hver gang man lagrer en fil). Først når man sier ifra tar versjonskontrollprogrammet et øyeblikksbilde av prosjektmappen og legger det til i historikken. Så lenge man vet at en fil eller kodelinje ligger lagret i et tidligere øyeblikksbilde, kan man redigere eller slette den med god samvittighet. Dersom man angrer seg, kan man alltid bla tilbake til tidligere øyeblikksbilder og hente opp igjen det man endret. Det er vanlig å lagre mange øyeblikksbilder underveis, og ikke bare når man utgir en ny versjon, selv om navnet «versjonskontroll» skulle tilsi at man bare tok et øyeblikksbilde når man hadde en fullstendig versjon klar. Mellom to versjoner ligger det som regel mange øyeblikksbilder som bare representerer steg på veien fra én versjon til den neste. Når man gir ut en ny versjon som brukerne skal få ta i bruk, sier man fra til versjonskontrollprogrammet om hvilket øyeblikksbilde som representerer den nye versjonen.


Jobbe med IT-prosjekter

BLA GJENNOM OG SAMMENLIGNE ØYEBLIKKSBILDER Man kan bruke versjonskontrollprogrammet for å bla frem og tilbake mellom øyeblikksbildene, nesten som om man blar fort gjennom polaroidbilder eller papirark som ligger etter hverandre i en oppbevaringsboks. For eksempel gir man programmet en kommando som git checkout v12.3.0 , og noen sekunder etterpå ser mappen på datamaskinen ut akkurat som da man ga ut versjon 12.3.0. Man kan også be programmet om å visualisere hvilke linjer som ble endret fra ett øyeblikksbilde til et annet, slik at man selv slipper å lete etter forskjellen.

SAMARBEIDE MED VERSJONSKONTROLL Det er vanlig å lagre en sikkerhetskopi av kodebasen i skyen, slik at kodebasen ikke blir borte dersom en utviklers datamaskin krasjer. Når man først har lagret kodebasen i skyen, blir det mulig for flere utviklere å jobbe med den samme kodebasen samtidig.

Når utviklere lagrer kodebasen sin i skyen og bruker versjonskontroll, har man alltid en sikkerhetskopi av koden. Det blir også lett å samarbeide om koden.

Felles kopi av kodebasen i skyen

Alices datamaskin

Bobs datamaskin

Christines datamaskin

143


144

Kapittel 6

Hvis du har brukt fildelingssprogrammer som lagrer ting i skyen før, har du kanskje opplevd at to personer endrer den samme filen samtidig. Da får man ofte to forskjellige versjoner av en fil som man er nødt til å sy sammen selv. Det samme skjer ofte når man bruker versjonskontroll i et prosjekt, men versjonskontrollprogrammet klarer ofte å sy sammen endringene automatisk uten at man trenger å bry seg. Når programmet ikke klarer det, sier det fra om hvilke kodelinjer og filer det er i tvil om, slik at utvikleren kan bruke sin egen dømmekraft til å sy sammen de ulike endringene. Nedenfor ser du hvordan programmet ville markert en endring det ikke klarte å fikse automatisk i en fil som heter eksempel.txt. 1 Disse linjene i starten av filen var enten uendret, 2 eller så klarte versjonskontrollprogrammet å sy dem 3 sammen av seg selv. 4 <<<<<<< yours:eksempel.txt 5 Denne linjen endret Alice på sin maskin 6 ======= 7 Disse linjene endret Bob på sin maskin 8 >>>>>>> theirs:eksempel.txt 9 Disse linjene var også problemfrie for 10 versjonskontrollprogrammet.

Github er den mest populære skytjenesten for å samarbeide dersom man bruker versjonskontrollprogrammet Git. Andre alternativer er Gitlab og Bitbucket. Hvis du vil programmere mye i fremtiden, er versjonskontroll en av de nyttigste tingene du kan lære seg. Dessverre er ikke versjonskontrollprogrammene spesielt begynnervennlige. Det tar tid å beherske dem godt. Før vi behersker dem, er det lett å gjøre feil som vi ikke har forutsetning for å rette opp i. Github, som eies av Microsoft, er den mest brukte skytjenesten for å jobbe sammen om en kodebase med Git.


Jobbe med IT-prosjekter

в I et IT-prosjekt utvikles et produkt. De som utfører et prosjekt, kalles et team. Et

SAMMENDRAG

team består sjelden av flere enn seks–sju personer. в I fossefallsmetoder er arbeidet delt inn i oppgaver som skal gjøres i en bestemt

rekkefølge: analyse og planlegging, design, programmering og til slutt testing. в I smidige utviklingsmetoder foregår arbeidet med planlegging, utvikling, testing

og evaluering i kortere intervaller kalt iterasjoner eller sprinter. En sprint varer i én–fire uker. Med en slik arbeidsmetode vil en enkel utgave av produktet være klart tidlig i prosjektet. Produktet blir deretter gradvis forbedret. Teamet får hyppige tilbakemeldinger fra oppdragsgiver og brukere. в Oppdragsgiveren er den eller de som har bestilt produktet. в Brukerne er de som skal bruke sluttproduktet. I et prosjekt bidrar ofte represen-

tanter for brukergruppen med tilbakemeldinger og testing av produktet. в Interessentene i et prosjekt kan være representanter både for oppdragsgiveren

og brukerne. Interessentene bidrar ved å komme med utviklingsønsker og gi tilbakemeldinger i sprint-gjennomganger. в Produkteieren bestemmer hvilke deler av et produkt teamet skal lage, og i hvil-

ken rekkefølge. Produkteieren er ofte en del av teamet. в Backloggen er en prioritert liste over hva interessentene ønsker seg. Ønskene kan

være formulert som brukerhistorier med formen «Som <type bruker> vil jeg kunne <utføre en gitt handling>, slik at <verdi for noen>». в Det er vanlig å opprette oppgavetavler. En oppgavetavle har kolonner for opp-

gaver som skal gjøres, oppgaver som pågår, og oppgaver som er ferdige. Tavla kan være fysisk eller digital. Oppgavene skrives på en lapp eller i en boks som kan flyttes mellom kolonnene. På den måten får teamet oversikt over hva som skal gjøres, hva de andre jobber med, og hvilke oppgaver som er ferdige. в Utviklingsteamet må ha kontroll på og tilgang til filene som produseres under-

veis i prosjektet. Det er viktig at den nyeste versjonen av koden er lagret i en mappe. Denne mappen kaller vi fasit-mappen. в Når noen skal lage en ny kodebit, arbeider de i en kopi av den nyeste versjonen.

Når kodebiten er ferdig, ser en annen på teamet over den nye koden som en kvalitetskontroll. Når den nye koden er godkjent, kan den flettes inn i filene i fasit-mappen. Det er viktig å kommunisere med de andre i teamet når man fletter, slik at det ikke er flere som fletter samtidig.

145


Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.