Dataindkapsling i JavaScript: getters og setters
Når man opbygger større JavaScript-applikationer, er der hurtigt behov for at opdele dem i moduler, der er forbundet med klare kontrakter. Når det drejer sig om langsigtet vedligeholdelse, er der behov for en måde at bevare kompatibiliteten ved ændringer af grænsefladen på. For at gøre det, er objektorienterede sprog afhængige af indkapsling eller information, der skjuler implementeringsdetaljer fra brugerne af et stykke kode, så det kan ændres uden at påvirke klienterne.
I dette indlæg vil vi tale om dataindkapsling i JavaScript. Mere specifikt vil vi tale om indkapsling af egenskaber bag getters og setters.
Nogle begreber
JavaScript blev designet med princippet om andetypning som grundlag:
“Når jeg ser en fugl, der går som en and og svømmer som en and og kvækker som en and, kalder jeg den fugl for en and.”
Med andre ord er JavaScript i sin kerne et objektorienteret sprog, men til forskel fra andre objektorienterede sprog som C# og Java er klasser i JavaScript andenrangsborgere, og interfaces eksisterer ikke. Objekter har ingen typer, og derfor skal der skrives kode for at skabe grænseflader til objekter, der har bestemte egenskaber og metoder. Kontrakter mellem moduler er baseret på et sæt metoder og egenskaber, som antages at være til stede i de udvekslede objekter.
Indkapsling er grundlæggende i objektorienteret programmering. Det betyder, at et givet objekt bør være i stand til at skjule detaljerne i sit indre arbejde, således at andre objekter, der interagerer med det, ikke er afhængige af detaljerne i dets implementering, men af en grænseflade på højt niveau, der er aftalt. Dette gør livet for udviklere enklere, både på udbyderens og brugernes side, da begge ved, at implementeringsdetaljerne kan ændres uden at ødelægge klientobjekter.
Hvorfor det er en dårlig idé at eksponere data på en grænseflade
I JavaScript er et objekt simpelthen en ordbog af egenskabsnavne, der er afbildet på værdier, og når værdien af en egenskab er en funktion, kalder vi det en metode. Grænsefladen mellem metoder er det sæt af egenskaber, som klientkoden forventer at finde på et objekt.
Hvis et objekt kun eksponerer metoder, er det let at få grænsefladen til at udvikle sig med tiden. En metode kan kontrollere sine parametre og reagere i overensstemmelse hermed. Det er også muligt at oprette nye metoder, der giver nye funktioner. Og støtte de gamle metoder, der tilpasser den gamle adfærd til den nye. Med egenskaber er det ikke muligt at gøre det.
Med egenskaber er det ikke let at holde en stabil grænseflade. For som standard kan klientkode henvise til ikke-eksisterende egenskaber for et objekt. Hvis man skriver til en ikke-eksisterende egenskab, vil den blive oprettet. Læsning af den vil returnere udefineret. Klientkode vil ikke hurtigt kunne opdage, om den anvender deprecated properties. Hvad værre er, kan fejlen sprede sig til andre dele af koden, hvilket gør det vanskeligere at opdage problemet.
Den standardiserede JavaScript-API har metoder, der kan være nyttige til at undgå sådanne problemer: Det er muligt at fryse eller forsegle et objekt, så ikke understøttede egenskabsnavne ikke kan bruges.
Getter/Setters skal redde dagen
Det er nemt at skjule detaljerne i metodeimplementeringen.
Getters/sætters er blevet officielt indført i sproget i ECMAScript 5.1 (ECMA-262). De understøttes i øjeblikket i alle større desktop- og mobilbrowsere.
Den grundlæggende idé er, at den tilføjer syntaks til at definere accessor-egenskaber som metoder i stedet for simple dataegenskaber. En getter defineres ved nøgleordet get efterfulgt af en funktion opkaldt efter egenskaben, der ikke tager nogen argumenter og returnerer værdien af egenskaben. En setter defineres ved nøgleordet set efterfulgt af en funktion, der er opkaldt efter egenskaben, og som tager egenskabens nye værdi som en parameter.
Det følgende eksempel illustrerer en getter og en setter, der bruges til at definere en accessor-egenskab kaldet prop:
var obj = { v: 0, get prop() { return this.v; }, set prop(newValue) { this.v = newValue; }};console.log(obj.prop);obj.prop = 42;console.log(obj.prop);
Output:
042
Man kan også oprette en skrivebeskyttet ejendom ved at definere en getter uden setter:
var obj = { get prop() { return -1; },};console.log(obj.prop);obj.prop = 42;console.log(obj.prop);
Output:
-1-1
En egenskab kan så erstattes af et par metoder, der i tilfælde af, at implementeringen af en klasse ændres, kan reagere på det og tilpasse klassens adfærd. I værste fald kan den rejse en undtagelse for at fortælle brugeren, at egenskaben er deprecated og ikke bør bruges længere.
Videre læsning
Jeg håber, at du har lært noget nyt om design af komplekse evolutive applikationer i JavaScript i dette indlæg. I dette afsnit vil jeg give dig et par links til links, der indeholder flere oplysninger.
Introduktionsartikler
Dette afsnit indeholder et par korte artikler, der præsenterer begreberne og giver et overblik over de relaterede API’er:
- Property getters and setters.
- JavaScript Getters og Setters
Mere avancerede artikler
Disse artikler omhandler mere avancerede relaterede emner som f.eks. de problemer, som getters og setters medfører, og hvordan de kan løses.
- Hvorfor getters/setters er en dårlig idé i JavaScript
- Dataindkapsling i JavaScript
Referencedokumentation
Finalt, for virkelig at mestre emnet, er der links til mozilla-dokumentationen af de relaterede API’er:
- get og set giver mulighed for at definere getters og setters. Ud over det, vi har forklaret her. Der er andre interessante funktioner som f.eks. understøttelse af dynamisk genererede egenskabsnavne og brug af getters og setters til at simulere indstilling/indhentning af værdier i et array
- seal og freeze giver mulighed for at styre, hvilke egenskaber i et objekt der kan ændres eller ej og hvordan. Det kan bruges til at undgå, at klienter bruger forældede dele af et objekts API.
- defineProperty giver mulighed for at definere getters og setters og samtidig have mere kontrol over, hvordan disse egenskaber ses af klienter, og hvor modificerbare de er.