Föreläsning 2 – Funktioner och livekodning
Läsanvisningar
Section titled “Läsanvisningar”Färgförklaring för chipsen nedan:
- Läs
- Skumma
- Hoppa över
Clean Code- Ch. 3
- Ch. 7
- Ch. 8
- Ch. 9
- Ch. 10
- Ch. 11
Föreläsning
Section titled “Föreläsning”Föreläsningen har två delar: en genomgång av Clean Code kapitel 3 (funktioner), följt av ett livekodningsavsnitt där principerna tillämpas på ett litet spel som byggs från grunden.
Föreläsningen: funktioner (Clean Code kapitel 3)
Section titled “Föreläsningen: funktioner (Clean Code kapitel 3)”Reglerna nedan ramas in som tumregler snarare än absoluta lagar – det finns undantag och tillfällen då det är motiverat att bryta mot dem, så länge du vet att du bryter mot dem och varför. Målet med Laboration 2 är inte att följa varje regel perfekt, utan att ha reflekterat över var du valt att följa dem och var du valt att avvika.
Kodförfattaren och kodanvändaren
Section titled “Kodförfattaren och kodanvändaren”Ett återkommande perspektivskifte: som utvecklare är du oftare kodanvändare än du tror. Som kodförfattare tillhandahåller du ett gränssnitt (funktioner, klasser, interface, API:er) samt stöd för att förstå det – bra namngivning, dokumentation, exempel. Som kodanvändare har du ett funktionsbehov, anropar metoder och skapar instanser, och försöker förstå kodförfattarens avsikt genom att läsa dokumentation, experimentera och felsöka. Skiftet i Laboration 2 jämfört med tidigare labbar är att du för första gången aktivt ska ha båda perspektiven i huvudet samtidigt, inte bara lösa ditt eget problem som författare.
Funktioner bör vara små
Section titled “Funktioner bör vara små”Clean Code driver principen långt: en funktion ska inte vara stor nog för att innehålla nästlade
kontrollstrukturer (en if i en if, till exempel), och det ska helst inte gå att extrahera
ytterligare en funktion ur den utan att resultatet bara blir en omskrivning av samma implementation.
Föreläsaren var tydlig med att detta är en medveten extremposition – även erfarna utvecklare följer
den sällan fullt ut i praktiken – men att Laboration 2 är ett tillfälle att öva på att driva
uppdelningen i funktioner så långt det går, just för att få erfarenheten av var gränsen känns rimlig
att dra. Skillnaden demonstrerades med guess the number-spelet skrivet som en enda lång
main-metod (svår att läsa på tre sekunder) jämfört med samma program uppdelat i flera korta,
namngivna metoder (startNewGame, guessIsLower, showGuessPrompt) som var och en gör en sak.
Abstraktionsnivåer
Section titled “Abstraktionsnivåer”En abstraktion döljer implementationsdetaljer och möjliggör separation of concerns: den som
använder en funktion ska kunna lita på namnet och kontraktet utan att behöva läsa innehållet. Kod på
hög abstraktionsnivå anropar projektets egna klasser och metoder, skapar inga magiska tal och
beskriver en ordning mellan anrop (runOneGame → model.startNewGame() → runGameLoop()). Kod på
låg abstraktionsnivå arbetar med primitiva datatyper, API-anrop, strängkonkatenering,
jämförelseoperatorer och beräkningar – den är oftast plattforms- och språkberoende, medan kod på hög
abstraktionsnivå ofta går att flytta mellan språk och ramverk med små justeringar.
Regeln håll en abstraktionsnivå per funktion: blanda inte de två i samma funktion. Namnet på en
låg-nivå-funktion (randomizeCorrectAnswer, resetNumberOfGuesses) höjer själva
abstraktionsnivån utåt, även om innehållet fortsatt är lågnivåkod.
Sidoeffekter
Section titled “Sidoeffekter”En sidoeffekt är en observerbar påverkan på något utanför funktionens lokala beräkning, som inte
framgår av namnet, interfacet eller kontraktet. Exemplet mouseUp(x, y) som i smyg skriver till
konsolen är ofarligt isolerat, men blir ett problem i större skala när loggutskrifter stör
felsökningen. Ett annat exempel: calculateTotal(prices) som förväntas returnera en summa men även
sparar undan värdet i localStorage – om namnet istället hade varit calculateAndSaveTotal (eller
liknande) hade det inte längre räknats som en sidoeffekt, eftersom då är det just vad namnet lovar.
Problemet med sidoeffekter är att de förstör tilltron till abstraktionen: du kan inte längre lita på
funktionsnamnet ensamt utan måste läsa koden eller testa noggrant för att upptäcka dem.
Nära besläktat: toSorted() jämfört med sort() i JavaScript – sort() muterar och sorterar den
mottagna arrayen på plats (en sidoeffekt på argumentet), medan toSorted() returnerar en ny,
sorterad kopia utan att röra originalet.
Command Query Separation
Section titled “Command Query Separation”En funktion bör antingen utföra en handling (command) eller svara på en fråga (query) – inte
båda samtidigt. Exemplet removeLastLine() bryter mot principen: den tar bort sista raden
(command), sparar undan resterande data i localStorage (ytterligare en sidoeffekt) och returnerar
samtidigt de borttagna raderna (query). Mönstret att blanda get och set i samma metod är särskilt
vanligt vid felhantering, till exempel äldre PHP-kod som försöker ansluta till en databas och
returnerar false vid fel men annars fortsätter att utföra en handling och returnera true.
Namngivning av funktioner
Section titled “Namngivning av funktioner”Låt funktionsnamnet hinta om returvärdet: isConnected() och hasAllInformation() signalerar en
boolean, getCustomer() signalerar ett Customer-objekt (eller ett Person-objekt – vilket också
visar att namnet bör vara tillräckligt specifikt för att undvika den frågan). Ett namn som
getRawInputs() är ett exempel på ett oklart returvärde: om du måste stiga in i funktionen eller
debugga anropet för att förstå vad som kommer tillbaka är namnet, eller returtypen, otydligt.
Föredra att returnera objekt av projektets egna, namngivna klasser framför anonyma objekt eller
arrayer, så att en editor kan visa typen direkt vid hovring.
Blanda inte olika returtyper mellan lyckade och misslyckade anrop (till exempel ett giltigt objekt
i vanliga fallet men 0, false eller undefined vid fel) – det tvingar den som anropar att lägga
in extra kontroller på varje anropsplats. Två etablerade lösningar diskuterades:
- Null object pattern – returnera ett harmlöst objekt (t.ex. en
GuestUsersom implementerar samma interface som en inloggadUsermen svararfalsepåcanEdit()) istället förnull. Fördelen är att allaif-kontroller för null försvinner i anropande kod; nackdelen är risken för silent errors – koden fortsätter köra i sitt normala spår utan att någon egentligen får reda på att något gick fel. - Exceptions istället för felkoder – kasta ett undantag (
throw new Exception(...)) snarare än att returnera magiska felvärden som-1,99ellerfalse. Om du fortfarande tycker undantag känns osäkra är Laboration 2 ett bra tillfälle att öva på att faktiskt använda dem.
Gömda beroenden och tillstånd
Section titled “Gömda beroenden och tillstånd”Håll det som måste vara känt utanför en funktion eller klass till ett minimum – det handlar i grunden
om att abstrahera rätt saker. Jämförelsen drawBoxPlot(jsonData) mot drawBoxPlotAsSvgString(numbers, width, height) illustrerar avvägningen: den första kräver att anroparen känner till JSON-strukturen
och var resultatet ritas, den andra är explicit om vad som krävs (en array med tal, en bredd och en
höjd) och vad som kommer tillbaka. Rekommendationen för Laboration 2, om du använder ett bibliotek:
skriv en liten testkodsnutt som faktiskt använder biblioteket, och låt en kurskamrat läsa den utan
att ha skrivit den själv – kan hen förstå vad som behövs och vad som händer utifrån bara anropet?
Namnge funktioner som du annars skulle ha kommenterat
Section titled “Namnge funktioner som du annars skulle ha kommenterat”Om du känner ett behov av en kommentar inne i en funktion – bryt ut den koden till en egen funktion
och använd kommentartexten som funktionsnamn istället. En kommentar ovanför en kort funktion är ofta,
i praktiken, ett bättre funktionsnamn som ännu inte flyttats dit. Svårigheten att hitta ett bra namn
är själv ett tecken: om funktionen gör mer än en sak blir den svår att namnge kort och träffande –
dela upp den. Använd gärna ett verb i namnet, låt namnet antyda returvärdet, och låt argumenten ge
ytterligare ledtrådar om vad funktionen gör. Sikta på att anropet object.method() går att läsa som
en mening, till exempel customers.add(customer).
Håll antalet argument lågt
Section titled “Håll antalet argument lågt”Fler argument kräver mer mental ansträngning, gör funktioner svårare att testa och försämrar läsbarheten. Helst inga argument alls, och undvik fler än två. Undantaget är konstruktorer, där längre argumentlistor är mer accepterat – men om flera argument alltid hör ihop (till exempel gatuadress, postnummer, ort och land som tillsammans bildar en adress) är det ett tecken på att de egentligen utgör ett eget koncept eller en egen klass. I JavaScript kan destrukturerande tilldelning användas för att ta emot bara de egenskaper en funktion faktiskt behöver ur ett objekt, snarare än många separata argument eller ett stort objekt med tio egenskaper.
Funktioner med ett argument (monadiska funktioner) delas in i tre typiska användningsfall:
- Fråga:
isValidEmail(email)– ställer en ja/nej-fråga om argumentet. - Transformera:
sanitizeHtml(input)– tar in ett värde och returnerar en bearbetad version. - Event callback:
handleClick(event)– reagerar på en händelse.
Två eller tre argument (dyadiska/triadiska funktioner) är mer acceptabelt i konstruktorer, till
exempel new Point(x, y, z).
Referensargument (writeTo(output) där output muteras av anropet) är en form av dold
sidoeffekt, eftersom parameterens tillstånd förändras utan att detta alltid syns tydligt i namnet –
särskilt lätt att missa i språk där call-by-reference inte är uppenbart. Att istället låta funktionen
returnera det uppdaterade objektet (htmlWriter = loginView.write(htmlWriter)) gör beroendet
tydligare, även om det, som diskuterades, inte är renodlad Command Query Separation eftersom
funktionen fortfarande utför en handling.
Livekodning: Ripe Tomato
Section titled “Livekodning: Ripe Tomato”Efter genomgången byggde föreläsaren och en medhjälpande lärare tillsammans ett litet plattformsspel (“Ripe Tomato”) i vanilla JavaScript (ES-moduler) med biblioteket p5.js i instansläge, för att visa principerna i praktiken snarare än på slides. Några konkreta beslut under kodningen:
- Ett magiskt tal (
20, en förskjutning för var titeltexten ritas) bröts ut till en namngiven konstant. Namnkonventionen som användes: UPPER_SNAKE_CASE för konstanter vars värde är bestämt vid skrivtillfället (t.ex.TITLE_TEXT_TOP_DISPLACEMENT), men camelCase för värden som beräknas i koden (t.ex.20 * 2hade fått ett camelCase-namn eftersom det då är ett uträknat värde, inte en konstant satt direkt i källkoden).Skillnaden som beskrevs (UPPER_SNAKE_CASE kontra camelCase) handlar om huruvida värdet är ett bokstavligt värde skrivet direkt i koden eller ett beräknat uttryck – inte om språkets
const- nyckelord i sig. Ettconst-deklarerat värde som beräknas vid körning (const total = a + b) räknas alltså inte som en konstant i den meningen som avgör namnkonventionen här. - En blandad rad ritkod (bakgrund + titeltext) bröts ut till två separata funktioner,
drawBackgroundochdrawTitle, för att hålla en abstraktionsnivå per funktion.drawTitle-funktionens genererade kommentar jämfördes med funktionsnamnet för att se om kommentaren innehöll ord (t.ex. att en canvas används) som avslöjade en implementationsdetalj som hellre bör döljas än läggas till i namnet. - Ett eget koordinatsystem infördes för spelvärlden (“tomater” som måttenhet, där
1motsvarar en tomats höjd) separat från skärmens pixelkoordinater – ett klassiskt exempel på model-koordinater kontra skärmkoordinater. Konverteringen mellan de två extraherades till en egen funktion,convertToCanvasCoordinates, som sedan bröts ut till en egen klass (CameraView) eftersom dess ansvar (att räkna om koordinater) skiljer sig tydligt från de övriga funktionernas ansvar (att rita ut saker) – ett konkret exempel på att en avvikande abstraktionsnivå eller ett avvikande ansvarsområde inom en funktion är en signal om att bryta ut en ny klass. - Tomatens position och storlek, som ursprungligen låg hårdkodade inuti ritfunktionen, flyttades ut
till parametrar (
centerPositionX,centerPositionY,diameter) för att göra dem möjliga att styra utifrån, till exempel via tangentbord eller mus i en senare version.
Fortsättningen (att bryta ut ritkoden till en vy-klass och modellera tomatens tillstånd i en egen modellklass) sparades till nästa föreläsnings livekodningsavsnitt.
Koden som byggs under livekodningen finns i det öppna GitHub-repot
ripe-tomato och växer fram commit för
commit, direkt på main, allteftersom fler livekodningspass hålls. Vill du utgå från exakt samma
startpunkt som föreläsningen började från: repot har startläget bevarat som taggen start-here,
som aldrig ändras. Efter varje pass sammanfattar en fil i repots rot – döpt SESSION_N.md, där N
är passets nummer – vad som byggdes och varför, skrivet utifrån commit-historiken; efter det första
passet hittar du den som
SESSION_1.md.