Getters, Setters och Organizing Responsibility in JavaScript

en gång i tiden fanns det ett språk som heter C, 1 och det här språket hade något som heter a struct, och du kan använda den för att skapa heterogent aggregerade datastrukturer som hade medlemmar. Det viktigaste att veta om C är att när du har en struktur som heter currentUser och en medlem som id, och du skriver något som currentUser.id = 42, gjorde C complier detta till extremt snabba monteringsinstruktioner. Samma för int id = currentUser.id.

också viktigt var att du kunde ha pekare till funktioner i strukturer, så att du kunde skriva saker som currentUser->setId(42) om du föredrog att göra en id en funktion, och detta översattes också till fast assembler.

och slutligen har C-programmering en mycket stark kultur av att föredra ” extremt snabb ”till bara” snabb ” och därmed om du ville ha en C-programmerares uppmärksamhet, var du tvungen att se till att du aldrig gör något som bara är snabbt när du kunde göra något som är extremt snabbt. Detta är naturligtvis en generalisering. Jag är säker på att om vi frågar runt kommer vi så småningom att träffa båda C-programmerare som föredrar eleganta abstraktioner framför extremt snabb kod.

Flathead Dragster

java och javascript

sedan fanns det ett språk som heter Java, och det var utformat för att köras i webbläsare och vara bärbart över alla typer av hårdvara och operativsystem, och ett av dess mål var att få C-programmerare att skriva Java-kod i webbläsaren istället för att skriva C som bodde i ett plugin. Eller snarare, det var en av dess strategier, målet var att Sun Microsystems skulle vara relevant i en värld som Microsoft commoditizing, men det är ett annat kapitel i historieboken.

så de trevliga människorna bakom Java gav den C-liknande syntax med hängslen och uttalandet/uttrycket dikotomi och punktnotationen. De har ”objekt” istället för strukturer, och objekt har mycket mer på gång än strukturer, men Java: s designers gjorde en åtskillnad mellan currentUser.id = 42 och currentUser.setId(42) och såg till att den ena var extremt snabb och den andra var bara snabb. Eller snarare, den var snabb och den andra var bara ok jämfört med C, men C-programmerare kunde känna att de gjorde viktigt tänkande när de bestämde sig för om id borde vara direkt åtkomlig för prestanda eller indirekt åtkomlig för elegans och flexibilitet.

historien har visat att detta var rätt sätt att sälja ett nytt språk. Historien har också visat att den faktiska prestationsskillnaden var irrelevant för nästan alla. Prestanda är bara för nu, kodflexibilitet är för alltid.

Tja, det visade sig att Sun hade rätt om att få C-programmerare att använda Java (det fungerade på mig, jag drog CodeWarrior och Lightspeed C), men fel om att använda Java i webbläsare. Istället började folk använda ett annat språk som heter JavaScript för att skriva kod i webbläsare och använde Java för att skriva kod på servrar.

kommer det att överraska dig att lära dig att JavaScript också var utformat för att få C-programmerare att skriva kod? Och att det gick med den C-liknande syntaxen med lockiga hängslen, uttalandet/uttrycket dikotomi och punktnotation? Och även om JavaScript har en sak som är kinda-sorta som ett Java-objekt, och kinda-sorta som en Smalltalk-ordbok, kommer det att överraska dig att lära dig att JavaScript också skiljer mellan currentUser.id = 42 och currentUser.setId(42)? Och det var ursprungligen en långsam, och den andra hunden-långsam, men programmerare kunde göra viktiga tankar om när man ska optimera för prestanda och när man ska ge en hoot om programmerare förnuft?

Nej, det kommer inte att överraska dig att lära dig att det fungerar kinda-sorta som C på samma sätt som Java kinda-sort fungerar som C, och av exakt samma anledning. Och anledningen spelar ingen roll längre.

Professor Frink på Java

problemet med direktåtkomst

mycket snart efter att människor började arbeta med Java i skala, lärde de sig att direkt åtkomst till instansvariabler var en hemsk ide. JIT-kompilatorer minskade prestandaskillnaden mellan currentUser.id = 42 och currentUser.setId(42) till nästan ingenting av relevans för någon, och kod med currentUser.id = 42 eller int id = currentUser.id var anmärkningsvärt oflexibel.

det fanns inget sätt att dekorera sådana operationer med övergripande problem som loggning eller validering. Du kunde inte åsidosätta beteendet att ställa in eller få ett id I en underklass. (Java programmerare älskar underklasser!)

under tiden skrev JavaScript-programmerare också currentUser.id = 42, och så småningom upptäckte de också att detta var en hemsk ide. En av katalysatorerna för förändring var ankomsten av ramar för JavaScript-applikationer på klientsidan. Låt oss säga att vi har en löjligt enkel personklass:

class Person { constructor (first, last) { this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};

och en lika löjlig vy:

class PersonView { constructor (person) { this.model = person; } // ... redraw () { document .querySelector(`person-${person.id}`) .text(person.fullName()) }}

varje gång vi uppdaterar personklassen måste vi komma ihåg att rita om vyn:

const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUserView.redraw();currentUser.first = 'Ragnvald';currentUserView.redraw();

Varför spelar det någon roll?

Tja, om du inte kan kontrollera var vissa ansvarsområden hanteras kan du inte riktigt organisera ditt program. Underklasser, metoder, mixins och dekoratörer är tekniker: vad de möjliggör är att välja vilken kod som är ansvarig för vilken funktionalitet.

och det är det hela med programmering: organisera funktionaliteten. Direktåtkomst tillåter dig inte att organisera funktionaliteten i samband med att få och ställa in egenskaper, det tvingar koden att göra att få och ställa in att också vara ansvarig för allt annat som är förknippat med att få och ställa in.

Magnetic Core Memory

hämta och ställ in

det tog inte lång tid för JavaScript-biblioteksförfattare att ta reda på hur man gör det här med hjälp av en get och set metod. Avskalad till det väsentliga för illustrativa ändamål, vi kunde skriva detta:

class Model { constructor () { this.listeners = new Set(); } get (property) { this.notifyAll('get', property, this); return this; } set (property, value) { this.notifyAll('set', property, value); return this = value; } addListener (listener) { this.listeners.add(listener); } deleteListener (listener) { this.listeners.delete(listener); } notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}class Person extends Model { constructor (first, last) { super(); this.set('first', first); this.set('last', last); } fullName () { return `${this.get('first')} ${this.get('last')}`; }};class View { constructor (model) { this.model = model; model.addListener(this); }}class PersonView extends View { // ... notify(notifier, method, ...args) { if (notifier === this.model && method === 'set') this.redraw(); } redraw () { document .querySelector(`person-${this.model.id}`) .text(this.model.fullName()) }}

vår nya Model superclass hanterar manuellt tillåter objekt att lyssna på get och set metoder på en modell. Om de kallas, meddelas ”lyssnare”via .notifyAll – metoden. Vi använder det för att haPersonView lyssna på dessPerson och ring sin egen.redraw metod när en egenskap ställs in via.set metod.

så vi kan skriva:

const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.set('first', 'Ragnvald');

och vi behöver inte ringa currentUserView.redraw(), eftersom meddelandet inbyggt i .set gör det för oss.

Vi kan göra andra saker med .get och .set, förstås. Nu när de är metoder kan vi dekorera dem med loggning eller validering om vi väljer. Metoder gör vår kod flexibel och öppen för förlängning. Till exempel kan vi använda en ES.senare dekoratör för att lägga till loggningsråd till .set:

const after = (behaviour, ...methodNames) => (clazz) => { for (let methodName of methodNames) { const method = clazz.prototype; Object.defineProperty(clazz.prototype, methodName, { value: function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }, writable: true }); } return clazz; }function LogSetter (model, property, value) { console.log(`Setting ${property} of ${model.fullName()} to ${value}`);}@after(LogSetter, 'set')class Person extends Model { constructor (first, last) { super(); this.set('first', first); this.set('last', last); } fullName () { return `${this.get('first')} ${this.get('last')}`; }};

medan vi inte kan göra något liknande med direkt tillgång till egendom. Att förmedla fastighetsåtkomst med metoder är mer flexibelt än direkt åtkomst till fastigheter, och detta gör att vi kan organisera vårt program och distribuera ansvaret ordentligt.

notera: alla ES.senare klass dekoratörer kan användas i vanilj ES 6 kod som vanliga funktioner. Istället för @after(LogSetter, 'set') class Person extends Model {...}, skriv helt enkelt const Person = after(LogSetter, 'set')(class Person extends Model {...})

tekniker

getters och setters i javascript

problemet med getters och setters var väl förstått, och förvaltarna bakom javascript: s utveckling svarade genom att införa ett speciellt sätt att göra direkt tillgång till egendom till en slags metod. Så här skriver vi vårPerson klass med ”getters” och ” setters:”

class Model { constructor () { this.listeners = new Set(); } addListener (listener) { this.listeners.add(listener); } deleteListener (listener) { this.listeners.delete(listener); } notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}const _first = Symbol('first'), _last = Symbol('last');class Person extends Model { constructor (first, last) { super(); this.first = first; this.last = last; } get first () { this.notifyAll('get', 'first', this); return this; } set first (value) { this.notifyAll('set', 'first', value); return this = value; } get last () { this.notifyAll('get', 'last', this); return this; } set last (value) { this.notifyAll('set', 'last', value); return this = value; } fullName () { return `${this.first} ${this.last}`; }};

När vi inleder en metod med nyckelordet get definierar vi en getter, en metod som kommer att kallas när kod försöker läsa från egenskapen. Och när vi förord en metod med set, vi definierar en setter, en metod som kommer att kallas när koden försöker skriva till egenskapen.

Getters och setters behöver faktiskt inte läsa eller skriva några egenskaper, de kan göra någonting. Men i den här uppsatsen talar vi om att använda dem för att förmedla tillgång till egendom. Med getters och setters kan vi skriva:

const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.first = 'Ragnvald';

och allt fungerar fortfarande precis som om vi hade skrivit currentUser.set('first', 'Ragnvald') med .set-stilkod.

Getters och setters tillåter oss att ha semantiken att använda metoder, men syntaxen för direktåtkomst.

Keypunch

en efterkombinator som kan hantera getters och setters

Getters och setters verkar vid första anblicken vara en magisk kombination av bekant syntax och kraftfull förmåga att meta-program. En getter eller setter är dock inte en metod i vanlig mening. Så vi kan inte dekorera en setter med exakt samma kod som vi skulle använda för att dekorera en vanlig metod.

med.set – metoden kunde vi direkt komma åtModel.prototype.set och linda in den i en annan funktion. Så fungerar våra dekoratörer. Men det finns ingen Person.prototype.first metod. Istället finns det en egenskapsbeskrivare som vi bara kan introspektera med Object.getOwnPropertyDescriptor() och uppdatera med Object.defineProperty().

av denna anledning fungerar inte Na-dekoratörenafter dekoratör som anges ovan för getters och setters.2 Vi skulle behöva använda en typ av dekoratör för metoder, en annan för getters och en tredje för setters. Det låter inte som kul, så låt oss ändra vårafter combinator så att du kan använda en enda funktion med metoder, getters och setters:

function getPropertyDescriptor (obj, property) { if (obj == null) return null; const descriptor = Object.getOwnPropertyDescriptor(obj, property); if (obj.hasOwnProperty(property)) return Object.getOwnPropertyDescriptor(obj, property); else return getPropertyDescriptor(Object.getPrototypeOf(obj), property);};const after = (behaviour, ...methodNames) => (clazz) => { for (let methodNameExpr of methodNames) { const = methodNameExpr.match(/^(?:(get|set) )(.+)$/); const descriptor = getPropertyDescriptor(clazz.prototype, methodName); if (accessor == null) { const method = clazz.prototype; descriptor.value = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.writable = true; } else if (accessor === "get") { const method = descriptor.get; descriptor.get = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.configurable = true; } else if (accessor === "set") { const method = descriptor.set; descriptor.set = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.configurable = true; } Object.defineProperty(clazz.prototype, methodName, descriptor); } return clazz; }

Nu kan vi skriva:

const notify = (name) => function (...args) { this.notifyAll(name, ...args); };@after(notify('set'), 'set first', 'set last')@after(notify('get'), 'get first', 'get last')class Person extends Model { constructor (first, last) { super(); this.first = first; this.last = last; } get first () { return this; } set first (value) { return this = value; } get last () { return this; } set last (value) { return this = value; } fullName () { return `${this.first} ${this.last}`; }};

Vi har nu frikopplat koden för att meddela lyssnare från koden för att få och ställa in värden. Vilket framkallar en enkel fråga: om koden som spårar lyssnare redan är avkopplad i Model, varför ska inte koden för att utlösa aviseringar vara i samma enhet?

det finns några sätt att göra det. Vi använder en universell mixin istället för att fylla den logiken i en superklass:

const Notifier = mixin({ init () { this.listeners = new Set(); }, addListener (listener) { this.listeners.add(listener); }, deleteListener (listener) { this.listeners.delete(listener); }, notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}, { notify (name) { return function (...args) { this.notifyAll(name, ...args); } }});

detta tillåter oss att skriva:

@Notifier@after(Notifier.notify('set'), 'set first', 'set last')@after(Notifier.notify('get'), 'get first', 'get last')class Person { constructor (first, last) { this.init(); this.first = first; this.last = last; } get first () { return this; } set first (value) { return this = value; } get last () { return this; } set last (value) { return this = value; } fullName () { return `${this.first} ${this.last}`; }};

vad har vi gjort? Vi har införlivat getters och setters i vår kod, samtidigt som vi behåller möjligheten att dekorera dem med extra funktionalitet som om de var vanliga metoder.

det är en vinst för att sönderdela kod. Och det pekar på något att tänka på: när allt du har är metoder, uppmuntras du att göra tunga superklasser. Därför tvingar så många ramar dig att utöka sina specialklasser som Model eller View.

men när du hittar ett sätt att använda mixins och dekorera metoder kan du sönderdela saker i mindre bitar och applicera dem där de behövs. Detta leder i riktning mot att använda samlingar av bibliotek istället för en tungviktsram.

sammanfattning

Getters och setters tillåter oss att behålla den äldre stilen för att skriva kod som verkar direkt komma åt egenskaper, samtidigt som vi faktiskt förmedlar den åtkomsten med metoder. Med omsorg kan vi uppdatera vårt verktyg så att vi kan dekorera våra getters och setters, fördela ansvaret som vi tycker är lämpligt och befria oss från beroende av tungviktiga basklasser.

(diskutera på Hacker News)

Post Scriptum

detta inlägg använder lyssna på egendom setters som en ursäkt för att diskutera getter och setter mekanismer, och sätt att dekorera dem så att vi kan organisera kod kring oro.

naturligtvis är förökningsändringar genom uttrycklig anmälan inte det enda sättet att organisera kod som behöver hantera beroenden på databyte. Det är utanför ramen för detta inlägg att diskutera de många alternativen, men läsarna har föreslagit att utforska objekt.observera och arbeta med oföränderliga data.

flygande tefat för alla

en sak till

Rubyists hånar på:

get first () { return this;}set first (value) { return this = value;}get last () { return this;}set last (value) { return this = value;}

Rubyists skulle använda den inbyggda klassmetoden attr_accessorför att skriva dem för oss. Så bara för sparkar skriver vi en dekoratör som skriver getters och setters. Raw-värdena lagras i ettattributes karta:

function attrAccessor (...propertyNames) { return function (clazzOrObject) { const target = clazzOrObject.prototype || clazzOrObject; for (let propertyName of propertyNames) { Object.defineProperty(target, propertyName, { get: function () { if (this.attributes) return this.attributes; }, set: function (value) { if (this.attributes == undefined) this.attributes = new Map(); return this.attributes = value; }, configurable: true, enumerable: true }) } return clazzOrObject; }}

Nu kan vi skriva:

@Notifier@attrAccessor('first', 'last')@after(Notifier.notify('set'), 'set first', 'set last')@after(Notifier.notify('get'), 'get first', 'get last')class Person { constructor (first, last) { this.init(); this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};

attrAccessor tar en lista över fastighetsnamn och returnerar en dekoratör för en klass. Den skriver en vanlig getter-eller setter-funktion för varje egenskap, och alla definierade egenskaper lagras i .attributes hash. Detta är mycket bekvämt för serialisering eller andra persistensmekanismer.

(det är trivialt att också göra attrReader och attrWriter funktioner med den här mallen. Vi behöver bara utelämna setnär du skriver attrReaderoch utelämna getnär du skriver attrWriter.)

  1. det fanns också ett språk som heter BCPL, och andra innan det, men vår historia måste börja någonstans, och det börjar med C. bisexual

  2. inte heller kommer mixin recept vi har utvecklats i tidigare inlägg som att använda ES.senare dekoratörer som Mixins. Det kan förbättras för att lägga till ett specialfall för getters, setters och andra problem som att arbeta med POJOs. Till exempel Andrea Giammarchi ’ s Universal Mixin.

Lämna ett svar

Din e-postadress kommer inte publiceras.