Funksjoner er en av de viktigste byggesteinene i Norscode. De brukes for å gjøre koden mindre repetitiv, lettere å teste og enklere å forstå. Denne siden viser ikke bare syntaksen, men også hvordan funksjoner bør brukes i praksis når prosjekter vokser.
I små eksempler kan nesten hvilken som helst funksjon se grei ut. Utfordringen kommer når en kodebase får flere moduler, flere ruter, flere tester og mer forretningslogikk. Da blir funksjonsdesign en viktig del av både kvalitet, tempo og vedlikeholdbarhet. En god funksjon hjelper neste leser raskt videre. En svak funksjon tvinger leseren til å stoppe opp og tolke hva som egentlig skjer.
Hvis du er tidlig i læringen, holder det å forstå tre ting først: hva funksjonen heter, hva den tar inn, og hva den returnerer. Når det sitter, blir neste steg å se på ansvar, sammensetning og testbarhet. Det er i denne delen mange prosjekter enten blir ryddige eller gradvis tunge å jobbe i.
En god funksjon har et tydelig navn, en tydelig oppgave og et tydelig resultat. Hvis det er vanskelig å forklare med én setning hva funksjonen gjør, er det ofte et tegn på at den prøver å gjøre for mye.
Det betyr ikke at alle funksjoner må være veldig korte. Det betyr at de må være enkle å forstå som en enhet. En funksjon kan godt være noen titalls linjer hvis den fortsatt har ett tydelig formål og en lesbar intern struktur. Problemet oppstår når en funksjon både validerer input, gjør beregninger, bygger tekst, skriver til database og tar beslutninger om neste steg i samme blokk.
funksjon hils(navn: tekst) -> tekst {
returner "Hei, " + navn
}
Navnet bør beskrive handlingen. I praksis fungerer verb som hent_, beregn_, valider_ og formater_ godt fordi de sier noe om hensikten med funksjonen med en gang.
Dette høres enkelt ut, men har stor effekt. Når navnemønsteret er konsekvent, kan en utvikler skanne mange linjer kode og raskt forstå hva som er kontrollflyt, hva som er oppslag, og hva som er ren transformasjon. Det reduserer den mentale kostnaden ved å lese kode.
funksjon beregn_totalt(grunnpris: heltall, mva: heltall) -> heltall {
returner grunnpris + (grunnpris * mva / 100)
}
Når returtypen er tydelig, blir det lettere å se hvordan funksjonen skal brukes videre. Det hjelper både lesbarhet og feilsøking.
Returtypen fungerer også som en kontrakt. Den sier hva resten av systemet kan stole på. Hvis en funksjon returnerer tekst, bør den ikke noen ganger returnere tom struktur eller boolsk verdi bare fordi det er praktisk i øyeblikket. Jo mer forutsigbar kontrakten er, jo lettere blir det å bygge videre uten skjulte antagelser.
funksjon logg_hendelse(tekstlinje: tekst) {
skriv("LOG: " + tekstlinje)
}
Noen funksjoner finnes bare for å gjøre en sideeffekt, som logging eller utskrift. Da trenger de ikke nødvendigvis en eksplisitt returtype.
Dette er et nyttig skille i større prosjekter: noen funksjoner beregner et resultat, andre gjør noe i verden rundt seg. Når du holder dette skillet tydelig, blir kode lettere å teste. Rene funksjoner kan testes direkte med input og output. Sideeffekt-funksjoner kan legges i ytterkantene av systemet, der de er enklere å isolere.
funksjon registrer_kunde(fornavn: tekst, etternavn: tekst, epost: tekst, land: tekst) -> tekst {
returner fornavn + " " + etternavn + " (" + epost + ")"
}
Hvis en funksjon trenger mange parametere, kan det være et tegn på at logikken bør deles opp eller modelleres tydeligere.
Det viktigste er ikke et absolutt tall, men om signaturen er lett å lese riktig. Fire parametere kan være helt fint hvis de hører naturlig sammen og navngivingen er god. To parametere kan være for mange hvis de er uklare eller lett kan byttes om mentalt. Når du leser en funksjonssignatur, bør du raskt forstå hva som må inn, hvorfor det må inn, og hva funksjonen lover tilbake.
En ren funksjon tar input og returnerer output uten å skjule ekstra sideeffekter. Det betyr ikke at hele systemet kan være rent, men at logikken gjerne bør være det så langt det lar seg gjøre.
funksjon lag_visningsnavn(fornavn: tekst, etternavn: tekst) -> tekst {
returner fornavn + " " + etternavn
}
Denne typen funksjon er lett å teste, lett å gjenbruke og lett å flytte på. Når en funksjon derimot både bygger visningsnavn og samtidig logger, lagrer eller sender data videre, blir den raskt vanskeligere å bruke i nye sammenhenger.
funksjon er_gyldig_navn(navn: tekst) -> bool {
hvis navn == "" da {
returner usann
}
returner sann
}
funksjon normaliser_navn(navn: tekst) -> tekst {
hvis navn == "" da {
returner ""
}
returner navn
}
Når funksjonene er små og tydelige, blir de enklere å kombinere senere uten at hovedflyten blir tung å lese.
Her er målet ikke å splitte opp alt kunstig. Målet er å finne naturlige grenser. Validering, normalisering og beregning er ofte gode kandidater for egne funksjoner fordi de representerer ulike typer arbeid. Når de skilles fra hverandre, blir det tydeligere hvor en feil faktisk oppstår.
funksjon rabatt(total: heltall, prosent: heltall) -> heltall {
returner total - (total * prosent / 100)
}
test "rabatt skal senke pris" {
la pris: heltall = rabatt(1000, 20)
assert_eq(pris, 800, "Rabattlogikk feil")
}
Hvis en funksjon er viktig for pris, tilgang, validering eller beslutninger, bør den som regel ha en test ganske nær implementasjonen.
Dette gjelder særlig funksjoner som virker enkle ved første blikk. Mange feil oppstår i små hjelpefunksjoner fordi de blir brukt overalt. En enkel rabattfunksjon kan bli kritisk hvis den brukes i flere steg i en ordreprosess. Når du tester små funksjoner tidlig, fanger du feil før de blir pakket inn i større flyter.
I større kode er det ofte lurt å la én funksjon styre flyten, mens mindre funksjoner tar seg av detaljene.
funksjon basistekst(navn: tekst) -> tekst {
returner "Kunde: " + navn
}
funksjon statustekst(navn: tekst, aktiv: bool) -> tekst {
hvis aktiv da {
returner basistekst(navn) + " - aktiv"
}
returner basistekst(navn) + " - inaktiv"
}
Dette er et av de viktigste mønstrene i praksis. Du vil ofte ha en ytre funksjon som beskriver intensjonen med et steg, mens underfunksjoner gjør detaljarbeidet. Da blir hovedflyten lesbar nesten som en arbeidsbeskrivelse. Samtidig kan detaljene testes separat.
Et godt faresignal er når du begynner å bruke kommentarer inni funksjonen for å forklare flere ulike faser. Hvis du har blokker som egentlig heter “valider input”, “bygg modell”, “lagre”, “formater svar” og “logg”, har du ofte allerede oppdaget naturlige kandidatfunksjoner.
Et annet faresignal er når funksjonen blir vanskelig å navngi presist. Hvis navnet blir veldig bredt, som behandle_kundeforesporsel_og_send_svar, er det ofte fordi flere ansvar er blandet sammen. Da lønner det seg vanligvis å bryte den ned i deler som hver har en tydelig rolle.
Det motsatte problemet finnes også. Hvis du deler opp for aggressivt, kan du ende med en lang kjede av veldig små funksjoner som bare videresender data uten å tilføre tydelig mening. Da må leseren hoppe mellom mange definisjoner for å forstå noe ganske enkelt.
Et godt kompromiss er å dele opp når det gir bedre navn, bedre tester eller tydeligere ansvar. Hvis oppdelingen ikke forbedrer noen av disse tingene, er det ofte greit å holde funksjonen samlet.
valider_* for regler og kontroll av inputnormaliser_* for opprydding og standardiseringberegn_* for tall og logikkbygg_* eller lag_* for sammensetting av strukturerformater_* for tekst og presentasjonlagre_*, send_* eller hent_* for sideeffekter og integrasjonerPoenget er ikke at alle må bruke akkurat disse prefiksene. Poenget er at prosjektet bør ha et gjenkjennelig mønster. Da blir det lettere å lese koden som et system i stedet for som enkeltstående blokker skrevet på ulike måter.
En annen vanlig fallgruve er å la hjelpefunksjoner vokse uten at noen egentlig eier dem. Etter hvert blir de et sted der “litt ekstra logikk” stadig legges til. Det kan fungere en stund, men gjør til slutt koden mindre forutsigbar. Hvis en hjelpefunksjon blir viktig, bør den behandles som en ordentlig del av domenelogikken og få tydelig navn, tydelig kontrakt og gode tester.
valider_*-funksjoner for inputberegn_* eller transform_* for logikkformat_* for utdataDenne oppdelingen gjør det enklere å koble funksjoner til resten av arkitekturen. Web-laget kan validere og rute. Domenelag kan beregne og beslutte. Utdatalag kan formatere svar. Integrasjonslag kan ta seg av database, e-post eller eksterne API-er. Jo tydeligere funksjonene støtter denne delingen, jo lettere er det å holde prosjektet ryddig over tid.
Under ser du forskjellen mellom en funksjon som gjør litt for mye og en struktur som er enklere å jobbe videre med.
funksjon registrer(epost: tekst, navn: tekst) -> tekst {
hvis epost == "" da {
returner "Mangler e-post"
}
skriv("Ny registrering: " + epost)
returner navn + " registrert"
}
Denne fungerer, men blander validering, sideeffekt og presentasjon i én blokk.
funksjon valider_epost(epost: tekst) -> bool {
returner epost != ""
}
funksjon lag_registreringsmelding(navn: tekst) -> tekst {
returner navn + " registrert"
}
funksjon registrer(epost: tekst, navn: tekst) -> tekst {
hvis ikke valider_epost(epost) da {
returner "Mangler e-post"
}
skriv("Ny registrering: " + epost)
returner lag_registreringsmelding(navn)
}
Den andre versjonen er fortsatt enkel, men den gir tydeligere byggeklosser. Når logikken vokser, er dette som regel langt enklere å vedlikeholde.
Den viktigste lærdommen er ikke at alle funksjoner må se like ut, men at de bør være lette å lese riktig. Gode funksjoner gjør prosjektet mer forutsigbart. De reduserer feil, gjør testing enklere og hjelper nye utviklere raskere inn i koden. Når du er i tvil, er det ofte lurt å spørre: har denne funksjonen ett tydelig ansvar, en tydelig kontrakt og et navn som faktisk beskriver det den gjør?
Når dette