Översikt
Denna vecka jobbar du vidare med objektorienterade konstruktioner och tittar bland annat på arv. Information om veckans upplägg annonseras löpande i kursens Zulip-kanal.
Stödjande inspelningar
Section titled “Stödjande inspelningar”Inkapsling
Section titled “Inkapsling”Ska alla egenskaper på ett instansierat objekt alltid vara tillgängliga utanför klassen?
Problemet: oskyddade egenskaper
Section titled “Problemet: oskyddade egenskaper”En tärningsklass med två egenskaper (faceValue, maxFaceValue) fungerar bra i normalfallet, men inget hindrar att egenskaperna sätts till vilket värde som helst utifrån:
export class Die { constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }
roll () { this.faceValue = Math.floor(Math.random() * this.maxFaceValue) + 1 }}
const die = new Die()die.faceValue = 'This is a strange value for a die!' // fullt tillåtet, men feldie.maxFaceValue = 'twentyish'die.roll() // resultatet blir NaNGår det att dölja eller skydda en del av ett objekts data för omvärlden? Ja.
Ett fält är egentligen bara en egenskap, men deklarerad ovanför konstruktorn i klassdefinitionen istället för att skapas dynamiskt inuti den. Fördelen är att alla en klass egenskaper syns samlade på ett ställe:
class Die { maxFaceValue // ett fält
constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }}Ett fält kan vara publikt eller privat – att det är ett fält handlar bara om var egenskapen deklareras, inte om den är skyddad.
Semiprivata egenskaper (den gamla konventionen)
Section titled “Semiprivata egenskaper (den gamla konventionen)”Innan JavaScript hade riktiga privata fält användes en namnkonvention: ett inledande understreck (_age) signalerade “rör inte den här utifrån” – men skyddade den inte på något sätt rent tekniskt. En sådan semiprivat egenskap går fortfarande att läsa och skriva till precis som vilken annan egenskap som helst. Konventionen lever kvar i mycket äldre kod, men är på väg att fasas ut till förmån för riktiga privata fält.
Privata fält
Section titled “Privata fält”Ett privat fält skapas med ett inledande nummertecken (#) och kan bara nås inifrån klassdefinitionen – ett genuint språkskydd, inte bara en konvention:
class Die { #faceValue #maxFaceValue
constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }}Ett privat fält måste deklareras (skrivas ut som fält i klassdefinitionen) innan det kan användas – till skillnad från vanliga egenskaper kan de inte skapas dynamiskt “i farten”. Försöker kod utanför klassen komma åt ett privat fält blir det ett tydligt fel direkt, istället för att bara ge undefined.
Getters och setters
Section titled “Getters och setters”Ett privat fält är helt otillgängligt utifrån om inget annat görs – även för att bara läsa av det. Lösningen är accessor- och mutatormetoder: en getter (get) binder en egenskap till en funktion som anropas när värdet hämtas, en setter (set) binder egenskapen till en funktion som anropas när värdet sätts:
class Dog { #age
constructor (name, age) { this.name = name this.age = age // anropar settern }
get age () { return this.#age }
set age (value) { if (!Number.isInteger(value) || value < 1) { throw new Error('Invalid age.') } this.#age = value }}En getter och setter anropas som om de vore en vanlig egenskap – myDog.age, inte myDog.age() – men implementationsmässigt är de funktioner. En getter tar aldrig emot argument (inget att skicka med vid hämtning); en setter tar alltid emot exakt ett argument (värdet som ska sättas) och kan innehålla godtycklig valideringslogik, inklusive att kasta ett undantag om värdet inte duger.
Har en klass bara en getter (ingen setter) för ett fält blir egenskapen skrivskyddad (“read-only”) – ett försök att tilldela den ett nytt värde ger ett tydligt fel istället för att tyst misslyckas.
Tärningen, tredje försöket
Section titled “Tärningen, tredje försöket”Med privata fält och getters/setters kan tärningsklassen skyddas fullt ut:
export class Die { #faceValue #maxFaceValue
constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }
get faceValue () { return this.#faceValue }
get maxFaceValue () { return this.#maxFaceValue }
set maxFaceValue (value) { if (!Number.isInteger(value) || value < 1) { throw new Error('Invalid number of faces.') } this.#maxFaceValue = value }
roll () { this.#faceValue = Math.floor(Math.random() * this.#maxFaceValue) + 1 }}faceValue har bara en getter – kan bara läsas, aldrig sättas direkt (bara via roll()). maxFaceValue har både getter och setter, men settern validerar värdet innan det accepteras. En getter/setter-kombination som bara returnerar respektive tilldelar fältets värde rakt av, utan någon egen logik, ger däremot inget skydd alls – då är det lika bra att ha en publik egenskap.
En viktig varning: referenstyper i privata fält
Section titled “En viktig varning: referenstyper i privata fält”Mönstret ovan fungerar problemfritt så länge det privata fältet är en värdetyp (som number). Är fältet istället en referenstyp (till exempel en array) räcker det inte att bara returnera fältet direkt i en getter – då lämnas referensen ut, och den som anropar kan muterar det “privata” innehållet ändå (en så kallad “privacy leak”). I det fallet behöver gettern istället returnera en kopia av objektet, annars är inkapslingen meningslös.
Beräknade egenskaper
Section titled “Beräknade egenskaper”Ibland finns det ingen underliggande fält alls bakom en getter – värdet som returneras kan istället vara beräknat från flera andra fält (till exempel en area beräknad från bredd och höjd). Det finns ingen anledning att lagra ett sådant värde separat när det ändå kan räknas fram vid behov.
Återanvändning av kod kan ske på många olika sätt. Här behandlas mekanismen “Arv”.
Upprepning av kod
Section titled “Upprepning av kod”Två klasser, Person och Student, kan hamna med exakt samma kod – samma egenskaper (name, age) och samma toString-metod, plus en eller två extra delar i Student. Behöver det vara så? Nej – med hjälp av arv (“inheritance”) kan kod återanvändas istället för att upprepas.
Återanvändning av kod genom arv
Section titled “Återanvändning av kod genom arv”Genom att låta Student ärva från Person med extends slipper Student duplicera det som redan finns i Person:
export class Person { constructor (name, age) { this.name = name this.age = age }
toString () { return `Jag, ${this.name}, är ${this.age} år.` }}
// student.jsimport { Person } from './person.js'
export class Student extends Person { constructor (name, age, onCampus) { super(name, age) this.onCampus = onCampus }
toString () { const form = this.onCampus ? 'campus' : 'distans' return `Jag, ${this.name}, läser på ${form}, och är ${this.age} år.` }}Person sägs vara basklass till Student, och Student utökar basklassen. Tumregeln för när arv passar: en Student är en Person (aldrig tvärtom) – men en Student är till exempel inte en hund, även om en hypotetisk Dog-klass också råkade ha name och age. Det gemensamma måste vara en genuin “är en”-relation, inte bara råkat ha samma egenskaper.
super(name, age) i konstruktorn anropar basklassens konstruktor för att initiera det den redan vet hur den ska initiera – annars måste Student duplicera det arbetet. super måste anropas innan this används i en konstruktor som ärver, och görs det så gott som alltid när arv används. Students egen toString överskuggar (“overrides”) den som ärvts från Person – anropas toString på ett Student-objekt är det alltid den mest specifika versionen (närmast i kedjan) som körs. Utelämnas den egna toString helt används istället Persons version.
Klassdiagram
Section titled “Klassdiagram”Ett klassdiagram (enligt standarden UML) är ett sätt att beskriva klasser och deras relationer utan att behöva rita alla de faktiska prototypreferenserna. Det består av tre fält: klassens namn överst, dess egenskaper i mitten, och dess metoder längst ner. En pil mellan två klasser visar arv – att den ena ärver från (är en) den andra.
Arv under huven
Section titled “Arv under huven”Bakom kulisserna byggs faktiskt två separata prototypkedjor upp när Student ärver från Person: en instansprototypkedja (en instans → Student.prototype → Person.prototype → Object.prototype) och en klassprototypkedja (Student → Person → Function.prototype, eftersom klasser i grunden är funktioner). Det är den förstnämnda som är intressant i praktiken. Klassdiagrammet ovan säger däremot ingenting om detta – det är en enklare, mer lättarbetad representation, men det är viktigt att komma ihåg att JavaScript gör betydligt mer under huven än vad diagrammet visar.
instanceof
Section titled “instanceof”Operatorn instanceof avgör om ett värde är en instans av en given klass, genom att följa just prototypkedjan:
const person = new Person('Nisse', 42)const student = new Student('Ellen', 23, true)
console.log(person instanceof Person) // OUTPUT: trueconsole.log(person instanceof Student) // OUTPUT: false – Nisse är ingen studentconsole.log(student instanceof Person) // OUTPUT: true – en student är en personconsole.log(student instanceof Student) // OUTPUT: trueinstanceof blir särskilt användbart längre fram i kursen vid felhantering: när ett undantag fångas i ett catch-block kan man fråga felobjektet vilken specifik feltyp det är, och hantera olika typer av fel på olika sätt.
Validering och strukturerad felhantering
Section titled “Validering och strukturerad felhantering”Att validera exempelvis strängar, kontrollera att de är skrivna på ett visst format, är mycket vanligt. Denna inspelning tittar på reguljära uttryck och ser om de kan hjälpa (eller stjälpa?) dig.
Validera en egenskaps värde
Section titled “Validera en egenskaps värde”Att kontrollera att en egenskap är korrekt formaterad – till exempel att ett svenskt postnummer består av fem siffror, eventuellt med ett mellanslag mellan tredje och fjärde siffran – kan göras med en handskriven kontroll i en setter, tecken för tecken:
set postalCode (value) { let valid = false if (value.length === 5 || (value.length === 6 && value.indexOf(' ') === 3)) { let i = 0 for (; i < value.length; i++) { if (value[i] === ' ') continue if (value[i] < '0' || value[i] > '9' || (i === 0 && value[i] === '0')) break } valid = i >= 5 && i <= 6 } if (!valid) { throw new Error('Invalid postal code.') } this._postalCode = value}Det fungerar, men blir snabbt både svårt att skriva korrekt och tungt att underhålla – och en enda ny regel (t.ex. att även sexsiffriga postnummer utan mellanslag ska godkännas) kräver att man river upp hela logiken igen.
Reguljära uttryck till undsättning
Section titled “Reguljära uttryck till undsättning”Ett reguljärt uttryck är ett mönster som beskriver kombinationer av tecken i en sträng, skrivet mellan snedstreck (/mönster/). Samma valideringslogik som ovan kan uttryckas i en enda rad:
set postalCode (value) { if (!/^[1-9]\d{2} ?\d{2}$/.test(value)) { throw new Error('Invalid postal code.') } this._postalCode = value}Uttrycket /^[1-9]\d{2} ?\d{2}$/ läses del för del: ^ markerar start av strängen, [1-9] exakt en siffra 1–9 (inte 0, eftersom postnummer aldrig börjar på 0), \d{2} exakt två siffror, ⎵? noll eller ett mellanslag, \d{2} ytterligare två siffror, och $ slutet av strängen. Ska även sexsiffriga postnummer (utan krav på mellanslag) godkännas räcker det att byta ut det sista \d{2} mot \d{2,3} – bara den delen behöver ändras.
Reguljära uttryck finns i de flesta programmeringsspråk (Python, Java, C# m.fl.), och det finns gott om webbaserade verktyg (till exempel regex101 och ExtendsClass Regex Tester) för att testa och förstå ett uttryck interaktivt innan det klistras in i koden. Mer finns att läsa på MDN under “Regular expressions”.
Kommandoradargument
Section titled “Kommandoradargument”En applikation kan göras mer generell genom att ta emot kommandoradargument när den startar (node src/app.js ett två 3 4). Node gör listan med argument – argumentvektorn – tillgänglig som en array via process.argv. De två första elementen (sökvägen till node.exe och sökvägen till applikationsfilen) är oftast ointressanta och hoppas över med Array#slice:
const add = function (a, b) { return a + b}
const myArgs = process.argv.slice(2)const sum = add(Number(myArgs[0]), Number(myArgs[1]))Ett alternativt, kortare sätt att plocka ut de två första relevanta argumenten och samtidigt konvertera dem till tal:
const [first, second] = process.argv.slice(2).map(Number)const sum = add(first, second)Avsluta en Node-applikation
Section titled “Avsluta en Node-applikation”Går allt bra sätter Node automatiskt applikationens avslutningskod till 0 när den avslutas. Kastas ett undantag som inte hanteras avslutas applikationen istället med avslutningskod 1. Det går att bestämma en egen avslutningskod innan applikationen avslutas – vanligtvis genom att sätta process.exitCode till ett värde och låta Node avsluta applikationen på ett kontrollerat sätt (att tvinga fram ett direkt avslut med process.exit() är oftast onödigt och bör undvikas):
try { main()} catch (err) { console.error(err.message) process.exitCode = 1
if (err instanceof MyError) { process.exitCode = 123 }}Genom att kontrollera vilken feltyp som fångades (med instanceof, se förra veckans genomgång av arv) kan olika fel resultera i olika avslutningskoder – något som blir relevant längre fram när automatiska tester ska tolka om en körning lyckades eller misslyckades.
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 A3
Section titled “Examinationsuppgift A3”Efter denna veckas innehåll är du fullt förberedd för att genomföra examinationsuppgift A3. Ett bra mål för denna vecka är att klona ner uppgiften och börja bekanta dig med den.
Läs mer om “Examinationsuppgift A3 - Tjugoett” här
Utmaningar
Section titled “Utmaningar”Denna veckans utmaning är lite annorlunda och låter dig skriva en egen algoritm för att lösa ett problem. Därefter kan du jämföra din lösning med andra lösningar som lärare och studenter kommit fram till. Hur bra kommer din lösning att vara och vad kan du lära dig av att studera andra lösningar på samma problem?
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.
- Jag håller på och lär mig JavaScript med objekt och klasser och jag vill bygga upp ett exempelprogram för att studera olika aspekter av objektorientering, jag kommer ge dig instruktioner så kan du föreslå kodlösningar och kommentera dem så kan jag lära mig från dig, ok?
- Jag vill börja med en tärningsklass Dice där jag skall rulla en 6-sidig tärning och skriva ut tärningens värde efter varje slag. Hur hade du gjort?
- Jag vill använda privata medlemmar för att kapsla in min implementation.
- Nu vill jag se hur man kan göra en klass GraphicalDice som ärver från Dice och skriver ut tärningen som utf-8 tecken.
- Nu vill jag skapa en klass för DiceHand och jag vill ha en metod där man kan lägga till tärningar till DiceHand och sedan rulla dem och skriva ut alla samtidigt.
- Lägg till metoder för att beräkna summan av tärningsslaget.
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.
- Försök komma igång med A3 redan denna veckan. Det är mycket att läsa och låt det ta ett par dagar att läsa, bearbeta och fundera på hur uppgiften kan angripas. Det är ingen brådska att komma igång att koda, låt problemlösningen ta sin tid och läsa igenom kravställningen.
- Jobba igenom de blå övningsuppgifterna så att du har koll på grundläggande konstruktioner i en klass och hur man jobbar med ett objekt. Då får du också grundläggande träning i hur man implemementerar klasser utifrån ett klassdiagram.
