Föreläsning 5 – Objekt och datastrukturer
Läsanvisningar
Section titled “Läsanvisningar”
Clean Code- Ch. 6
- Ch. 7
- Ch. 8
- Ch. 9
- Ch. 8
- Ch. 12
- Ch. 14
- Ch. 15
- Ch. 25
- Ch. 26
Föreläsning
Section titled “Föreläsning”Inspelningen har två delar: kursens sista föreläsning, som går igenom Clean Code kapitel 6–8 (objekt och datastrukturer, felhantering och gränser), och en kodgranskning av en alternativ version av spelet Ripe Tomato. I stället för att livekoda står Mats vid tangentbordet medan Daniel läser igenom koden och kommenterar den utifrån det kursen har tagit upp. Föreläsningen är, precis som boken, en samling tumregler.
Föreläsningen: objekt, datastrukturer, felhantering och gränser
Section titled “Föreläsningen: objekt, datastrukturer, felhantering och gränser”Dataabstraktion
Section titled “Dataabstraktion”En rätvinklig triangel kan beskrivas på flera sätt: med bredd och höjd, eller med hypotenusans längd och en vinkel. Känner du till två av värdena kan du räkna ut resten (till exempel med Pythagoras sats). Väljer du en representation och inte berättar vilken, har du abstraherat – du har dolt valet. I programmering är det samma sak när ett repository döljer att det ligger en SQL-databas bakom.
När passar en array?
Section titled “När passar en array?”Exemplet const centerPoint = [12, 7] med kommentaren //x på första platsen visar problemet:
koden som räknar ut radie och vinkel måste veta att x ligger på plats 0 och y på plats 1, och den
kunskapen finns bara i en kommentar. Regeln från bilderna:
- Använd en array när alla element har samma faktiska typ och samma logiska (konceptuella) typ – tänk painted types från första föreläsningen – och du skulle loopa över dem och behandla dem likadant.
- Använd inte en array när elementen har olika painted types. x och y är båda tal, men de betyder olika saker.
Tre exempel diskuterades:
["Daniel", "Toll", danielsPhoneNumbers, 37]– nej, inte ens samma faktiska typ.[userName, password, passwordRepetition]– tre strängar, men olika konceptuella typer. Mats sammanfattade: “allt är frukt, men det är skillnad på äpplen och päron.” Ett lösenord har dessutom egna regler och borde vara en egen typ.danielsPhoneNumbers– det beror på syftet. Är det bara “alla mina telefonnummer” utan särskild ordning går det bra. Betyder positionerna något (mobil, jobb, hem) ska de inte ligga i en array.
Anroparens perspektiv
Section titled “Anroparens perspektiv”En metod som view.getInputArray() leder till att anroparen skriver console.log(input) för att ta
reda på vad som finns var. Då har andra delar av programmet blivit beroende av hur vyn fungerar på
insidan. view.getUserPhoneNumberStrings() säger i stället direkt vad du får. (Mats förslag: en
DTO.) Tvingar du någon i framtiden att konsollogga för att förstå dina data borde du ha gjort på något
annat sätt.
Datastrukturer
Section titled “Datastrukturer”En datastruktur är tydligare än en array, eftersom fälten har namn. Men ett objektliteral som
{ name: "Daniel", lastName: "Toll", ... } har själv inget namn – anroparen vet inte vad den har
fått, och varken verktyg eller AI-assistenter kan slå upp typen. Använd därför klasser även för
datastrukturer: de ger ett namn och en plats för JSDoc. (I JavaScript blir det förvirrande, eftersom
datastrukturer byggs med det språket kallar objekt.)
Mats frågade hur synen på det här har påverkats av bättre utvecklingsverktyg – förr fanns ingen IntelliSense alls. Daniels svar: verktygen ska få så goda förutsättningar som möjligt, men det är förvånansvärt vanligt att läsa kod utan dem (i en terminal, på en server, på en Arduino), och AI-agenter behöver kunna slå upp det de behöver direkt ur koden. Han beskrev också en svängning över tid: när objektorienteringen kom gjordes nästan allt till objekt (inte minst i Java), och sedan dess har man tagit några steg tillbaka och accepterar datastrukturer som DTO:er igen.
En datastruktur har publika data och inga meningsfulla egna funktioner – funktionerna ligger utanför,
spridda i koden. Det är procedurell kod. Så länge representationen inte ändras fungerar det, men
när den ändras måste du leta upp alla ställen som använder den. Mats frågade om det är samma sak som
en POJO (plain old Java object). Ungefär, men i kursen heter det datastruktur, och begreppet är
bredare än ett visst språk – i C++ är det till exempel en struct.
Objekt
Section titled “Objekt”Med kursens definition är ett objekt något annat: det döljer sina data bakom en abstraktion och
exponerar funktioner som arbetar med dem. Klassen Point har privata fält och metoderna
setCartesian, polarRadius och polarTheta. Anroparen känner bara till gränssnittet och vet inte
att punkten lagrar x och y. En alternativ implementation lagrar i stället radie och vinkel – och
anroparens kod är exakt densamma.
Mats påpekade ett fel i bildernas exempel: fälten deklareras som #x och #y, men koden skriver
till this.x och this.y, alltså vanliga publika egenskaper. En uppgift inför Laboration 3, även för
din modul: gå igenom alla dina klasser och fråga dig om datan kan döljas. Deklarera fälten privata
– och använd dem sedan också.
Det här innebär att du kan skriva icke-objektorienterad kod i ett objektorienterat språk. Har en klass inga privata medlemsvariabler skulle dess funktioner lika gärna kunna ligga utanför den, och då är koden egentligen procedurell.
Data/objekt-antisymmetrin
Section titled “Data/objekt-antisymmetrin”| Datastrukturer (procedurell kod) | Objekt (objektorienterad kod) | |
|---|---|---|
| Lätt | Lägga till nya funktioner utan att ändra datastrukturerna | Lägga till nya klasser utan att ändra befintliga funktioner |
| Svårt | Ändra en datastruktur, eftersom alla funktioner måste ändras | Lägga till en ny funktion i ett gränssnitt, eftersom alla implementationer måste ändras |
Daniel ser den största vinsten med objekt i att du kan dela upp ett stort problem: den här klassen löser den här delen, och lösningen döljs bakom en abstraktion. Du kan arbeta top-down, hålla koll på bara vad som går in och kommer ut, och glömma resten tills du behöver ändra där. Mats frågade om kursen gäller även i en funktionell värld. Daniel trodde att det mesta gör det: tydliga namn som visar avsikt, inkapsling och uppdelning av problem är lika viktigt där.
Cohesion
Section titled “Cohesion”Alla metoder i Point använder både x och y – klassen har hög cohesion när det gäller data. Det är
ett mått bland flera. Saker kan också höra ihop på andra sätt, till exempel temporal cohesion
(metoder som alltid anropas efter varandra), eller för att de hör ihop i verkligheten. Granska din
kod från Laboration 2 och Laboration 3: används metoderna och fälten tillsammans? Om ungefär hälften
av metoderna använder x och hälften y borde det kanske vara två klasser. En UserCredentials där
användarnamnet sällan används tillsammans med lösenordet, och lösenordet har egna regler, kan
komponeras av en Username- och en Password-klass.
Law of Demeter och train wrecks
Section titled “Law of Demeter och train wrecks”Varje enhet ska ha begränsad kunskap om andra enheter – bara om de närmaste. Talk to friends, don’t
talk to strangers. En metod f i klassen C får bara anropa metoder på:
Csjälv (this.m()),- ett objekt som
fhar skapat (new C2()), - ett objekt som skickats som argument till
f, - en medlem i
C(this.part.m()).
Anropa inte metoder på objekt som returnerats från någon av dem. Får du tillbaka ett objekt kan du skicka det vidare som argument till en av dina egna metoder, men inte prata med det. Daniel tyckte att lagen borde heta tumregel: mycket går sönder om du följer den strikt (Mats nämnde factory-mönstret), men oftast är den bra att följa.
Exemplet: Game vill ha Marios startposition och skriver
this.mario.getWorld().getStartPosition(). Det är ett train wreck – anrop kopplade efter
varandra som tågvagnar – och ett brott mot Law of Demeter om och endast om anropen returnerar
objekt. För datastrukturer gäller inte regeln. Två lösningar:
- Låt
Gamesjälv känna tillWorld, i stället för att fråga Mario. - Fråga Mario, som är information expert:
this.mario.getStartPosition(). Det ger lägre koppling.
Mats lade till att en publik egenskap i stället för en getter (this.mario.world) bara döljer
samma problem. Håll data privat, så blir det här ett mindre problem. Daniel uppmanade dig att arbeta
in Law of Demeter i Laboration 3: ritar du ett klassdiagram ska det inte gå pilar från allt till
allt, utan du ska behöva känna till några få klasser i stället för femton.
Undvik hybrider
Section titled “Undvik hybrider”En hybrid är hälften objekt, hälften datastruktur: den har publika data (eller kanske getters och setters) och metoder som gör något signifikant – till exempel en publik sträng och en metod som trimmar den. En getter som bara returnerar ett värde är inte signifikant.
Felhantering med undantag
Section titled “Felhantering med undantag”En funktion ska göra en sak, och felhantering är en sak. Använd undantag – det vill kursledningen se i
Laboration 3. Ett PHP-exempel som returnerar false vid fel blir en härva av nästlade if-satser,
och anroparen måste kontrollera returvärdet. Med undantag kastar Folder och File var sitt fel i
konstruktorn, och felet kan fångas på ett enda ställe – i anroparen, eller längre upp i anropskedjan.
Skapa egna undantagsklasser (i JavaScript ärver du från Error), som
PathNotAFolderException och PathNotAFileException, så att felen skiljs åt genom sin typ och inte
genom olika strängar. Språk som Java, C# och PHP har en catch-sats per typ. I JavaScript finns
bara en catch, och du skiljer typerna åt med instanceof. Undantaget kan också skickas vidare, till
exempel till en vy som visar rätt meddelande för just den typen av fel.
Undantagsmeddelanden är inte till för användaren
Section titled “Undantagsmeddelanden är inte till för användaren”Varför inte bara skicka felmeddelandet som en sträng till vyn och visa det? Strängen ligger då kanske djupt nere i en modellklass, men användaren vill ha den på ett annat språk, eller visa felet med färger och bilder. Låt felet vara ett logiskt fel, representerat av sin typ. Text som ska visas för en användare är vyns ansvar.
Meddelandet i ett undantag hamnar oftast i en logg och är till för utvecklaren: vad behöver den
som läser loggen veta? "shit happened" säger ingenting. En student lade till en säkerhetsaspekt:
undantag kan avslöja hemligheter, till exempel var i koden felet uppstod och hur koden är
strukturerad, och lösenord ska inte heller hamna i loggar. Slutanvändare ska alltså inte se
undantagen.
Assert eller undantag?
Section titled “Assert eller undantag?”- Asserts riktar sig till programmeraren: något är fel i koden eller konfigurationen, och
programmet ska avbrytas så att någon felsöker. Exempel:
assert(age >= 13)där en lägre ålder aldrig ska kunna förekomma – då saknas validering någon annanstans. - Undantag riktar sig också till programmeraren, men betyder att något ovanligt hände som ska hanteras: fånga och reagera, till exempel på felaktig inmatning. Applikationen ska fortsätta fungera.
Att krascha låter extremt, men alternativet är att programmet fortsätter i ett felaktigt tillstånd
och att felet skjuts framåt i tiden. I många språk implementeras asserts med undantag, och i
JavaScript heter undantagen Error – håll isär begreppet och språkkonstruktionen. Asserts kan ofta
stängas av med en flagga i produktion. Asserts i tester har samma betydelse: testet har gått sönder,
ändra koden tills det lyckas. Asserts i produktionskod hör till defensiv programmering, som brukar
fungera bäst i typade språk.
Returnera inte null
Section titled “Returnera inte null”Returnerar en metod ibland null eller undefined och ibland en lista, måste varje anropare ha en
if-sats. Returnera en tom lista, eller kasta ett undantag. Ett alternativ är null object pattern:
en klass som implementerar samma gränssnitt men inte gör något (NullAnimal som inte säger något,
eller en utloggad användare vars metoder inte har någon effekt). Använd det med försiktighet – det kan
få fel att se ut som normal körning. Varken Daniel eller Mats brukar använda det.
Tredjepartskod: adapter eller wrapper
Section titled “Tredjepartskod: adapter eller wrapper”Den som erbjuder ett API vill ha många användare och en bred funktionalitet (en multiverktygskniv), medan den som använder det vill lösa ett specifikt problem (en specialskruvmejsel). Tillsammans gör det att moduler du använder tenderar att ändras. Daniel berättade om sin tid som spelutvecklare: först OpenGL, som gick från version 2 till 3 med nya funktioner som geometry shaders, och sedan ett byte till DirectX.
I Laboration 3 sitter du på båda sidor: du ska vara schysst mot den som redan använder din modul från Laboration 2, och samtidigt använder du någon annans modul, som kan få nya versioner. Isolera din kod:
- Adapter – definiera ett eget gränssnitt som pratar ditt domänspråk och döljer vad som finns
bakom. Exemplet
CityDALharsave,getAllCitiesochgetCitiesFromDistrict, men ingenting avslöjar att det ligger en databas bakom. - Wrapper – behåller det som finns bakom synligt men anpassar gränssnittet. Daniels gamla
MySQLiWrapperöversatte bibliotekets felkoder till undantag, men talade fortfarande SQL.
En adapter är en starkare abstraktion och kan mycket väl använda en wrapper på insidan.
Sammanfattning
Section titled “Sammanfattning”Isolera dig från hur andra klasser fungerar på insidan, och från så många andra klasser som möjligt. Den som läser koden ska behöva en så liten mental modell som möjligt.
Kodgranskning: Ripe Tomato i en alternativ MVC-design
Section titled “Kodgranskning: Ripe Tomato i en alternativ MVC-design”Inför passet hade Mats bett sin AI-assistent att glömma allt från de tidigare passen och skriva en
idiomatisk lösning för p5.js som följer MVC. Resultatet finns på grenen
spike/alt-mvc, tillsammans med
designdokument som MVC_DESIGN.md. Mats betonade att koden ändå är hans: han har styrt arbetet, och
ansvaret för eventuella misstag är hans – inte assistentens. Det är därför koden kan granskas som
vilken kod som helst.
Domänmodellen som karta
Section titled “Domänmodellen som karta”Daniel började utan att öppna en enda fil. Klassnamnen i models/ ska räcka för att förstå vilken
domän det handlar om: Game, GameStatus, levels (en samling banor), Tomato och hindren
Bird (något som flyger), Crate, Pit (något man inte vill ramla ner i) och basklassen
Obstacle. I en webbutik hade det varit ordrar, produkter och kategorier.
Mats förklarade varför vissa filnamn har stor och andra liten begynnelsebokstav: filer med stor bokstav innehåller en klass, filer med liten exporterar funktioner. Ur ett laborationsperspektiv tyckte Daniel att det är kod som inte är objektorienterad. Hjälpfunktioner är okej, men inte för att slippa göra objektorienterade konstruktioner. Märker du att du hela tiden importerar funktioner borde de kopplas ihop med sina data och bli klasser. Formatering av generiska datatyper, som datum, är ett typiskt fall där en funktion är rimlig.
geometry.js innehåller typdefinitionerna Circle och Rect (JSDoc-@typedef, inga klasser) och
funktionerna clamp och circleIntersectsRect. Det är datastrukturer, precis som i föreläsningen.
Med objekt hade Daniel haft en Circle- och en Rectangle-klass som döljer sina mått och själva
kan avgöra kollisioner.
- Namn som behöver förklaras. Fältet
#createLevelmed typen() => Levelvar svårt att förstå för den som inte skriver JavaScript dagligen. Det är en funktion som skapar banan – en sorts factory. Hade det varit ett objekt hade Daniel kallat detlevelFactory.#finishXbehövde en förklaring (en student föresloglevelEnd). Samtidigt gillade Daniel att namnet beskriver regeln och inte vyns flagga. - Kommentarer som säger vem som använder något. “The view reads it; only the game changes it”
på gettern
tomatosvarar på frågan om gettern kan tas bort. statusochisOveröverlappar, men det är logiskt: spelet pågår, eller är över för att det är vunnet eller förlorat.restartanropas också från konstruktorn, alltså även första gången. Daniel hade hellre kallat denstartLevel– en riktig omstart kanske behåller saker från förra försöket, som i ett spel med flera liv.- En
Level-klass saknas.createLevelreturnerar två saker samtidigt, hindren ochfinishX. Det finns enLevel-typ, men bara som typedef – alltså en datastruktur. - Data i kod. Banan är hårdkodad i
createFirstLevel. Det är okej tillfälligt, och ett bra sätt att arbeta top-down, men när det finns en baneditor eller hundra banor ska de läsas in från till exempel JSON. Gamesom fasad.jumpochsteervidarebefordrar till tomaten, så att kontrollern inte behöver känna till den. Det gillade Daniel, och han hade gärna sett ordet “fasad” i kommentaren.-1 | 0 | 1för riktning förekommer iGame,Tomato,KeyboardInputochGameController. Samma begrepp är kodat och förklarat på flera ställen. En uppräkningstyp (enum) för riktningen samlar det på ett ställe och hör hemma i modellen.updatebegränsar tidssteget med#MAX_TIME_STEP(1/30 sekund), så att ett långt uppehåll inte låter tomaten passera rakt igenom ett hinder. Raderna som avgör kollision och vinst borde brytas ut till en privat metod med ett bra namn.dt, frågade en student, kommer från p5:sdeltaTime(millisekunder sedan förra bildrutan), omräknat till sekunder.
Tomato
Section titled “Tomato”Är Tomato Mats eller assistentens beslut? Assistentens – Mats skulle ha valt det också. I pass 3
döptes karaktären om till GameCharacter (don’t be cute), och Daniel medgav att han själv bryter
mot regeln med Mario i sina bilder. Mats tyckte att det är svårt att diskutera spelet utan att
använda namnet på det som ritas ut. En student föreslog en GameCharacter där tomaten är en variant.
Det går, men då har spelets regler utökats med ett val av karaktär.
- Konstanter och fält. De statiska konstanterna beskriver tomatens fysik. Att radien inte är statisk antyder att tomaten kan ändra storlek.
- Undantag eller assert? Konstruktorn kastar
TypeErrorochRangeError. Kommer de meddelandena någonsin att visas för en användare? Nej. En negativ radie är ett programmeringsfel, och då borde spelet inte ens starta – ett fall för en assert. Skulle användaren själv mata in storleken ska ett undantag kastas och hanteras. JavaScript saknar assert, så det blir ettError. isOnGroundhandlar om samspelet mellan tomaten och marken. Den som äger båda –Game, eller en saknadLevel-klass som kan svara på om en position är mark eller grop – borde ansvara för det. BlirGameför stor kan den delas, till exempel med en klass för spelets fysik.steerkontrollerar att riktningen är -1, 0 eller 1. Med en enum hade det räckt att kontrollera typen – i JavaScript kan vad som helst skickas in, påpekade Mats.- En dubblerad regel.
xbegränsas till minst radien både i konstruktorn och iupdate, och även markkontrollen iupdateär egentligen ett samspel med banan, som kanske borde flyttas ut.
Är det overengineering?
Section titled “Är det overengineering?”Mats frågade när man ska göra avkall på principerna: spelet är litet, och med alla förslag kryllar det av klasser. Daniel skilde på två saker. Brott mot principer stör läsaren, och varje fix lugnar lite. Teknisk skuld gör däremot framtida ändringar dyrare och kan till slut få folk att undvika kodbasen helt. Förbättringar av kvaliteten bör du göra hela tiden. När två personer har olika uppfattning handlar det mer om att jämka än om att den ena ska vinna.
Hindren
Section titled “Hindren”Obstacleär en abstrakt basklass. Mats visade hurnew.targethindrar att den skapas direkt. Attxär vänsterkanten framgår av dokumentationen, men ett namn somleftEdgeXhade burit det själv.Pit.collidesWithjämför medthis.x + this.width, som Daniel läste upp som “högra kanten”. En getterrightEdgeiObstaclehade gjort koden lättare att läsa.Crateanroparsuper()– i JavaScript måste basklassens konstruktor ha körts innan du kan användathis. Validering i konstruktorerna gillade Daniel.Crate.collidesWithskickar tomaten och ett namnlöst objekt tillcircleIntersectsRect. För att skriva den koden måste du veta vilken form funktionen vill ha. Tomaten duger somCirclegenom duck typing, eftersom den harx,yochradius. Daniel tyckte inte att det var tydligt: tomaten säger inte själv att den är en cirkel. HadeCratehaft samma egenskaper som enRect(det saknasbottom, och vänsterkanten heterxi stället förleft) hade den kunnat skicka sig själv och det namnlösa objektet hade försvunnit.
Vyerna
Section titled “Vyerna”GameViewäger de andra vyerna och skickar in p5 och kameran till dem (dependency injection). Dessdrawläses som en tidningsingress på hög abstraktionsnivå, med detaljerna i privata metoder.this.#game.tomato.xiGameView.drawär ett brott mot Law of Demeter: vyn känner tillGame, men också tomaten och dess x-koordinat. Hade tomaten varit en datastruktur hade det varit okej.HudView.drawär inte uppdelad på samma sätt, påpekade Mats. Blocken åtskilda av tomrader borde vara privata metoder, som iGameView.- Strängar som visas för användaren ligger som konstanter överst i
HudView, och det är rätt: sådana strängar, och översättningen av dem, är vyns ansvar. Mats jämförde med ett datum som formas om innan det visas på en webbsida. Daniel samlar alla strängar i sitt eget spel på ett ställe, både för att kunna redigera dem och för översättning. - Omräkningen mellan världskoordinater och pixlar ligger i
Camera, och det är också vyns ansvar.
Kontrollern och input
Section titled “Kontrollern och input”Var KeyboardInput och PointerInput hör hemma är en pågående diskussion. Daniel ser dem som
vyklasser, och i grenen ligger de bland kontrollerna. GameController behöver därför känna till
kameran för att räkna om pekarens position till världskoordinater. I Daniels variant hade
kontrollern frågat vyn vad användaren vill göra, och inte behövt känna till kameran – men han kallade
skillnaden navelskåderi. Mats noterade att två AI-assistenter hade landat på olika sidor i frågan:
svaret beror också på hur du frågar.
Kodmetriker och AI-granskningen
Section titled “Kodmetriker och AI-granskningen”Mats visade två dokument i grenen. CODE_METRICS.md är en automatisk mätning av koden där varje
avvikande värde är kontrollerat mot koden. CLEAN_CODE_REVIEW.md är en granskning mot Clean Code.
Mätvärden som antal rader, antal funktioner och komplexitet säger inte allt. Clean Code handlar till
stor del om läsbarhet, och den går bara att bedöma genom att läsa.
AI-granskningens viktigaste åtgärder – sifferargument i nivåbeskrivningen och magiska tal i vyerna –
var inget Daniel hade tagit upp. Det enda större överlappet var begränsningen av x i Tomato, som
Daniel såg som en dubblerad regel och granskningen som ett överraskande beteende. Mats slutsats:
människan behövs också. Vad du reagerar på beror på din kunskap och erfarenhet, och mycket av Daniels
återkoppling handlade om arkitektur snarare än om hur enskilda rader är skrivna.
Daniel gick igenom några metriker. Antal kodrader är ett storleksmått, och cyklomatisk komplexitet
ett mått på hur många vägar som går genom en funktion. Den längsta funktionen, HudView.draw, är 22
rader. Vykod mot ett API som p5 blir ofta lång, så vinsten med att bryta ut den är inte självklar,
men blocken med tomrader pekar ändå på att det går. Alla 31 kontrollstrukturer är if, och Mats
förklarade varför: arraymetoder som forEach räknas inte.
Namngivna argument i nivåbeskrivningen
Section titled “Namngivna argument i nivåbeskrivningen”Mats tog upp en av AI-granskningens punkter: i createFirstLevel står rader som
new Crate(6, 1, 1) och new Bird(21, 1.6, 0.9, 2). Vill du skapa en ny bana måste du läsa tre
konstruktorer för att förstå vad talen betyder. JavaScript saknar namngivna parametrar (C# har
dem), men ett objekt som argument, destrukturerat i konstruktorn, ger nästan samma sak:
new Bird({ x: 21, baseY: 1.6, amplitude: 0.9, period: 2 })Det löser det lilla problemet, men är ett tecken på ett större. Daniel gillade lösningen, även om det är synd att den kräver ett objekt. Han knöt också an till föreläsningens början: får alla hinder ligga i samma array? Ja – de har samma basklass, behandlas likadant, och polymorfismen ger dem olika beteende.
Passet avslutades med att Mats spelade spelet. Versionen som granskades finns kvar på grenen
spike/alt-mvc i det öppna
GitHub-repot.