der var engang et sprog kaldet C, 1,og dette sprog havde noget kaldet a struct
, og du kunne bruge det til at lave heterogent aggregerede datastrukturer, der havde medlemmer. Den vigtigste ting at vide om C er, at når du har en struktur kaldet currentUser
, og et medlem som id
, og du skriver noget som currentUser.id = 42
, forvandlede C-complieren dette til ekstremt hurtige monteringsinstruktioner. Samme for int id = currentUser.id
.
også af betydning var, at du kunne have henvisninger til funktioner i structs, så du kunne skrive ting som currentUser->setId(42)
hvis du foretrak at indstille en id
en funktion, og dette blev også oversat til fast assembler.
og endelig har C-Programmering en meget stærk kultur med at foretrække “ekstremt hurtig” frem for bare “hurtig”, og hvis du ville have en C-programmørs opmærksomhed, måtte du sørge for, at du aldrig gør noget, der bare er hurtigt, når du kunne gøre noget, der er ekstremt hurtigt. Det er selvfølgelig en generalisering. Jeg er sikker på, at hvis vi spørger rundt, møder vi til sidst begge C-programmører, der foretrækker elegante abstraktioner frem for ekstremt hurtig kode.
java og javascript
så var der et sprog kaldet Java, og det var designet til at køre i Bro.Serere og være bærbart på tværs af alle slags udstyr og operativsystemer, og et af dets mål var at få C-programmører til at skrive Java-kode i Bro. sereren i stedet for at skrive C, der boede i et plugin. Eller rettere, det var en af dens strategier, målet var, at Sun Microsystems skulle forblive relevant i en verden, som Microsoft var commoditiserende, men det er et andet kapitel i historiebogen.
så de hyggelige mennesker bag Java gav det C-lignende syntaks med bøjlerne og udsagnet/ekspressionsdikotomien og dot notationen. De har “objekter” i stedet for struct ‘er, og objekter har meget mere foregår end struct’ er, men Java ‘ s designere skelner mellem currentUser.id = 42
og currentUser.setId(42)
og sørgede for, at den ene var ekstremt hurtig og den anden var bare hurtig. Eller rettere, den ene var hurtig, og den anden var bare ok sammenlignet med C, men C-programmører kunne føle, at de gjorde vigtig tænkning, når de besluttede, om id
burde være direkte adgang til ydeevne eller indirekte adgang til elegance og fleksibilitet.
historien har vist, at dette var den rigtige måde at sælge et nyt sprog på. Historien har også vist, at den faktiske præstationsforskel var irrelevant for næsten alle. Ydeevne er kun for nu, kode fleksibilitet er for evigt.det viste sig, at Sun havde ret i at få C-programmører til at bruge Java (det virkede på mig, jeg droppede Kodekrig og Lightspeed C), men forkert ved at bruge Java i Bro.sere. I stedet begyndte folk at bruge et andet sprog kaldet JavaScript til at skrive kode i Bro.sere og brugte Java til at skrive kode på servere.
vil det overraske dig at lære, at JavaScript også var designet til at få C-programmører til at skrive kode? Og at det gik med den C-lignende syntaks med krøllede seler, udsagnet/udtrykket dikotomi og dot notation? Og selvom JavaScript har en ting, der er kinda-sorta som et Java-objekt, og kinda-sorta som en Smalltalk-ordbog, vil det overraske dig at lære, at JavaScript også skelner mellem currentUser.id = 42
og currentUser.setId(42)
? Og det oprindeligt, den ene var langsom, og den anden hund-langsom, men programmører kunne gøre vigtige tanker om, hvornår de skulle optimere til ydeevne, og hvornår de skulle give et hoot om programmør sanity?
Nej, det vil ikke overraske dig at lære at det virker kinda-sorta som C på samme måde som Java kinda-sort fungerer som C, og af nøjagtig samme grund. Og grunden betyder virkelig ikke noget mere.
problemet med direkte adgang
meget snart efter at folk begyndte at arbejde med Java i skala, lærte de, at direkte adgang til instansvariabler var en forfærdelig ide. JIT-kompilatorer indsnævrede ydelsesforskellen mellem currentUser.id = 42
og currentUser.setId(42)
til næsten intet af relevans for nogen, og kode ved hjælp af currentUser.id = 42
eller int id = currentUser.id
var bemærkelsesværdigt ufleksibel.
der var ingen måde at dekorere sådanne operationer med tværgående bekymringer som logning eller validering. Du kunne ikke tilsidesætte opførslen ved at indstille eller få et id
i en underklasse. (Java programmører elsker underklasser!)
I mellemtiden skrev JavaScript-programmører ogsåcurrentUser.id = 42
, og til sidst opdagede de også, at dette var en forfærdelig ide. En af katalysatorerne for forandring var ankomsten af rammer for JavaScript-applikationer på klientsiden. Lad os sige, at vi har en latterligt enkel personklasse:
class Person { constructor (first, last) { this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};
og en lige så latterlig visning:
hver gang vi opdaterer personklassen, er vi nødt til at huske at gentegne visningen:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUserView.redraw();currentUser.first = 'Ragnvald';currentUserView.redraw();
hvorfor betyder det noget?
Hvis du ikke kan kontrollere, hvor visse ansvarsområder håndteres, kan du ikke rigtig organisere dit program. Underklasser, metoder, blandinger og dekoratører er teknikker: hvad de muliggør er at vælge, hvilken kode der er ansvarlig for hvilken funktionalitet.
og det er det hele ved programmering: organisering af funktionaliteten. Direkte adgang tillader dig ikke at organisere den funktionalitet, der er forbundet med at få og indstille egenskaber, det tvinger koden til at få og indstille til også at være ansvarlig for alt andet, der er forbundet med at få og indstille.
Hent og indstil
det tog ikke lang tid for JavaScript-biblioteksforfattere at finde ud af, hvordan man får dette til at forsvinde ved hjælp af enget
ogset
metode. Strippet ned til det blotte væsentlige til illustrative formå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()) }}
vores nyeModel
superklasse styrer manuelt at tillade objekter at lytte tilget
ogset
metoder på en model. Hvis de kaldes, meddeles “lytterne”via .notifyAll
– metoden. Vi bruger det til at have PersonView
lyt til dens Person
og kalder sin egen .redraw
metode, når en ejendom er indstillet via .set
metode.
så vi kan skrive:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.set('first', 'Ragnvald');
og vi behøver ikke at ringe currentUserView.redraw()
, fordi meddelelsen indbygget i .set
gør det for os.
Vi kan gøre andre ting med .get
og .set
, selvfølgelig. Nu hvor de er metoder, kan vi dekorere dem med logning eller validering, hvis vi vælger. Metoder gør vores kode fleksibel og åben for udvidelse. For eksempel kan vi bruge en ES.senere dekoratør for at tilføje logningsrå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 gøre noget lignende med direkte ejendomsadgang. Formidling af ejendomsadgang med metoder er mere fleksibel end direkte adgang til ejendomme, og dette giver os mulighed for at organisere vores program og distribuere ansvaret korrekt.
Bemærk: Alle ES.senere klasse dekoratører kan bruges i vanille ES 6 kode som almindelige funktioner. I stedet for
@after(LogSetter, 'set') class Person extends Model {...}
skal du blot skriveconst Person = after(LogSetter, 'set')(class Person extends Model {...})
getters and setters in javascript
problemet med getters og setters var godt forstået, og forvalterne bag javascript ‘ s evolution reagerede ved at indføre en særlig måde at gøre direkte ejendomsadgang til en slags metode. Sådan skriver vi vores Person
klasse ved hjælp af “getters” og ” 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 forord en metode med nøgleordet get
, definerer vi en getter, en metode, der vil blive kaldt, når kode forsøger at læse fra ejendommen. Og når vi forord en metode med set
, definerer vi en setter, en metode, der vil blive kaldt, når kode forsøger at skrive til ejendommen.
Getters og settere behøver faktisk ikke læse eller skrive nogen egenskaber, de kan gøre noget. Men i dette essay vil vi tale om at bruge dem til at formidle ejendomsadgang. Med getters og settere kan vi skrive:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.first = 'Ragnvald';
og alt fungerer stadig som om vi havde skrevet currentUser.set('first', 'Ragnvald')
med .set
-style code.
Getters og settere tillader os at have semantikken ved at bruge metoder, men syntaksen for direkte adgang.
en efterkombinator, der kan håndtere getters og settere
Getters og settere synes ved første øjekast at være en magisk kombination af velkendt syntaks og kraftig evne til metaprogram. En getter eller setter er dog ikke en metode i den sædvanlige forstand. Så vi kan ikke dekorere en setter ved hjælp af den nøjagtige samme kode, som vi ville bruge til at dekorere en almindelig metode.
med .set
– metoden kunne vi direkte få adgang til Model.prototype.set
og pakke det ind i en anden funktion. Sådan arbejder vores dekoratører. Men der er ingenPerson.prototype.first
metode. I stedet er der en ejendomsbeskrivelse, som vi kun kan introspektere ved hjælp af Object.getOwnPropertyDescriptor()
og opdatere ved hjælp af Object.defineProperty()
.
af denne grund fungerer na-kristen after
dekoratør, der er angivet ovenfor, ikke for getters og settere.2 Vi bliver nødt til at bruge en slags dekoratør til metoder, en anden til getters og en tredje til settere. Det lyder ikke som sjovt, så lad os ændre vores after
combinator, så du kan bruge en enkelt funktion 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; }
nu 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 nu afkoblet koden for at underrette lyttere fra koden for at få og indstille værdier. Hvilket fremkalder et simpelt spørgsmål: hvis koden, der sporer lyttere, allerede er afkoblet i Model
, hvorfor skulle koden til udløsende meddelelser ikke være i samme enhed?
der er et par måder at gøre det på. Vi bruger en universel blanding i stedet for at fylde den logik 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 tillader os at 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}`; }};
Hvad har vi gjort? Vi har indarbejdet getters og setters i vores kode, samtidig med at vi opretholder evnen til at dekorere dem med ekstra funktionalitet, som om de var almindelige metoder.
det er en gevinst for nedbrydning af kode. Og det peger på noget at tænke på: når alt hvad du har er metoder, opfordres du til at lave tunge superklasser. Derfor tvinger så mange rammer dig til at udvide deres specielle basisklasser som Model
eller View
.
men når du finder en måde at bruge blandinger og dekorere metoder på, kan du nedbryde ting i mindre stykker og anvende dem, hvor de er nødvendige. Dette fører i retning af at bruge samlinger af biblioteker i stedet for en tungvægtsramme.
oversigt
Getters og settere giver os mulighed for at opretholde den gamle stil med at skrive kode, der ser ud til at få direkte adgang til egenskaber, mens vi faktisk formidler denne adgang med metoder. Med omhu kan vi opdatere vores værktøj, så vi kan dekorere vores getters og setters, distribuere ansvar som vi finder passende og frigøre os fra afhængighed af tungvægtsbaseklasser.
(diskuter om Hackernyheder)
Post Scriptum
dette indlæg bruger at lytte til ejendomssættere som en undskyldning for at diskutere getter-og setter-mekanismerne og måder at dekorere dem på, så vi kan organisere kode omkring bekymringer.
selvfølgelig er udbredelse af ændringer gennem eksplicit anmeldelse ikke den eneste måde at organisere kode på, der skal styre afhængigheder af dataændring. Det er uden for rammerne af dette indlæg at diskutere de mange alternativer, men læsere har foreslået at udforske objekt.observere og arbejde med uforanderlige data.
en ting mere
Rubyists scoff at:
Rubyists ville bruge den indbyggede klassemetode attr_accessor
at skrive dem til os. Så bare for spark, skriver vi en dekoratør, der skriver getters og setters. Råværdierne gemmes i etattributes
kort:
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 skrive:
@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
tager en liste over ejendomsnavne og returnerer en dekoratør for en klasse. Det skriver en almindelig getter-eller setter-funktion for hver egenskab, og alle de definerede egenskaber gemmes i .attributes
hash. Dette er meget praktisk til serialisering eller andre vedholdende mekanismer.
(det er trivielt at også laveattrReader
ogattrWriter
funktioner ved hjælp af denne skabelon. Vi skal bare udelade set
når du skriver attrReader
og udelade get
når du skriver attrWriter
.)
-
Der var også et sprog kaldet BCPL, og andre før det, men vores historie skal starte et eller andet sted, og det starter med C. Kris
-
heller ikke
mixin
opskrift vi har udviklet os i tidligere indlæg som at bruge ES.senere dekoratører som blandinger. Det kan forbedres for at tilføje et specielt tilfælde til getters, setters og andre bekymringer som at arbejde med POJOs. For eksempel Andrea Giammarchi ‘ s universelle blanding. Larsen