Föreläsning 4 – Formatering
Läsanvisningar
Section titled “Läsanvisningar”
Clean Code- Ch. 5
- Ch. 6
Föreläsning
Section titled “Föreläsning”Inspelningen har tre delar: en genomgång av Clean Code kapitel 5 (formatering), några objektorienterade koncept inför Laboration 3, och ett livekodningsavsnitt där arbetet med spelet Ripe Tomato fortsätter. Daniel föreläser och Mats flikar in med frågor och egna perspektiv – och på flera punkter, framför allt kring MVC, tycker de olika. Det är en poäng i sig: det finns sällan en enda rätt lösning, men det sätt du väljer ska vara tydligt och konsekvent.
Föreläsningen: formatering (Clean Code kapitel 5)
Section titled “Föreläsningen: formatering (Clean Code kapitel 5)”Föreläsningen bygger på kapitel 5 i första upplagan (kapitel 6 i andra upplagan) och har inte uppdaterats sedan förra kursomgången.
Filstorlek
Section titled “Filstorlek”Bokens diagram (logaritmisk skala, en boxplot per projekt) visar hur långa filerna är i ett antal Java-projekt – JUnit, FitNesse, testNG, Tam, JDepend, Ant och Tomcat. Skillnaderna är stora: i Tam ligger den största filen runt 350 rader, medan Tomcat har filer på över 5 000 rader. Skillnaderna speglar ofta olika lag med olika regler, till exempel en övre gräns för filstorlek.
Små filer är lättare att förstå än stora. Men “så små som möjligt” är inte heller målet – tre rader
per fil skulle ge en explosion i antalet filer. Det är en avvägning, och filstorleken är bara en
aspekt: en större fil kan ändå ha high cohesion och low coupling. Som avskräckande exempel visades
main/main.c i PHP:s källkod, som i år är 2 792 rader (tidigare mätningar: 2 749 och 2 476) – den
pendlar upp och ner med ett par hundra rader, och utan en tydlig intern struktur är den svår att ge
sig in i som ny programmerare.
En stor fil kan fungera om du har en “inre karta” över den – men den kartan finns i den ursprungliga utvecklarens huvud, inte hos nästa. Dessutom finns ett nytt skäl att hålla filerna små: en AI-agent som ska arbeta i koden behöver läsa in filerna, och stora filer kostar input-tokens (och pengar). Kod som går att hitta i från lite kontext gynnar både människor och agenter. Samma resonemang gäller testfiler: hundra testfall för en enda metod blir hundra ställen att ändra när metodens signatur ändras.
Läs som en tidning
Section titled “Läs som en tidning”Koden läses vertikalt, som en tidningsartikel:
- Rubriken motsvarar klassnamnet (och oftast filnamnet). Den ska tala om vad klassen handlar om – inget “clickbait”, där namnet lovar något annat än innehållet. Klassnamnen är kartan som både människor och agenter använder för att hitta rätt fil.
- Ingressen motsvarar de publika metoderna, på hög abstraktionsnivå: anrop till egna metoder och klasser, med domänens begrepp i stället för heltal och strängar.
- Detaljerna motsvarar de privata metoderna, som kan ligga på låg abstraktionsnivå.
Det är samma princip som overview first, details on demand: först en översikt, sedan detaljerna när du har hittat rätt ställe.
Vertikal formatering
Section titled “Vertikal formatering”- Vertikal öppenhet – separera koncept med blankrader. Varje grupp av rader representerar en fullständig tanke, som ett stycke i en text. Det gäller också inom en funktion. En inline-kommentar hör till raderna under den, så ingen blankrad mellan kommentaren och koden.
- Vertikal täthet – rader som hör tätt ihop ska stå tätt ihop, till exempel
margin,leftSpaceochbottomSpacei en klass som ritar boxplots. - Vertikalt avstånd – avståndet mellan två saker ska spegla hur viktiga de är för att förstå varandra. En metod kan bara ligga på ett ställe, så det blir aldrig perfekt. Utgå från anropsordningen, men metoder kan också vara kopplade på andra sätt, till exempel en som skapar en datastruktur och en annan som konsumerar den.
- Beroende funktioner – den anropande funktionen ovanför den anropade.
- Konceptuell närhet – funktioner som gör liknande saker ska ligga nära varandra, till exempel
överlagrade
assertTrue-metoder i JUnit eller event-hanterarnamouseUpochmouseDown.
Mats frågade om privata egenskaper ska dokumenteras var och en, eftersom kommentarerna syns i editorns IntelliSense. Daniels svar: det beror på filens storlek. Om deklarationen och användningen syns på samma skärm går typen och betydelsen att avkoda direkt; används variablerna på många ställen i en lång fil ökar värdet av en kommentar (särskilt i äldre PHP-versioner, som saknar typade egenskaper).
Var deklareras variabler?
Section titled “Var deklareras variabler?”- Lokala variabler deklareras så nära användningen som möjligt. Mats har en variant: överst i det
block där variabeln används, oavsett om blocket är en funktion eller en
if-sats. Daniels argument för bokens variant: variabeln och koden som använder den hör ihop och följer med tillsammans när du senare bryter ut raderna till en egen privat metod. - Instansvariabler ska ligga på ett väl känt ställe – i C++ traditionellt längst ner, i Java
längst upp – och grupperas efter relation (
xnäray).
Kursens regel: följ vad kursboken säger, eller hur kursledningen tolkar den. Du behöver inte hålla med, men det ger en gemensam sanning att förhålla sig till – och avvägningar du gör är bra material för dina reflektioner. Mats lyfte också värdet av att faktiskt pröva en stil innan du förkastar den. Daniel uppmanade på samma sätt att skriva små funktioner i laborationerna även om det känns ovant, så att du visar att du kan – sedan kan du hitta din egen nivå.
Horisontell formatering
Section titled “Horisontell formatering”De flesta rader i bokens Java-projekt är korta, och programmerare föredrar korta rader. Hur lång en
rad får vara diskuterades länge: från runt 70 tecken (en tryckt boksida) och 80 tecken (äldre
terminaler) till dagens breda skärmar. Mats vill kunna läsa en rad utan att flytta blicken i sidled.
Daniel tycker att det är bra att kunna ha flera filer bredvid varandra utan att scrolla horisontellt.
Han uppskattade att GitHub visar ungefär 125 tecken utan scroll, och kursens mallprojekt är
konfigurerade med Prettier för 120 tecken (printWidth: 120) – formateringsverktyget sköter alltså
radlängden åt dig.
Sätt att korta raderna:
- Bryt upp beräkningar och lägg delarna i namngivna variabler (
isOldEnough = ageInYears > AGE_LIMIT) eller i privata metoder. Ett långt villkor somisset($studentRankings[$student]) && !(... < 1.30 && ... > 0.0)blirstudentRankings.hasValidRanking(student). Du höjer abstraktionsnivån. - Radbrytningar i långa uttryck, till exempel efter en
&&, eller i en lång argumentlista. En lång argumentlista är dock oftast ett tecken på att funktionen tar för många argument – noll argument är bäst.
Daniel berättade också att en AI-modell han provade skrev mycket långa rader som standard, tills den uttryckligen ombads skärpa sig.
Indentering och lagets regler
Section titled “Indentering och lagets regler”Indentering är avgörande för kodförståelsen: du bygger mönster i hjärnan för att läsa kod, och
oformaterad kod tvingar dig att kämpa (och kan väcka irritation hos läsaren). Clean Codes
rekommendation är att följa lagets schema även om du tycker att du vet bättre. Bryt inte heller
indenteringen för korta konstruktioner, som en enradsfunktion eller en while-loop med en tom
kropp.
I JavaScript finns ingen inbyggd standard utan flera konkurrerande: Google, Airbnb och Standard, där den mest synliga skillnaden är att Standard utelämnar semikolon. Var klammerparentesen ska stå väcker starka känslor (Daniel lärde sig C++ och kände närmast för att slåss när Java ville ha den på samma rad). Mats regel: rätta dig efter språkets och ramverkets egen konvention – på samma rad i JavaScript och Java, på egen rad i C# – och “våldför dig inte” på språket eller ramverket; konstruktörerna har oftast genomtänkta skäl. Daniels tillägg: det är lagets standard som gäller. Tycker laget olika – rösta, och följ beslutet. Play by the team rules.
Föreläsningen: objektorienterade koncept inför Laboration 3
Section titled “Föreläsningen: objektorienterade koncept inför Laboration 3”Den andra delen repeterar objektorienterade begrepp som du sannolikt mött tidigare. Laboration 3 kräver objektorienterad kod, och kursledningen vill se att du har brottats med de här frågorna och beskriver avvägningarna i din reflektion.
Modultänk och bakåtkompatibilitet
Section titled “Modultänk och bakåtkompatibilitet”Tänk dig att din modul från Laboration 2 redan används av andra applikationer i den form du lämnade in den. Bryt därför inte gränssnittet. Mats påpekade att det i verkligheten går att göra brytande ändringar enligt ett protokoll, till exempel deprecation och en ny major-version. Daniel svarade att det är samma tänk: hänsyn till användarna. Det kan kräva eftergifter i form av mer kod, till exempel en adapter eller en klass du helst hade tagit bort. Vill du ta bort något ur modulen kan du skapa ett nytt gränssnitt utan delen och samtidigt stödja det gamla – se Laboration 3 för hur kursen vill att du hanterar föråldrade delar.
Vilka klasser bör finnas?
Section titled “Vilka klasser bör finnas?”- Modellens klasser berättar om domänen.
Board,GameModel,Move,PlayerochPositionavslöjar direkt att det handlar om ett brädspel där pjäser flyttas. (Mats påpekade den inkonsekventa namngivningen –gameModel.jshar suffix men inte de andra, ochPosition.jshar stor begynnelsebokstav till skillnad från övriga filer. Daniel bjöd medvetet på egna fula kodexempel: “även solen har sina fläckar”.) - Kontrollern berättar vad användaren kan göra – samlingar av actions. Följ ramverkets konvention.
- Vyn berättar hur applikationen interagerar med användaren.
Utgå från problemdomänen: i Super Mario finns en spelarkaraktär, power-ups, fiender och olika typer av tiles (förstörbara, oförstörbara, bonus, rör). Molnen och buskarna går däremot inte att interagera med. De är ren dekoration och hör bara hemma i vyn, inte i domänmodellen. Poängen är troligen bara ett heltal i modellen, men kan ha en egen vy.
Cohesion, information expert och feature envy
Section titled “Cohesion, information expert och feature envy”Metoder som arbetar med samma medlemsvariabler hör hemma i samma klass. mouseUp och mouseDown
använder båda #mouseWasPressedOnArea och #activeToolView – det är high cohesion. (Exemplet hade
själv brister, bland annat en olämplig radbrytning och en tveksam användning av parseInt.)
Information expert – den som har informationen som krävs för en uppgift utför den – leder till information hiding, som leder till högre abstraktion: fler saker kan vara privata, helst inga publika getters, och omvärlden får en enklare bild med färre metoder. Feature envy – flera anrop i rad till ett annat objekt för att komma åt dess data – lockar till fler getters. Fundera i stället på om funktionaliteten hör hemma i det andra objektet, eller i en tredje klass.
Kommunicera med objekt, inte primitiva datatyper
Section titled “Kommunicera med objekt, inte primitiva datatyper”Mellan dina egna klasser skickar du objekt av dina egna klasser, inte strängar och heltal: en
Password-klass i stället för en sträng. Målen:
- Högre abstraktionsnivå – klasserna pratar med domänens begrepp.
- Typsäkerhet – utnyttja den om språket har den.
- Våga lita på koden – om ett
Passwordvalideras innan objektet skapas kan ettPasswordaldrig vara ogiltigt, och du behöver inte leta någon annanstans för att veta det. - Mindre duplicering.
Primitiva datatyper används bara vid gränserna, till exempel när koden pratar med DOM:en, HTTP eller en canvas.
Arkitekturregler och MVC
Section titled “Arkitekturregler och MVC”Ett exempel på en arkitekturregel: all domänlogik ska finnas i modellen, inte i vyn eller kontrollern. Fördelen med att välja en arkitektur och hålla sig till den är att den som känner till arkitekturen får en karta in i ditt program. Daniels regel är att vyn ansvarar för både indata och utdata. Den kommer från spelutveckling: vyn vet var en knapp är ritad och är därför information expert på om ett klick träffade den, och vid multiplattformsspel (webbläsare, mobil, Steam, Xbox) är vyn den enda komponent som behöver bytas. Vyn har bara läsrättighet till modellen, medan kontrollern får ändra i den. Mats ser i stället HTTP-flödet framför sig: användarens händelser går till kontrollern, och vyn bara presenterar. Det är just därför de har svårt att förstå varandra – de använder samma begrepp med olika innebörd.
I Laboration 3 behöver du inte följa någon av deras varianter, men du ska välja ett tydligt sätt och hålla dig till det. (Var databasen hör hemma? Oftast känner modellen till den via en adapter – och i verkliga applikationer finns ofta fler lager, till exempel service- och repository-lager.)
Osynliga beroenden och strängberoenden
Section titled “Osynliga beroenden och strängberoenden”Leta efter osynliga beroenden och synliggör dem. Ett <input name="..."> i HTML och koden som läser
samma namn ur HTTP-requesten är ett strängberoende: samma sträng finns på två ställen, och ingen
kompilator kontrollerar att de stämmer. Andra exempel är filnamn och sökvägar, databastabeller och
fält, och positioner i arrayer:
return [username, password] // olika konceptuella typer i samma arrayreturn { username, password } // missar chansen till en namngiven abstraktionreturn new UserCredentials(username, password) // gör så härHantera strängberoenden genom att dokumentera dem, eller ännu hellre genom att låta strängen finnas på ett enda ställe, till exempel som en privat medlemsvariabel i vyn som både skapar formuläret och läser datan.
Kapsla in beroenden
Section titled “Kapsla in beroenden”Kapsla in lagring (local storage, databaser) bakom ett mellanlager, till exempel ett repository, så
att resten av koden inte vet om det är en databas eller en JSON-fil. Låt inget läcka igenom: ingen
saveToDatabase, utan snarare något i stil med store. Isolera dig på samma sätt från extern kod,
till exempel den andra studentens modul, som troligen kommer att ändras: skapa en egen klass med ett
eget gränssnitt som pratar ditt domänspråk. Två delar av din kod ska inte prata med varandra genom
tredje part.
Dependency injection
Section titled “Dependency injection”En klass får sina beroenden utifrån, typiskt via konstruktorn, i stället för att skapa dem själv. Det:
- förenklar automatiska tester (möjliggör mockning),
- gör koden konfigurerbar och utbyggbar,
- förenklar polymorfa konstruktioner – vilken subklass eller implementation som helst av rätt gränssnitt kan skjutas in,
- gör konstruktorn till ett recept för hur objektet byggs upp,
- minskar antalet argument till metoderna, eftersom gemensamma argument kan flyttas till konstruktorn.
Beroenden kan injiceras via konstruktorn, via metoder eller via egenskaper. Konstruktorn är
vanligast och både Mats och Daniel föredrar den – då kan objektet aldrig vara halvfärdigt, utan
initialize-metoder eller setters.
Hierarkier av klasser och var valideringen sker
Section titled “Hierarkier av klasser och var valideringen sker”Allt behöver inte ligga i en klass: en kontroller som växer kan delas upp i en master controller med
subkontrollers (LoginController, RegisterController), och vyer kan använda andra vyer. Att bryta ut
en privat metod till en publik metod i en ny klass löser också problemet att privata metoder är
svåra att testa utifrån – Daniels hållning är att testa det publika och “hold the line” kring
abstraktionen.
Valideringen ska ske när modellobjektet skapas, så att det aldrig finns ett ovaliderat objekt. (Formulärdata som användaren håller på att fylla i kan behöva en egen, ovaliderad representation.) Mats nyansering, som Daniel höll med om: initiera valideringen från konstruktorn, men lägg den inte nödvändigtvis i själva konstruktorn – om en setter senare behöver samma validering ska den inte dupliceras. Bryt ut när dupliceringen uppstår, inte i förväg (det vore overengineering): först en privat metod, och en separat validator först när minst två klasser behöver den.
Livekodning: Ripe Tomato, pass 3
Section titled “Livekodning: Ripe Tomato, pass 3”Inför passet bytte Mats och Daniel roller – Mats kodade och Daniel navigerade. Mats beskrev
först sin bild av problemet: de kan enas om variabelnamn och klassnamn, men utan en gemensam bild av
arkitekturen vet han inte vad Daniel vill bygga. Ett förslag som CoordinateConverter kan visa sig
bortkastat när han inte känner till helheten. Det handlar alltså om mer än kod – klassdiagram,
sekvensdiagram och en beskriven arkitektur behövs innan det blir effektivt att prata kod.
Mats ritade upp sin syn på MVC i p5.js: p5:s huvudloop anropar draw ungefär 60 gånger per sekund,
och händelser som mousePressed och keyPressed landar i p5-loopen och skickas därifrån till
kontrollern, som uppdaterar modellen. Vyn läser från modellen och ritar. Daniel såg inte synsätten
som oförenliga: för honom har p5 i sig vy-egenskaper (det skapar output och hanterar input), och han
ville ha ett mellanlager för input.
Från Tomato till GameCharacter
Section titled “Från Tomato till GameCharacter”Passet började med refaktorering. Modellen Tomato döptes om till GameCharacter – en ny
programmerare förstår direkt vilken karaktär spelaren styr (jämför “don’t be cute”). Namnet diameter
lyftes fram som bra, eftersom size lämnar öppet om det är radien eller diametern. Konstruktorn
borde validera att koordinaterna är tal och att diametern är större än noll. I passet blev det en
TODO, men efteråt implementerades valideringen med Number.isFinite (eftersom typeof NaN är
'number'), plus ett RangeError för en diameter som inte är positiv.
Ska tomaten känna till sin egen färg? Daniels svar: modellen kan hålla en logisk färg (till exempel “röd”), som vyn översätter till en specifik RGB-färg, en gråskala på en svartvit skärm eller en sprite. En RGB-färg hör däremot inte hemma i modellen.
Game – en modell för spelets tillstånd
Section titled “Game – en modell för spelets tillstånd”GameView.drawTomato tog x, y och diameter som separata argument. Nu skickas i stället hela
GameCharacter-objektet in, och omvandlingen till primitiva värden sker vid gränsen mot p5. Mats
föreslog destrukturering för att visa vilka tre egenskaper som används, men duck typing räckte här.
En ny modellklass, Game, fick ansvaret för spelets aktuella tillstånd. Ett nytt spel ger ett nytt
Game-objekt, med en ny GameCharacter på startpositionen. Game injiceras i GameViews
konstruktor, så att draw inte längre behöver några argument och vyn själv läser karaktären via en
getter. Mats påpekade direkt att gettern ger en privacy leak: den returnerar en referens till
ett privat fält, och vyn skulle kunna ändra objektet. Detta dokumenterades i en kommentar i stället
för att lösas. Ground height, som låg som en konstant i vyn, är egentligen en spelregel och flyttades
till Game (kanske senare till en Level).
Modellen ska däremot inte känna till vyn. Ska något visuellt hända på grund av något i modellen – ketchup som sprutar när tomaten slår i marken – kan Observer-mönstret (en listener) användas som indirektion.
GameController och top-down-design
Section titled “GameController och top-down-design”För att tomaten ska kunna röra sig tillkom en GameController med en update-metod som anropas från
p5:s draw. Kontrollern får Game, GameView och en Input via konstruktorn. Namnet valdes med
tanke på framtiden: en meny får en MenuController, ett kartläge en MapController och så vidare.
Daniel designade top-down: han skrev anrop till klasser och metoder som ännu inte fanns, på
hög abstraktionsnivå – if (this.#input.playerWantToJump()) { this.#game.jump() } – och lät
implementationen komma senare. Input placerades bland vyerna och returnerar tills vidare alltid
true, med en TODO om tangentbord, mus och liknande. Därmed skjuts beslutet om hur input ska
hanteras upp, oavsett plattform. Mats frågade varför GameView har suffixet View men inte Input.
Daniels svar: GameView är spelets “master view”, medan Input bara abstraherar inmatningen. Det
avgörs i Game om karaktären får hoppa – bara den känner till både karaktären och marken – med en
privat #isGameCharacterOnGround (Mats valde is-prefixet för en boolesk egenskap).
Mats frågade om top-down-kodning inte bara är ett tecken på att designarbetet inte är gjort. Daniel menade att han har en domänmodell i huvudet efter att ha skrivit flera spel, men höll med om att en regel som “man måste vara på marken för att hoppa” (i vissa spel finns double jump) är ett beslut som borde ha kommunicerats först.
Passet slutade med kod som inte fungerade fullt ut. Efter passet rättades bland annat att Input
aldrig skickades till kontrollern, att den privata gettern anropades som en funktion och att
GameCharacter saknade en jump-metod (själva hoppet återstår som en TODO). Koden finns i det öppna
GitHub-repot ripe-tomato.