Översikt
DEnna vecka börjar du titta på objektorienterade konstruktioner i form av klasser. Information om veckans upplägg annonseras löpande i kursens Zulip-kanal.
Veckostart med klasser
Section titled “Veckostart med klasser”Tisdagen i kursvecka 6 inleddes med veckans översikt och frågor om A2 inför fredagens deadline: var du hittar uppgiftens issues, hur inlämningen via merge request fungerar och vad som gäller om du missar deadline. Därefter tog Mikael de första stegen med klasser och livekodade en tärning med konstruktor, metoder och privata fält, och förklarade vad inkapsling innebär.
Om passet
Section titled “Om passet”Tisdagens pass i kursvecka 6 hölls av Mats och Mikael tillsammans. Mats började med att gå igenom veckans översikt och besvara frågor om examinationsuppgift A2, som har deadline denna vecka: issues, inlämning via merge request och vad som händer om du missar deadline. Därefter tog Mikael sina första steg med klasser genom att livekoda en tärning i sitt playground-projekt.
Exemplet Ordräknaren och A2:s mode
Section titled “Exemplet Ordräknaren och A2:s mode”Bland veckans stödjande inspelningar finns ett nytt tillskott, exemplet Ordräknaren. Det finns
där för att mode (typvärde) historiskt har varit den lurigaste funktionen i A2. Exemplet delar upp
en text i ord, bygger en frekvenstabell och hittar det vanligaste ordet. I A2 handlar det om tal i
stället för ord, men principerna är desamma. Kör du fast på mode är exemplet, med både exempelkod
på GitLab och en inspelning, rätt ställe att hämta stöd.
Mikaels råd är att göra ditt bästa i A2 med så lite hjälp som möjligt. Då får du en ärlig bild av var
du står och vad du behöver träna på. Ett annat tips är att inte börja med mode utan snarare ta den
sist, när de andra funktionerna är på plats.
Mikael berättar att han själv behövde fräscha upp gymnasiematematiken första gången han löste uppgiften. Mats jämför med ett sommarjobb som 17-åring, då han portade ett C-program för signalanalys utan att förstå all bakomliggande teori. Det gick ändå, eftersom han förstod koden och kunde översätta beskrivningen till programkod. Mikael håller med: det är ett rimligt sätt att ta sig igenom A2. Du behöver förstå koden, inte nödvändigtvis bemästra all statistik bakom den.
Varför klasser?
Section titled “Varför klasser?”Klasser kom till JavaScript med ES6 (ECMAScript 2015), som Mikael ser som en rejäl nystart för språket. En klass är ett annat sätt att skriva den kod som objekt skapas utifrån. Förra veckan var det fabriksfunktioner, nu är det klass-syntaxen. Under ytan är det fortfarande prototyper, med samma pilar som i förra veckans prototypkedja. Därför kallades klasser länge för syntaktiskt socker, även i dokumentationen på MDN. Den beskrivningen används inte längre, eftersom klasser sedan dess har fått funktioner som tidigare var svåra att uppnå i JavaScript, t.ex. privata fält. Arv och inkapsling med privata fält återkommer de närmaste veckorna.
Är klasser något man faktiskt använder? Ja. Mikael ser i dag JavaScript som ett klassiskt objektorienterat språk, jämförbart med andra sådana. Om några veckor kommer webbkomponenter, som skrivs som klasser, och klasser fortsätter i parallellkursen i serverprogrammering och senare i Java-kursen, där all kod skrivs i klasser. Det är ett grundbegrepp som följer med under hela utbildningen.
Veckans material
Section titled “Veckans material”Det som brukar vara lurigt i början är att förstå vad en klass är och vilken roll den har i förhållande till objekten som skapas av den. Den stödjande inspelningen och presentationen om klasser är tänkta att hjälpa till med det. Läsanvisningarna omfattar ungefär 17 sidor i kursboken, med alternativ på MDN och i Eloquent JavaScript om du inte har tillgång till den.
Övningsuppgifterna är få den här veckan, eftersom fokus ligger på att bli klar med A2: Ellips med klass, som har både lösningsförslag och en inspelning, och den fördjupande Legobitar. Utmaningarna sparas till torsdagens pass.
A2: inlämning via merge request
Section titled “A2: inlämning via merge request”A2 har deadline fredag 9 oktober kl. 17.00. Det finns ingen inlämning i Moodle. Du lämnar in genom att skapa en merge request (MR) och följa guiden Inlämning via MR.
Skapa MR:en när du är färdig, men gärna i god tid. Är det första gången du gör en är det bra att ha några dagar på dig. Mats rekommendation är att vara klar med hela uppgiften senast onsdag eller torsdag. Då hinner du se att pipelinen går igenom och felsöka om den inte gör det. Klockan 17.00 på fredagen tittar Mats på vilka MR:er som är öppna. Skulle du skapa en MR för tidigt och inte hinna klart kan du stänga den. En stängd MR bryr sig ingen om.
Några saker att känna till om pipelinen:
- Det är exakt samma tester som körs lokalt, vid varje push till GitLab och i MR:ens pipeline. Det blir alltså inga överraskningar. (I vissa andra kurser körs andra tester vid inlämning, men inte här.)
- Det enda MR-pipelinen lägger till är en kontroll av att du lämnat in i tid.
- När MR:en väl är öppen påverkar varje ny push till
mainMR:en och startar pipelinen på nytt. - Hittar du en bugg några timmar efter deadline är det okej att rätta småsaker. Men du byter inte ut en stor del av koden efter deadline, och ändringar efter deadline syns tydligt som en röd markering i pipelinen för den som bedömer.
Hitta A2:s issues
Section titled “Hitta A2:s issues”En fråga gällde hur man hittar uppgiftens issues. README-filen hänvisar till dem med länkar som
#2, #3 och så vidare, t.ex. efter funktionsnamnet maximum. Klicka på länken så kommer du till
respektive issue med mer information. Vill du se alla samlade finns de i projektets vänstermeny
under Plan, antingen som Work items (GitLabs nya samlingsnamn för bland annat issues) eller som
Issue boards. Det är samma issues, bara presenterade på olika sätt. Mallen har 17 stycken, från
varje funktion till reflektionsdokumentet. Mer än så finns det inte, det är de som README-filen
beskriver.
Läs igenom varje issue ordentligt, de innehåller detaljer som behövs för att lösa uppgiften som den är tänkt. Checkboxarna kan du kryssa i själv, du kan skriva kommentarer och du kan stänga en issue när du är klar med den. Det blir ett arbetsflöde som hjälper dig att hålla ordning på vad som återstår.
Missad deadline, omexamination och komplettering
Section titled “Missad deadline, omexamination och komplettering”Missar du deadline är nästa chans inte om två veckor, som Mats sa förra veckan, utan om fem: fredag 13 november kl. 17.00. Förklaringen är att kursledningen har tre veckor på sig att bedöma och meddela resultat, och att den som blir underkänd sedan ska ha ytterligare 14 dagar på sig inför omexaminationen. Alla tillfällen finns på sidan för A2.
Utöver godkänt (G) och underkänt (U) finns ett mellanläge, komplettering: något behöver förbättras för att nå godkänt. Du får då ett datum då kompletteringen senast ska vara gjord, och instruktioner i samband med återkopplingen. Är den inte gjord i tid blir det automatiskt U, men det händer sällan. Är det för mycket som behöver förbättras blir det U direkt, och då lämnar du in vid något av omexaminationstillfällena.
Huvudspåret är förstås att lämna in en färdig uppgift: alla tester gröna och god koll på din kod. Det finns ingen anledning att lämna in något halvfärdigt.
Livekodning: en första klass
Section titled “Livekodning: en första klass”Mikael öppnar en ny fil, class.js, i sitt playground-projekt. Förra veckans objektlitteraler och
prototypkedja är fortfarande relevanta, men nu läggs de lite längre bak i huvudet. Han börjar med en
tom klass och skapar ett objekt av den:
class Dice {}
const dice = new Dice()console.log(dice) // Dice {}Redan här finns begreppen att hålla isär. Klassen är mallen som koden skrivs i, och med new
instansieras ett objekt av klassen. Variabeln dice är en vanlig variabel, men i stället för en
sträng eller ett tal innehåller den ett objekt, en instans av klassen Dice. Utskriften visar
klassens namn följt av måsvingar, objektets innehåll. Mats poängterar att rätt ord sparar många ord:
den som känner till begrepp som “instansiera ett objekt” förstår direkt vad som menas, oavsett språk.
Konstruktor, egenskaper och this
Section titled “Konstruktor, egenskaper och this”En tärning har ett antal sidor. Det blir en egenskap, och för att tärningen ska vara flexibel skickas antalet in när objektet skapas:
class Dice { sides
constructor(sides) { this.sides = sides }}
const dice = new Dice(6)console.log(dice) // Dice { sides: 6 }constructor är en metod som anropas när ett objekt skapas med new, och det är just new Dice(6)
som är anropet till konstruktorn. Klassen har alltså egenskaper (data) och metoder (det man kan
göra). Parametern sides sparas undan i objektet med this.sides = sides. this pekar på det
objekt som metoden körs i.
En metod för att kasta tärningen
Section titled “En metod för att kasta tärningen”En tärning behöver kunna kastas och ge ett värde mellan 1 och 6:
roll() { const value = Math.floor(Math.random() * this.sides) + 1 return value}console.log(dice.roll()) // e.g. 5Mikael tycker att konstruktionen är krånglig jämfört med andra språk. Mats tror att det delvis
beror på att JavaScript bara har en taltyp, number, för både heltal och decimaltal. Språk med
separata heltalstyper som int slipper ofta konstruktioner som Math.floor().
En fråga i chatten gällde varför man använder Math.floor() och inte Math.ceil(), som skulle göra
+ 1 onödigt. Svaret ligger i vad Math.random() returnerar: ett tal i intervallet 0 till 1, där 0
kan förekomma men inte 1.
Math.floor(Math.random() * 6)ger 0–5, och+ 1flyttar det till 1–6.Math.ceil(Math.random() * 6)ger nästan alltid 1–6, men omMath.random()returnerar exakt 0 blir resultatet 0. Det är ovanligt, men en tärning som någon gång visar 0 är fel.
Math.floor(Math.random() * n) + 1 är därför det vanliga sättet att slumpa ett heltal mellan 1 och
n i JavaScript.
Privata fält och inkapsling
Section titled “Privata fält och inkapsling”Mikael städar upp koden för att göra den “Mats-säker” och börjar med att sätta en brädgård (#)
framför egenskapens namn, överallt där den används:
class Dice { #sides
constructor(sides) { this.#sides = sides }
roll() { const value = Math.floor(Math.random() * this.#sides) + 1 return value }}Utan # var egenskapen publik och kunde läsas och ändras utifrån. Med # blir den ett privat
fält som bara syns inuti klassen. Har du erfarenhet av andra språk är du kanske van vid nyckelordet
private, men i JavaScript skrivs en brädgård i stället. Till skillnad från publika egenskaper
måste privata fält deklareras i klasskroppen innan de kan användas.
Skillnaden syns direkt i utskriften. Med det privata fältet skriver console.log(dice) inte längre
ut antalet sidor, privata delar syns inte utanför klassen. Mikael tar bort # en gång för att visa
skillnaden och konstaterar att han då måste ändra på alla ställen där fältet används.
Det här är inkapsling, ett grundbegrepp i objektorientering: en klass kapslar in data (egenskaperna) tillsammans med det man kan göra med datan (metoderna), paketerat på ett ställe. Allt som ingen utanför klassen behöver känna till görs privat. Det, säger Mikael, ska sitta i ryggraden.
Varje objekt som skapas av klassen får sina egna värden på de privata fälten. Skapar du flera tärningar har var och en sitt eget antal sidor.
Tärningens tillstånd
Section titled “Tärningens tillstånd”Mikael är inte nöjd med roll(). Värdet försvinner så fort det returnerats. En riktig tärning visar
fortfarande en sexa nästa gång du tittar på den, och objektet bör modellera det. Han sparar därför
värdet som en del av objektets tillstånd (state) och lägger till en metod för att läsa av det:
class Dice { #sides #value
constructor(sides) { this.#sides = sides }
roll() { this.#value = Math.floor(Math.random() * this.#sides) + 1 return this.#value }
value() { return this.#value }}
const dice = new Dice(6)const lastRoll = dice.roll()console.log(lastRoll) // e.g. 5console.log(dice.value()) // 5Den lokala variabeln i roll() ersätts av fältet this.#value. Det privata fältet #value och den
publika metoden value() krockar inte, eftersom # är en del av fältets namn.
Att läsa en klass
Section titled “Att läsa en klass”Mats sammanfattar vad som finns i klassen och i vilken ordning det brukar stå:
- Nyckelordet
classoch ett namn i PascalCase, där varje ord inleds med stor bokstav. - Överst egenskaperna, här de privata fälten.
- Därefter konstruktorn. När du läser en klass vill du tidigt veta hur du instansierar objekt av den, här genom att skicka med antalet sidor.
- Sist metoderna, här
roll(), som slumpar ett värde i det slutna intervallet 1 till 6, ochvalue(), som ger det senaste kastet för just det här objektet.
Redan klassens namn berättar att det handlar om en tärning, och metodernas namn att den kan kastas och att resultatet kan läsas av. Mycket mer går att bygga vidare på, men det här är ett bra första steg att sova på. Torsdagens pass fortsätter med klasser, kanske med tärningen eller ett annat exempel, beroende på vilka frågor som kommer in.
Torsdag med tärningsspel
Section titled “Torsdag med tärningsspel”Torsdagen i kursvecka 6 inleddes med en genomgång av inlämningen av A2 via merge request: vad inlämningsrapportens gröna bockar betyder, vad pipelinen kontrollerar och vad som gäller för antalet commits. Därefter tog Mikael tisdagens tärning vidare till ett litet tärningsspel och visade vägen från spelidé via krav, flödesdiagram och pseudokod till en fungerande klass.
Om passet
Section titled “Om passet”Torsdagens pass i kursvecka 6 hölls av Mats och Mikael tillsammans, dagen före A2:s deadline. Mats började med att skapa en merge request för A2 och reda ut några frågor om inlämningsrapporten och pipelinen. Därefter tog Mikael tisdagens tärning vidare till ett litet tärningsspel, Bäst av fem, och visade vägen från spelidé via krav, flödesdiagram och pseudokod till fungerande kod.
Inlämning via merge request
Section titled “Inlämning via merge request”Mats visar inlämningen steg för steg i sitt eget A2-projekt. Pipelinen är redan grön efter senaste push, och de jobb som körs där (förberedelse, kodkvalitet och tester) körs igen när merge requesten skapas, tillsammans med några kontroller till. Är de gröna efter din push bör de alltså vara gröna även i merge requesten.
Merge requesten går från main till lnu/submit. När du väljer main som källa fylls målgrenen i
automatiskt, något som är nytt för i år. Därefter fyller du i inlämningsrapporten (Assignment
report), som är på engelska eftersom den hör till koden. Kryssa i rutorna: att du installerat
beroenden, kört npm start, uppfyllt alla obligatoriska funktionella och icke-funktionella krav,
skrivit REFLECTION.md, och att du på heder och samvete förstår koden du lämnar in. Rutorna
kontrolleras när merge requesten skapas. Har du missat någon går den inte igenom i pipelinen.
Välj även rätt milestone för A2. Assignees, reviewers och labels behöver du inte fylla i.
En fråga i chatten var om något mer behöver skrivas i rapporten. Svaret är nej, bara att kryssrutorna är markerade. Har du lagt till egna issues kan du nämna dem där om du vill.
De gröna bockarna i rapporten
Section titled “De gröna bockarna i rapporten”En fråga i Zulip gällde de gröna bockarna vid kraven i inlämningsrapporten. De fylls inte i automatiskt och säger ingenting om din lösning. De visar bara vilka krav som är obligatoriska. I A2 är alla krav obligatoriska utom #12, DRY (Don’t Repeat Yourself). Att #12 saknar grön bock betyder alltså inte att din kod bryter mot DRY, bara att kravet är frivilligt. Mats har inte fått frågan tidigare men ser att det går att missförstå, och funderar på hur det kan visas tydligare.
Pipelinen vid en merge request
Section titled “Pipelinen vid en merge request”Direkt efter att merge requesten skapats visar Mats en pipeline med ett misslyckat jobb. Det visar sig bara vara ofärdigt: jobben kör i tur och ordning och grå bockar betyder att de inte är klara än. Efter en stund blir allt grönt. Mats går igenom vad som kontrolleras:
- Att en milestone är satt.
- Att inlämningsrapporten är ifylld.
- Att reflektionsdokumentet faktiskt har ändrats.
- En förberedelse som kopierar testerna och projektkonfigurationen från mallen, så att ingen kan manipulera testerna.
- Kodkvalitet och tester, samma som körs lokalt.
- Att inlämningen skett före deadline enligt milestonen.
Det är ditt ansvar att tester och kodkvalitet är gröna. Blir något av de andra jobben rött trots att du lämnat in i tid är det inte ditt fel, utan systemets. Och missar du deadline är det inte hela världen: omexaminationen är om fem veckor, det är samma uppgift och det påverkar inte bedömningen.
Mikael påpekar att pipelines är något ni kommer att skriva själva längre fram, när kod ska testas och driftsättas automatiskt. Det brukar kallas CI/CD, Continuous Integration och Continuous Delivery eller Deployment.
Antalet commits
Section titled “Antalet commits”En student hade sett att merge requesten visade 14 commits och undrade om det var därför något misslyckades. Mats tror inte att pipelinen kontrollerar antalet commits, och hans egen merge request gick igenom med ungefär lika många.
Mats undrade varför hans merge request visade 14 commits när historiken innehöll 15. Förklaringen
är troligen att en merge request bara räknar de commits som inte redan finns i målgrenen, och att
lnu/submit innehåller mallens startcommit. Hur många startcommits mallen har beror på när du
skapade din kopia. I tidiga kopior finns 3–5 commits från kursledningen, i senare bara en. Det gör
det enklare vid bedömningen: under Changes syns bara det du själv har ändrat. Vill du räkna dina
commits fungerar git log --oneline eller git rev-list --count HEAD, som någon tipsade om i
chatten.
Merge request och återkoppling
Section titled “Merge request och återkoppling”Bedömningen och återkopplingen sker i merge requesten. Där kan Mats och Mikael kommentera direkt i koden. Mikael påpekar att arbetssättet är vanligt när man utvecklar i grupp: en ny funktion går inte rakt in i projektet utan via en merge request som kollegor granskar först. Därför används samma arbetssätt i kursen, så att du tränar på det redan från början.
Från spelidé till krav
Section titled “Från spelidé till krav”Mikael tar fram exempelprojektet
dice-best-of-five och dess
designdokument, DESIGN.md. Spelidén är enkel: spelaren möter datorn i fem rundor, båda kastar en
tärning per runda, den som slår högst vinner rundan och den som vunnit flest rundor vinner spelet.
Mats har själv skrivit nästan samma spel, med skillnaden att varje deltagare kastar två tärningar.
Hade spelet gått att skriva redan för några veckor sedan, utan objekt och klasser? Ja, säger båda. Men då blir det en samling lösryckta variabler och funktioner som på något sätt ska samverka. Mats liknar det vid att handla åtta varor och försöka bära hem dem i händerna i stället för i en kasse. Allt som hör till tärningen hör hemma på ett ställe, i en klass.
Ur spelidén skissar Mikael en kravlista:
- Funktionella krav beskriver vad spelet ska göra: två deltagare, fem rundor, högst kast vinner, lika ger oavgjort och så vidare.
- Icke-funktionella krav påverkar inte vad spelet gör, utan hur det byggs: att tärningsklassen
från tisdagen används oförändrad, att programmet körs med
node main.js, och att spellogiken inte skriver ut något. Ofta handlar sådana krav om kodkvalitet eller den miljö programmet ska köras i, som i A2. - Avgränsningar säger vad som inte ska byggas: inga insatser, ingen inmatning och ingen sparad statistik.
Mikael tycker särskilt om avgränsningarna. När koden börjar växa åt olika håll hjälper de honom att backa och fråga sig vad det minsta är som behöver göras för att lösa uppgiften. Mats håller med: att veta var man ska börja är det svåra, och ju mer man kan ta bort, desto enklare blir det.
Flödesdiagram och pseudokod
Section titled “Flödesdiagram och pseudokod”Nästa steg är att rita. Mats tänker i bilder och ritar ofta upp en array med pilar fram och tillbaka, även efter 30 år som programmerare. Mikael tänker hellre i lådor och legobitar. Båda ritningarna hjälper till att strukturera tankarna.
Designdokumentet innehåller ett flödesdiagram: skapa spelet, spela rundor tills alla fem är spelade (kasta, jämför, dela ut poäng, skriv ut, räkna upp), jämför sedan poängen och utse en vinnare. När Mikael går igenom det ser han redan if-satser och loopar framför sig. Diagrammet är skrivet i Mermaid, som är text i markdown-filen. Det versionshanteras alltså tillsammans med koden.
Efter diagrammet kommer pseudokoden, som tar ett steg närmare koden men ännu inte är JavaScript:
SÅ LÄNGE rundor spelade < 5, OM spelarens kast > datorns kast och så vidare. Den ska fungera för
vilket programmeringsspråk som helst. Mats blandar ofta de två teknikerna, en bit pseudokod och en
figur, och översätter sedan till det språk han använder för stunden.
Mikael sammanfattar kedjan så långt: problemformulering, krav och avgränsningar, diagram och skisser, pseudokod. Alla är bara verktyg. Hur du kombinerar dem är personligt, och Mats och Mikael gör olika.
Bäst av fem, på riktigt?
Section titled “Bäst av fem, på riktigt?”En fråga i chatten var hur koden ändras om spelet verkligen ska vara “bäst av fem”, alltså att spelet tar slut när någon har vunnit tre rundor. Mats föreslår en kontroll i slutet av loopen: om spelarens eller datorns poäng är större än två, bryt.
Mikael erkänner att hans eget huvud också spinner iväg mot nya regler, till exempel att ett par ettor ska slå 5 + 6 med två tärningar. Men han säger stopp till sig själv. Planen är bra, håll dig till den och gör den färdig först. Mats kallar det att tillåta sig själv att lyckas: lös så små delar som möjligt åt gången.
Kodstrukturen
Section titled “Kodstrukturen”Sista delen av designdokumentet delar upp koden i tre filer:
| Fil | Ansvar |
|---|---|
dice.js |
Slumpar ett tärningskast. Känner inte till spelet. |
game.js |
Spelreglerna: håller poängen, spelar en runda, avgör vinnaren. Skriver inget. |
main.js |
Startar spelet, loopar över rundorna och skriver ut allt. |
Mikael hade kunnat lägga all spelkod direkt i main.js, men samlar allt som hör till spelet i en
egen “kasse”, klassen Game. Han beskriver två sätt att angripa ett problem: top-down, där man
först skissar alla delar av helheten (mer Mats stil), och bottom-up, där man börjar med små
kodkonstruktioner och bygger upp klassen bit för bit (mer Mikaels). Båda medger att spelet går att
skriva enklare utan klasser. Klasserna finns här för att träna på dem.
Tärningen, ett steg längre
Section titled “Tärningen, ett steg längre”dice.js är tisdagens tärning, lite utbyggd. Mats pekar ut det nya:
- Konstruktorn validerar argumentet och kastar ett
RangeErrorom antalet sidor inte är ett heltal som är minst 2. - Metoden
value()har ersatts av en getter,get value(), och det finns ävenget sides(). Fälten är privata, men värdena går ändå att läsa utifrån. toString()ger en textbeskrivning av objektet (värdet, eller-om tärningen inte har kastats),valueOf()ger objektets numeriska värde ochtoJSON()styr hur objektet blir JSON. Alla tre är delar av JavaScripts objektmodell som klassen kan definiera själv.
En liten klass, men den visar många grundbegrepp: privata fält, inkapsling, getters och en textbeskrivning av objektet.
export default eller namngiven export?
Section titled “export default eller namngiven export?”game.js exporterar klassen med export default class Game, och main.js importerar den. Mats
gillar inte default. Med en standardexport kan den som importerar välja vilket namn som helst.
Mikael demonstrerar genom att importera klassen som Moped, och det fungerar. Mats använder därför
alltid namngivna exporter:
export class Game { // ...}
// main.jsimport { Game } from './game.js';Då måste den som importerar använda samma namn som exporten, inom klamrar.
Getter eller metod?
Section titled “Getter eller metod?”I main.js loopar Mikael så länge spelet inte är slut, while (!game.isOver). isOver är en getter
och läses som en egenskap, utan parenteser. Mats tumregel för att välja mellan getter och metod: behöver
du skicka med data, alltså argument? Då blir det en metod. Vill du bara fråga objektet om något i
dess inre tillstånd är det en kandidat för en getter. Mikael lägger till att båda varianterna fungerar
och att du inte behöver fastna i valet just nu.
Livekodning: en spelrunda
Section titled “Livekodning: en spelrunda”Pseudokoden från designdokumentet ligger redan som kommentarer i game.js och main.js. Mikael vill
kunna köra programmet hela tiden, särskilt när det saknas tester. Därför börjar han med att låta
playRound() returnera hårdkodade värden, så att utskriften i main.js kan skrivas klart först:
import Game from './game.js';
const game = new Game();let round = 1;
while (!game.isOver) { const result = game.playRound(); console.log(`Spelrunda ${round}: player ${result.player} vs computer ${result.computer}, winner is ${result.winner}`); round++;}new Game() räcker, eftersom konstruktorn har standardvärdet 5 rundor, precis som kraven säger.
All utskrift sker i main.js, aldrig i Game. Mats ger principen ett namn: separation of concerns.
playRound() ska bara avgöra vem som vinner rundan, inte presentera resultatet. Mikael lägger till
att en klass utan console.log dessutom är förberedd för enhetstester.
Rundräknaren för utskriften hamnade i main.js. Borde Game hålla reda på den i stället? Kanske,
säger Mats, men lösningen är tillräckligt bra. Vi vet för lite för att fatta det perfekta beslutet
nu, och att sitta och optimera tidigt är en risk. Det perfekta beslutet just nu är något som
fungerar. Det går alltid att återvända senare. Mikael tycker att rådet borde sitta på väggen.
Felsökning: från ingen loop till oändlig loop till fem rundor
Section titled “Felsökning: från ingen loop till oändlig loop till fem rundor”Programmet krånglar flera gånger på vägen, och varje fel är lärorikt:
- Ingenting händer. Villkoret var skrivet
while (game.isOver). Spelet är inte slut från början, så loopen körs aldrig. Det ska vara!game.isOver. TypeError: Assignment to constant variable. Rundräknaren var deklarerad medconstoch kunde inte räknas upp. Den ska deklareras medlet.- Oändlig loop.
isOvervar inte implementerad än, så spelet tog aldrig slut. Med Ctrl+C avbryts programmet. Ett tillfälligtreturn truegav motsatt problem: då körs ingen runda alls. - Fortfarande oändlig loop. Mikael inför två privata fält, ett för maxantalet rundor och ett
som räknar spelade rundor, och räknar upp det senare i
playRound(). Men räknaren saknade startvärde.undefined + 1blirNaN, ochNaN >= 5är alltidfalse. Med#rounds = 0fungerar det.
#maxRounds;#rounds = 0;
constructor(maxRounds = 5) { this.#maxRounds = maxRounds;}
get isOver() { return this.#rounds >= this.#maxRounds;}Konstruktorns parameter och fältet för maxantalet hänger ihop. Mats undrade om de två rounds var
samma sak, och det var just det som fick Mikael att ge fältet för maxantalet ett eget namn,
#maxRounds, skilt från räknaren. Mikael påpekar att just den här typen av fel i jämförelser är
anledningen till att skriva tester. Han har varit med i projekt där sådana fel har kostat mycket pengar.
Två tärningar och en vinnare
Section titled “Två tärningar och en vinnare”En student påpekade att varje runda slutade oavgjort, eftersom resultatet fortfarande var hårdkodat. Mikael ger spelet två riktiga tärningar, en för spelaren och en för datorn, som privata fält. Privata fält måste deklareras i klasskroppen. Glömmer du det får du ett syntaxfel innan programmet ens körs.
Mats påpekar styrkan i det objektorienterade synsättet här: Game bryr sig inte om hur ett kast
går till, den anropar bara roll() och får tillbaka ett värde.
För att avgöra vinnaren börjar Mikael med ett antagande: rundan är oavgjord. Sedan behöver bara två
fall kontrolleras. Hans första försök hade en if/else, och då kunde det aldrig bli oavgjort. Den
andra grenen behöver ett eget villkor:
playRound() { const player = this.#diePlayer.roll(); const computer = this.#dieComputer.roll(); let winner = 'draw';
if (player > computer) { winner = 'player'; } else if (computer > player) { winner = 'computer'; }
this.#rounds++; return { player, computer, winner };}Mats tycker att just det här, att göra ett lämpligt antagande, är bland det svåraste att lära sig.
Det är svårt att se när man vinner på det, men i A3 finns till exempel konstruktioner där det lönar
sig mycket. Den vanliga if/else if/else med tre grenar fungerar lika bra. Mikaels råd: använd
den konstruktion du kan, lös uppgiften och känn dig nöjd. Annars sitter du där i timmar och letar
efter den perfekta konstruktionen, och då blir det ingen lunch.
Poängräkningen och vinnaren av hela spelet hann inte kodas live. Den finns på exempelprojektets
solution-gren.
Vidare på egen hand
Section titled “Vidare på egen hand”Sist i DESIGN.md finns fem övningar att bygga vidare med, gärna med uppdaterat flödesdiagram och
pseudokod innan du kodar: två tärningar per deltagare (E1), ett valfritt antal tärningar (E2),
utskrift av varje enskilt kast (E3), tärningar som Unicodetecken (E4) och en särskild regel för par
(E5). Även “bäst av fem” på riktigt, från chattfrågan, är en bra övning.
Mikael sammanfattar syftet med passet: att bygga upp kedjan från problemförståelse och problemformulering, via olika tekniker för problemlösning, ända in i koden, och att bygga koden av små, små delar som fungerar hela vägen. Nästa vecka handlar om arv. Mats funderar på att låta tärningsspelet följa med, eftersom återanvändning av kod är precis vad arv handlar om.
Stödjande inspelningar
Section titled “Stödjande inspelningar”Exempel - Ordräknaren
Section titled “Exempel - Ordräknaren”Ett moment som många har svårt med i examinationsuppgift A2 är funktionen “mode”. För att underlätta löses här ett exempel som löser samma grundläggande problem.
Exempelkoden hittar du på GitLab.Uppgiften
Section titled “Uppgiften”Ett förberett dataunderlag – en array med ett antal korta citat, sammanfogade till en enda lång text med join('\n') – ska analyseras: hur många gånger förekommer varje ord, och vilket eller vilka ord är vanligast? Övningen är tänkt som en mindre variant av samma problem som löses i examinationsuppgift A2 (mode), utan att vara identisk med den.
Innan kodandet börjar är det värt att lösa problemet på papper: skriva upp varje ord man stöter på, kontrollera om det redan finns uppskrivet, och antingen lägga till det eller öka en räknare. Det tankesättet – ett ords namn kopplat till ett antal – är exakt vad ett objekt (nyckel-värde-par) är till för.
Städa texten
Section titled “Städa texten”Rå text innehåller skiljetecken och radbrytningar som stör en enkel uppdelning i ord. Ett första försök tar bort punkter och kommatecken med String#replace/replaceAll (replaceAll fungerar bara i tillräckligt nya Node-versioner, vilket upptäcktes live via ett felmeddelande – ett bra exempel på att kontrollera dokumentationen och sin egen miljö innan man antar att en metod finns tillgänglig). En sträng är immutable – replace returnerar alltid en ny sträng, den ursprungliga strängen kan aldrig ändras i sig.
Efter en första körning dyker fler problem upp i utskriften: kvarvarande radbrytningstecken (\n) mellan citaten. Att byta ut radbrytningarna mot mellanslag (inte mot en tom sträng) blir den sista pusselbiten – annars klistras ord från olika citat ihop till ett enda felaktigt ord.
Dela upp i ord
Section titled “Dela upp i ord”String#split(' ') delar upp den rensade, gemenifierade (toLowerCase()) textmassan till en array av ord. (En alternativ metod, slice, undersöktes också men visade sig inte passa – den extraherar en delsträng utifrån position, inte utifrån ett sökmönster.)
Bygg en frekvenstabell
Section titled “Bygg en frekvenstabell”Ett tomt objekt skapas utanför loopen (skapas det inuti loopen istället byggs ett nytt, tomt objekt upp på varje varv – ett vanligt nybörjarmisstag). Sedan itereras ordlistan igenom med en for...of-loop, som är att föredra framför en klassisk indexbaserad for-loop när själva indexet inte behövs:
const wordFrequency = {}
for (const word of words) { if (!wordFrequency[word]) { wordFrequency[word] = 1 } else { wordFrequency[word]++ }}Att fråga efter en egenskap som inte finns på ett objekt (wordFrequency[word]) ger undefined, vilket är falsy – precis det villkoret bygger på för att avgöra om ordet redan setts eller inte.
Hitta det mest förekommande ordet
Section titled “Hitta det mest förekommande ordet”Att bara plocka ut det numeriskt största värdet räcker inte – uppgiften efterfrågar vilket/vilka ord som är vanligast, inte hur många gånger. Object.values() ger alla frekvenser som en array (för att till exempel sortera fram det högsta värdet); Object.keys() ger motsvarande lista med alla unika ord. Med maxfrekvensen känd kan Object.keys(wordFrequency) itereras igenom en gång till, och varje ord vars frekvens matchar maxvärdet samlas i en resultatarray:
const result = []for (const key of Object.keys(wordFrequency)) { if (wordFrequency[key] === maxFrequency) { result.push(key) }}En detalj värd att lägga märke till: Object.keys(obj) inuti själva for...of-uttrycket utvärderas bara en enda gång totalt, inte en gång per iteration – att spara resultatet i en egen namngiven variabel är ändå ofta tydligare att läsa.
Från utforskande kod till färdig lösning
Section titled “Från utforskande kod till färdig lösning”Koden ovan byggdes utforskande, steg för steg, med tillfälliga variabelnamn och lite extra krångel på vägen – helt normalt när man löser ett nytt problem, och något att städa upp i efterhand snarare än att sträva efter en perfekt lösning direkt. Den färdiga referenslösningen bryter ut logiken till en separat modul (text-analyzer.js) med tre rena, exporterade funktioner – en för att dela upp text i ord, en för att räkna ordfrekvens, och en för att hitta de mest förekommande orden – och finns i två varianter: en med samma traditionella teknik som visas här, och en som använder nyare, mer kompakta ES6-konstruktioner (itererande array-metoder) som kursen återkommer till senare.
Klasser
Section titled “Klasser”I JavaScript finns inte bara ett sätt att skapa ett objekt. Från enkel objektliteral till klass – flera vägar leder fram.
PresentationAllt handlar om prototypkedjor
Section titled “Allt handlar om prototypkedjor”Innan klass-syntax fanns i JavaScript byggdes prototypkedjor upp för hand, ofta med hjälp av det som kallas “factory”-designmönstret – en funktion som skapar och returnerar ett objekt:
const pioneerPrototype = { says (text) { return `${this.firstName} says "${text}"` }}
const createPioneer = function (firstName, lastName) { const pioneer = Object.create(pioneerPrototype) pioneer.firstName = firstName pioneer.lastName = lastName return pioneer}
const lovelace = createPioneer('Ada', 'Lovelace')Object.create(pioneerPrototype) skapar ett nytt objekt vars interna __proto__ pekar på pioneerPrototype. Nästan alla objekt i JavaScript har en sådan kedja av ett eller flera prototypobjekt – det är språkets viktigaste sätt att återanvända funktionalitet mellan objekt.
Klassdeklaration
Section titled “Klassdeklaration”Klass-syntax är ett kompakt sätt att sätta upp exakt samma sorts prototypkedja:
class Pioneer { constructor (firstName, lastName) { this.firstName = firstName this.lastName = lastName }
says (text) { return `${this.firstName} says "${text}"` }}
const lovelace = new Pioneer('Ada', 'Lovelace')En klass är i grunden bara en funktion – typeof Pioneer ger 'function'. Precis som för alla funktioner skapas automatiskt egenskapen prototype, vars värde är ett objekt som i sin tur innehåller egenskapen constructor. says-metoden hamnar på Pioneer.prototype, inte direkt på varje instans – alla instanser delar alltså samma metodobjekt.
Varför klasser och inte “constructor/prototype”?
Section titled “Varför klasser och inte “constructor/prototype”?”Innan klass-syntax fanns användes vanliga konstruktorfunktioner tillsammans med manuell .prototype-tilldelning för att uppnå samma sak. Klasser föredras idag av flera skäl: de används genomgående när man skriver webbkomponenter, ramverk som React och Angular bygger på dem, och JavaScript-motorer är bra på att optimera kod skriven med klass-syntax. Arv är också betydligt enklare att implementera med extends, även från inbyggda konstruktorfunktioner.
Det finns baksidor att vara medveten om: skillnaden mellan hur klasskod ser ut och vad som faktiskt sker under huven är stor, det är lockande att stoppa för mycket funktionalitet i en och samma klass, och arv är så enkelt att tillämpa att det ibland görs i för stor omfattning.
Flera sätt att definiera en klass
Section titled “Flera sätt att definiera en klass”Utöver klassdeklaration (class Pioneer { ... }) finns klassuttryck, både anonyma och namngivna:
// Anonymous class expressionconst Pioneer = class { ... }
// Named class expressionconst Pioneer = class MyClass { ... }Oavsett hur klassen definierats skapas en instans med new följt av klassnamnet och eventuella argument.
Klassdefinitionens delar
Section titled “Klassdefinitionens delar”Alla delar av en klassdeklaration ger motsvarande egenskap på Pioneer.prototype – förutom de som föregås av static, som istället hamnar direkt på klassen (funktionen) själv:
class Pioneer { constructor (firstName, lastName) { this.firstName = firstName this.lastName = lastName }
static from (other) { return new Pioneer(other.firstName, other.lastName) }
get fullName () { return `${this.firstName} ${this.lastName}` }
says (text) { return `${this.firstName} says "${text}"` }}
const lovelace = new Pioneer('Ada', 'Lovelace')const copy = Pioneer.from(lovelace)constructor anropas automatiskt när en ny instans skapas och kan uteslutas helt om ingen initiering behövs. fullName är en getter – en metod som beter sig som en vanlig (skrivskyddad) egenskap när den läses, utan parenteser vid anropet: lovelace.fullName, inte lovelace.fullName(). En static-metod som from anropas direkt på klassen (Pioneer.from(...)), inte på en instans, och används ofta som en alternativ “fabrik” för att skapa objekt – här en kopia baserad på ett annat objekts egenskaper.
Kapsla in kod med klasser
Section titled “Kapsla in kod med klasser”Ett enkelt exempel: en tärning. Ett första försök håller ett värde och en metod som slumpar fram ett nytt:
class Die { constructor (faceValue) { this.faceValue = faceValue }
roll () { this.faceValue = Math.floor(Math.random() * 6) + 1 }}
const die = new Die()Problemet: konstruktorn initierar inte objektet på ett bra sätt – faceValue sätts inte till ett giltigt slumpat värde direkt, och antalet sidor är hårdkodat till sex. Ett andra försök löser båda: konstruktorn anropar roll direkt, och antalet sidor blir en parameter med ett standardvärde:
class Die { constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }
roll () { this.faceValue = Math.floor(Math.random() * this.maxFaceValue) + 1 }}
const die = new Die()console.log(die.faceValue) // a number between 1 and 6Skickas inget argument med skapas en sexsidig tärning; anges ett annat värde (t.ex. new Die(20)) skapas en tärning med det antalet sidor istället. Ett kvarstående problem: faceValue och maxFaceValue går fortfarande att sätta till vilket värde som helst utifrån, eftersom inget skyddar de “riktiga” egenskaperna – någonting kommande presentationer tar upp.
Läsanvisningar
Section titled “Läsanvisningar”Under läsanvisningar hittar du veckans kapitel som ska läsas i kurslitteraturen. Planera upp och dela upp läsningen under veckan.
Övningar
Section titled “Övningar”Under Övningsuppgifter hittar du övningsuppgifter som det är lämpligt att du arbetar med under veckan.
Seminarier + individuell handledning
Section titled “Seminarier + individuell handledning”Examinationsuppgift A2
Section titled “Examinationsuppgift A2”Nu bör du vara i full färd med att färdigställa examinationsuppgift A2. (För att komma igång se Examinationsuppgifter->Om->Kom Igång).
Här kan du läsa om hur du lämnar in din uppgift via en MR.
Utmaningar
Section titled “Utmaningar”Det finns en del utmaningar att titta på även denna veckan. Tänk att utmaningar kan vara bra för att komma igång med egna kodexempel kring veckans fokus.
Studiefrågor
Section titled “Studiefrågor”Här är ett antal frågor som du kan försöka besvara och diskutera tillsammans med din AI-assistent. Tveka inte att lyfta någon av frågorna till handledningen för att fortsätta diskussionen med lärare och studiekamrater.
- Ge mig en kort förklaring till begreppet klasser i JavaScript.
- Sparar man en klass i sin egen fil och använder man då import och export?
- Vilka är de grundläggande konstruktionerna i en klass?
- Vad innebär begreppet private kontra public i en klass?
- Bör man använda private eller public på properties och metoder?
- Jag vill lära mig att skydda data, jag har hört begreppet “encapsulation”.
- Visa mig hur getters och setters fungerar i en klass.
- Vad kan man säga kort om fördelar med objektorienterad programmering?
- Jag har lärt mig om JavaScript objektliteral och hur man skapar objekt i JavaScript med object.create. Hur förhåller det sig till klasser?
- Är det vanligt att skriva objektorienterad kod med klasser i JavaScript?
Här är en utskrift från hur det såg ut när jag diskuterade ovan frågor med min AI-assistent Ajj.
Här följer tips till hur du kan prioritera bland kursmaterialet för att komma igång med veckan.
- Det finns en deadline för A2, sikta på att möta den. Missa inte inspelningen om
modesom kan ge dig tips. - När du är klar med A2 så kan du redan nu börja titta på och läsa in dig på kraven för A3. Du kommer känna igen dig i strukturen på uppgiften.
- Föreläsningen om klasser ger dig bakgrunden till vad objektorientering handlar om och hur koden är tänkt att skrivas i A3.