Getters, Settere Og Organiseringsansvar I JavaScript

En gang var det et språk som heter C,1 og dette språket hadde noe som heter en struct, og du kan bruke den til å lage heterogent aggregerte datastrukturer som hadde medlemmer. Det viktigste å vite Om C er at når du har en struct kalt currentUser, og et medlem somid, og du skriver noe somcurrentUser.id = 42, gjorde c-komplikatoren dette til ekstremt raske assembler-instruksjoner. Samme for int id = currentUser.id.

også av betydning var at du kunne ha pekere til funksjoner i structs, slik at du kunne skrive ting som currentUser->setId(42)hvis du foretrakk å sette inn enid en funksjon, og dette ble også oversatt til fast assembler.Og Til slutt Har C-programmering en veldig sterk kultur for å foretrekke » ekstremt rask «til bare» rask», og dermed hvis Du ønsket En c-programmerer oppmerksomhet, måtte du sørge for at du aldri gjør noe som er bare fort når du kunne gjøre noe som er ekstremt fort. Dette er selvsagt en generalisering. Jeg er sikker på at hvis vi spør rundt, vil vi til slutt møte Begge c-programmerere som foretrekker elegante abstraksjoner til ekstremt rask kode.

Flathead Dragster

java og javascript

Så var det et språk som heter Java, Og det var designet for å kjøre I nettlesere, og være bærbar på tvers av alle slags maskinvare og operativsystemer, og et av målene var å få c-programmerere til å skrive Java-kode i nettleseren i stedet For å skrive C som levde i en plugin. Eller rettere sagt, det var en av strategiene, målet Var At Sun Microsystems skulle være relevant I En verden Som Microsoft var commoditizing, men Det er et annet kapittel i historieboken.

så de hyggelige menneskene bak Java ga Det C-lignende syntaks med bøylene og setningen / uttrykket dikotomi og punktnotasjonen. De har «objekter» i stedet for structs, og objekter har mye mer å gå på enn structs, Men Javas designere skiller mellom currentUser.id = 42og currentUser.setId(42), og sørget for at man var ekstremt rask og den andre var bare rask. Eller heller, den ene var rask, og den andre var bare ok i Forhold Til C, Men c-programmerere kunne føle at De gjorde viktig tenkning når de bestemte seg for omid burde være direkte tilgjengelig for ytelse eller indirekte tilgjengelig for eleganse og fleksibilitet.

Historien har vist at dette var den riktige måten å selge et nytt språk på. Historien har også vist at den faktiske prestasjonsforskjellen var irrelevant for nesten alle. Ytelse er bare for nå, kode fleksibilitet er for alltid.vel, Det viste Seg At Sun hadde rett om å få c-programmerere til Å bruke Java (det fungerte på Meg, jeg droppet CodeWarrior Og Lightspeed C), men feil om Å bruke Java i nettlesere. I stedet begynte folk å bruke Et annet språk Kalt JavaScript for å skrive kode i nettlesere, og brukte Java til å skrive kode på servere.Vil Det overraske deg å høre At JavaScript også ble designet for Å få c-programmerere til å skrive kode? Og at Det gikk Med C – lignende syntaks med krøllete braces, setningen/uttrykket dikotomi og dot notasjon? Og Selv Om JavaScript har en ting som er ganske-sorta som Et Java-objekt, og ganske-sorta som En Smalltalk-ordbok, vil Det overraske deg å lære At JavaScript også har et skille mellom currentUser.id = 42og currentUser.setId(42)? Og det opprinnelig var en sakte, og den andre hunden-sakte, men programmerere kunne gjøre viktig å tenke på når man skal optimalisere for ytelse og når man skal gi et hoot om programmerer sunnhet?

Nei, det vil ikke overraske deg å lære at Det fungerer ganske-sorta Som C på samme måte Som Java kinda-sort fungerer Som C, og av nøyaktig samme grunn. Og grunnen spiller egentlig ingen rolle lenger.

Professor Frink På Java

problemet med direkte tilgang

Veldig snart etter at folk begynte å jobbe Med Java i skala, lærte de at direkte tilgang til instansvariabler var en forferdelig ide. JIT-kompilatorer reduserte ytelsesforskjellen mellomcurrentUser.id = 42 og currentUser.setId(42) til nesten ingenting av relevans for noen, og kode som bruktecurrentUser.id = 42 ellerint id = currentUser.id var bemerkelsesverdig ufleksibel.

det var ingen måte å dekorere slike operasjoner med tverrgående bekymringer som logging eller validering. Du kan ikke overstyre oppførselen til å sette eller få en id i en underklasse. (Java-programmerere elsker underklasser!)

I Mellomtiden Skrev JavaScript-programmerere også currentUser.id = 42, og til slutt oppdaget de også at dette var en forferdelig ide. En av katalysatorene for endring var ankomsten av rammer for Klientsiden JavaScript-applikasjoner. La oss si at vi har en latterlig enkel personklasse:

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

Og en like latterlig visning:

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

redraw utsikten:

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

Hvorfor betyr dette noe?

Vel, Hvis du ikke kan kontrollere hvor visse ansvarsområder håndteres, kan du egentlig ikke organisere programmet ditt. Underklasser, metoder, mixins og dekoratører er teknikker: det de gjør mulig er å velge hvilken kode som er ansvarlig for hvilken funksjonalitet.

og det er hele greia med programmering: Organisering av funksjonaliteten. Direkte tilgang tillater deg ikke å organisere funksjonaliteten knyttet til å få og sette egenskaper, det tvinger koden gjør får og innstillingen for å også være ansvarlig for noe annet knyttet til får og innstilling.

Magnetisk Kjerneminne

få og sett

Det tok ikke lang Tid For JavaScript-bibliotekforfattere å finne ut hvordan Du får Dette til å gå bort ved å bruke en get og set metode. Strippet ned til det aller nødvendigste for illustrasjonsformål, vi kunne skrive dette:

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 nyeModelsuperclass håndterer manuelt slik at objekter kan lytte tilgetogsetmetoder på en modell. Hvis de blir kalt, blir» lyttere «varslet via.notifyAll– metoden. Vi bruker det til å haPersonViewlytt til sinPersonog kaller sin egen.redrawmetode når en egenskap er angitt via.setmetode.

så vi kan skrive:

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

Og vi trenger Ikke å ringecurrentUserView.redraw(), fordi meldingen innebygd.setgjør det for oss.

Vi kan gjøre andre ting med.get og.set, selvfølgelig. Nå som de er metoder, kan vi dekorere dem med logging eller validering hvis vi velger. Metoder gjør vår kode fleksibel og åpen for forlengelse. For eksempel kan VI bruke EN ES.senere dekoratør å legge logging råd til .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')}`; }};

mens vi Ikke kan gjøre noe sånt med direkte eiendomstilgang. Formidling av eiendomstilgang med metoder er mer fleksibel enn direkte tilgang til eiendommer, og dette gjør at vi kan organisere vårt program og distribuere ansvar på riktig måte.

Merk: ALLE ES.senere klasse dekoratorer kan brukes i vanilje ES 6 kode som vanlige funksjoner. I stedet for @after(LogSetter, 'set') class Person extends Model {...}, skriv bare const Person = after(LogSetter, 'set')(class Person extends Model {...})

Teknikker

getters og setters i javascript

problemet med getters og settere var godt forstått, og forvalterne bak javascript ‘ s evolusjon reagerte ved å introdusere en spesiell måte å slå direkte eiendomstilgang til en slags metode. Slik skriver vi vår Person klasse ved hjelp av «getters» og » settere:»

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 forord en metode med søkeordet get, definerer vi en getter, en metode som vil bli kalt når koden forsøker å lese fra egenskapen. Og når vi forord en metode med set, definerer vi en setter, en metode som vil bli kalt når kode forsøker å skrive til eiendommen.Getters og settere trenger ikke faktisk lese eller skrive noen egenskaper, de kan gjøre noe. Men i dette essayet snakker vi om å bruke dem til å formidle eiendomstilgang. Med getters og settere kan vi skrive:

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

og alt fungerer fortsatt akkurat som om vi hadde skrevetcurrentUser.set('first', 'Ragnvald')med.set-stilkode.

Getters og settere tillater oss å ha semantikken til å bruke metoder, men syntaksen for direkte tilgang.

Keypunch

en etterkombinator som kan håndtere getters og settere

Getters og settere synes ved første øyekast å være en magisk kombinasjon av kjent syntaks og kraftig evne til meta-program. En getter eller setter er imidlertid ikke en metode i vanlig forstand. Så vi kan ikke dekorere en setter med nøyaktig samme kode som vi ville bruke til å dekorere en vanlig metode.

med .set – metoden kan vi få direkte tilgang til Model.prototype.set og pakke den inn i en annen funksjon. Det er slik våre dekoratører jobber. Men det er ingen Person.prototype.first metode. I stedet er det en egenskapsbeskrivelse vi bare kan introspect ved hjelp av Object.getOwnPropertyDescriptor() og oppdatere ved hjelp av Object.defineProperty().

av denne grunn vil naï after dekoratør gitt ovenfor ikke fungere for getters og settere.2 Vi må bruke en slags dekoratør for metoder, en annen for getters, og en tredje for settere. Det høres ikke ut som moro, så la oss endre vårafter combinator slik at du kan bruke en enkelt funksjon med metoder, getters og settere:

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; }

Nå kan vi skrive:

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 nå koblet koden for å varsle lyttere fra koden for å få og sette verdier. Hvilket provoserer et enkelt spørsmål: hvis koden som sporer lyttere allerede er koblet fra i Model, hvorfor skal ikke koden for å utløse varsler være i samme enhet?

det er noen måter å gjøre det på. Vi bruker en universell mixin i stedet for å fylle den logikken i en superklasse:

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); } }});

dette tillater oss å skrive:

@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}`; }};

Hva har vi gjort? Vi har innlemmet getters og settere i koden vår, samtidig som vi opprettholder muligheten til å dekorere dem med ekstra funksjonalitet som om de var vanlige metoder.

det er en seier for rotne kode. Og det peker på noe å tenke på: når alt du har er metoder, oppfordres du til å lage tunge superklasser. Derfor tvinger så mange rammer deg til å utvide sine spesielle grunnklasser som Modeleller View.

Men når du finner en måte å bruke mixins og dekorere metoder, kan du dekomponere ting i mindre stykker og bruke dem der de trengs. Dette fører i retning av å bruke samlinger av biblioteker i stedet for et tungt rammeverk.

sammendrag

Getters og setters tillater oss å opprettholde den eldre stilen med å skrive kode som ser ut til å få direkte tilgang til egenskaper, mens vi faktisk formidler den tilgangen med metoder. Med forsiktighet kan vi oppdatere vårt verktøy for å tillate oss å dekorere våre getters og settere, distribuere ansvar som vi ønsker og frigjøre oss fra avhengighet av tungvektsbaseklasser.

(diskuter På Hacker News)

Post Scriptum

dette innlegget bruker å lytte til eiendomssettere som en unnskyldning for å diskutere getter og setter mekanismer, og måter å dekorere dem slik at vi kan organisere kode rundt bekymringer.

selvfølgelig, spre endringer gjennom eksplisitt varsling er ikke den eneste måten å organisere kode som må håndtere avhengigheter på data endring. Det er utenfor omfanget av dette innlegget for å diskutere de mange alternativene, men leserne har foreslått å utforske Objekt.observere og arbeide med uforanderlige data.

Flygende tallerkener for alle

en ting til

Rubyister spotter på:

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

Rubyister ville bruke den innebygde klassemetoden attr_accessor for å skrive dem for oss. Så bare for spark, skriver vi en dekoratør som skriver getters og settere. Råverdiene vil bli lagret i en attributes kart:

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; }}

attrAccessortar en liste over egenskapsnavn og returnerer en dekoratør for en klasse. Den skriver en vanlig getter-eller setter-funksjon for hver egenskap, og alle egenskapene som er definert, lagres i.attributeshash. Dette er veldig praktisk for serialisering eller andre persistansmekanismer.

(det er trivielt å også lageattrReader ogattrWriter funksjoner ved hjelp av denne malen. Vi trenger bare å utelate set når du skriver attrReader og utelateget når du skriverattrWriter.)

  1. DET var også et språk som heter BCPL, og andre før det, men vår historie må starte et sted, og det starter Med C. ↩

  2. Verken Vilmixin oppskrift vi har utviklet seg i tidligere innlegg Som Å Bruke ES.Senere Dekoratører Som Mixins. Det kan forbedres for å legge til et spesielt tilfelle for getters, settere og andre bekymringer som å jobbe med POJOs. For Eksempel, Andrea Giammarchi Er Universell Mixin. ↩

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert.