Getters, Setters, and Organizing Responsibility in JavaScript

Olipa kerran kieli nimeltä C, 1 ja tällä kielellä oli jotain nimeltään struct, ja sen avulla saattoi tehdä heterogeenisesti aggregoituja tietorakenteita, joilla oli jäseniä. Olennaista C: stä on se, että kun on strukturoitu currentUser, ja jäsen kuten id, ja kirjoitetaan jotain currentUser.id = 42, C-komppaaja muutti tämän äärimmäisen nopeiksi assembler-ohjeiksi. Sama int id = currentUser.id.

tärkeää oli myös se, että struktuureissa saattoi olla osoittimia funktioille, joten saattoi kirjoittaa esimerkiksi currentUser->setId(42) jos halusi mieluummin tehdä asetuksesta id funktion, ja tämä käännettiin myös nopeaksi assembleriksi.

ja lopuksi, C-ohjelmoinnissa on hyvin vahva kulttuuri suosia ”äärimmäisen nopeaa” vain ”nopeaa”, ja siten jos halusi C-ohjelmoijan huomion, piti varmistaa, ettei koskaan tee jotain, joka on vain nopeaa, kun voi tehdä jotain, joka on äärimmäisen nopeaa. Tämä on tietysti yleistys. Olen varma, että jos kyselemme, tapaamme lopulta molemmat C-ohjelmoijat, jotka suosivat elegantteja abstraktioita äärimmäisen nopean koodin sijaan.

Flathead Dragster

java ja javascript

sitten oli Java-niminen kieli, joka oli suunniteltu toimimaan selaimissa ja olemaan kannettavissa kaikenlaisissa laitteistoissa ja käyttöjärjestelmissä, ja yksi sen tavoitteista oli saada C-ohjelmoijat kirjoittamaan Java-koodia selaimessa pluginissa asuvan C: n kirjoittamisen sijaan. Tai pikemminkin, se oli yksi sen strategioista, tavoitteena oli Sun Microsystems pysyä relevanttina maailmassa, että Microsoft oli commoditizing, mutta se on toinen luku historiankirjan.

joten mukavat ihmiset Javan takana antoivat sille C: n kaltaisen syntaksin, jossa on henkselit ja lauseke / lauseke kahtiajako ja piste-notaatio. Niissä on struktien sijaan ”objekteja”, ja esineissä on paljon enemmän tekeillä kuin strukteissa, mutta Javan suunnittelijat tekivät eron currentUser.id = 42 ja currentUser.setId(42) ja varmistivat, että toinen oli äärimmäisen nopea ja toinen vain nopea. Tai pikemminkin, että toinen oli nopea, ja toinen oli ihan ok verrattuna C: hen, mutta C-ohjelmoijat saattoivat kokea tekevänsä tärkeää ajattelua päättäessään, pitäisikö id käyttää suoraan suorituskykyä tai epäsuorasti eleganssia ja joustavuutta.

historia on osoittanut, että tämä oli oikea tapa myydä uutta kieltä. Historia on myös osoittanut, että varsinaisella suorituserolla ei ollut merkitystä lähes kaikille. Suorituskyky on vain nyt, koodin joustavuus on ikuisesti.

no, kävi ilmi, että Sun oli oikeassa saadakseen C-ohjelmoijat käyttämään Javaa (se toimi minuun, hylkäsin Codewarriorin ja Lightspeed C: n), mutta väärässä Javan käytöstä selaimissa. Sen sijaan ihmiset alkoivat käyttää toista kieltä nimeltä JavaScript koodin kirjoittamiseen selaimissa, ja käyttivät Javaa koodin kirjoittamiseen palvelimilla.

yllättääkö tieto, että JavaScript oli myös suunniteltu saamaan C-ohjelmoijat kirjoittamaan koodia? Ja että se meni C: n kaltaisen syntaksin kanssa kihara olkaimet, lauseke/lauseke kahtiajako, ja piste merkintä? Ja vaikka JavaScript on tavallaan kuin Java-objekti ja kinda-sorta kuin Smalltalk-sanakirja, yllättääkö se, että Javascriptillä on myös ero currentUser.id = 42 ja currentUser.setId(42)? Ja että alun perin, yksi oli hidas, ja toinen koira-hidas, mutta ohjelmoijat voisivat tehdä tärkeää ajattelua milloin optimoida suorituskykyä ja milloin antaa hoot noin ohjelmoija mielenterveys?

ei yllätä, että se toimii tavallaan C: n tapaan kuin Java kinda-sort toimii C: n tavoin, ja täsmälleen samasta syystä. Eikä syyllä ole enää väliä.

professori Frink Java

suoran pääsyn ongelma

hyvin pian sen jälkeen, kun ihmiset alkoivat työskennellä Javan kanssa mittakaavalla, he saivat tietää, että instanssimuuttujien suora käyttö oli kauhea idea. JIT: n kääntäjät kaventivat currentUser.id = 42 ja currentUser.setId(42) lähes mihinkään, millä oli merkitystä kenellekään, ja koodi currentUser.id = 42 tai int id = currentUser.id oli huomattavan joustamaton.

tällaisia toimia ei ollut mahdollista koristella monialaisilla asioilla, kuten kirjaamisella tai validoinnilla. Et voinut ohittaa asetuskäyttäytymistä tai saada id aliluokassa. (Java-ohjelmoijat rakastavat alaluokkia!)

samaan aikaan JavaScript-ohjelmoijat kirjoittivat myös currentUser.id = 42, ja lopulta hekin huomasivat, että tämä oli kamala idea. Yksi muutoksen katalysaattoreista oli asiakaspuolen JavaScript-sovellusten kehysten tulo. Sanotaan, että meillä on naurettavan yksinkertainen henkilöluokka:

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

ja yhtä naurettava näkemys:

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

aina kun päivitämme henkilöluokkaa, on muistettava piirrä näkymä uudelleen:

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

miksi tällä on merkitystä?

no, jos ei pysty kontrolloimaan, missä tietyt vastuut hoidetaan, ei voi oikein järjestää ohjelmaansa. Alaluokat, menetelmät, mixinit ja sisustajat ovat tekniikoita: niiden avulla on mahdollista valita, mikä koodi vastaa mistäkin toiminnallisuudesta.

ja se on ohjelmoinnin koko juttu: toiminnallisuuden järjestäminen. Direct access ei salli sinun järjestää toimintoja, jotka liittyvät ominaisuuksien saamiseen ja asettamiseen, se pakottaa koodin, joka tekee saamisen ja asettamisen, myös vastaamaan kaikesta muusta, mikä liittyy saamiseen ja asettamiseen.

magneettinen Ydinmuisti

get and set

ei kestänyt kauaa, kun JavaScript-kirjaston tekijät keksivät, miten tämä saadaan katoamaan käyttämällä get ja set – menetelmää. Riisuttu alas paljaaseen olennaiseen havainnollistamiseksi, voisimme kirjoittaa tämän:

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

Uusi Modelsuperluokka hallitsee manuaalisesti sallien olioiden kuunnellagetjasetmenetelmät mallin mukaan. Jos niitä kutsutaan, ”kuuntelijoille” ilmoitetaan .notifyAll – menetelmällä. Käytämme sitä PersonView kuunnella sen Person ja kutsua omaa .redraw – menetelmää, kun ominaisuus asetetaan .set – menetelmää.

niin voimme kirjoittaa:

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

eikä meidän tarvitse soittaa currentUserView.redraw(), koska .set tehty ilmoitus tekee sen puolestamme.

voimme toki tehdä muutakin .get ja .set. Nyt kun ne ovat menetelmiä, voimme koristella ne kirjaamisella tai validoinnilla, jos haluamme. Menetelmät tekevät koodistamme joustavan ja laajennettavissa olevan. Esimerkiksi, Voimme käyttää ES.myöhempi koristelija lisää kirjausneuvoja .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')}`; }};

kun taas suoralla kiinteistöllä ei voi tehdä mitään vastaavaa. Kiinteistöjen käyttöoikeuksien välittäminen menetelmin on joustavampaa kuin suoraan kiinteistöihin pääsy, ja näin voimme järjestää ohjelmamme ja jakaa vastuuta oikein.

Huom: Kaikki ES.myöhemmät luokan koristelut voidaan käyttää vanilla ES 6-koodissa tavallisina funktioina. Sijasta @after(LogSetter, 'set') class Person extends Model {...}, yksinkertaisesti kirjoittaa const Person = after(LogSetter, 'set')(class Person extends Model {...})

tekniikoita

getterit ja setterit javascriptissä

gettereiden ja settereiden ongelma ymmärrettiin hyvin, ja JavaScriptin evoluution takana olleet stuertit vastasivat ottamalla käyttöön erityisen tavan muuttaa suora kiinteistökäyttö eräänlaiseksi menetelmäksi. Näin kirjoittaisimme Person luokan käyttäen ”gettereita” ja ” settereitä:”

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

kun esitämme metodin hakusanalla get, määrittelemme getterin, menetelmän, jota kutsutaan, kun koodi yrittää lukea ominaisuudesta. Ja kun esitämme menetelmän set, määrittelemme setterin, menetelmän, jota kutsutaan, kun koodi yrittää kirjoittaa ominaisuuteen.

Gettereiden ja settereiden ei tarvitse varsinaisesti lukea tai kirjoittaa mitään ominaisuuksia, he voivat tehdä mitä tahansa. Mutta tässä esseessä, puhumme niiden käyttämisestä välittämään omaisuuden pääsyä. Gettereilla ja settereillä voidaan kirjoittaa:

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

ja kaikki toimii edelleen aivan kuin olisimme kirjoittaneetcurrentUser.set('first', 'Ragnvald').set -tyylisellä koodilla.

Getterit ja setterit mahdollistavat menetelmien käytön semantiikan, mutta suoran pääsyn syntaksin.

Keypunch

after combinator, joka pystyy käsittelemään gettereita ja settereitä

Getterit ja setterit vaikuttavat ensisilmäyksellä maagiselta yhdistelmältä tutusta syntaksista ja tehokkaasta kyvystä metaohjelmoida. Kuitenkin, getter tai setter ei ole menetelmä tavanomaisessa merkityksessä. Setteriä ei voi sisustaa samalla koodilla kuin tavallista.

.set – menetelmällä voitiin suoraan käyttää Model.prototype.set ja kääriä se toiseen funktioon. Niin sisustajamme toimivat. Person.prototype.first – menetelmää ei kuitenkaan ole olemassa. Sen sijaan on olemassa ominaisuuskuvaus, jota voimme tarkastella vain käyttämällä Object.getOwnPropertyDescriptor() ja päivittää käyttämällä Object.defineProperty().

tästä syystä edellä annettu naiivi after koristelija ei toimi gettereille ja settereille.2 meidän olisi käytettävä yhdenlaista sisustaja menetelmiä, toinen getters, ja kolmas setters. Tuo ei kuulosta hauskalta, joten muokataan after combinator niin, että voidaan käyttää yhtä funktiota metodeilla, gettereillä ja settereillä:

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

nyt voidaan kirjoittaa:

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

olemme nyt irrottaneet kuuntelijoille ilmoittamista koskevan koodin arvojen saamista ja asettamista koskevasta koodista. Mikä herättää yksinkertaisen kysymyksen: Jos kuuntelijoita seuraava koodi on jo irrotettu tuotannostaModel, miksei ilmoitusten laukaisukoodi olisi samassa kokonaisuudessa?

siihen on muutama keino. Käytämme universaalia mixiniä sen sijaan, että sulloisimme tuon logiikan superluokkaan:

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

näin voimme kirjoittaa:

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

mitä olemme tehneet? Olemme sisällyttäneet getters ja setters meidän koodi, säilyttäen kyky koristella niitä lisätoimintoja ikään kuin ne olisivat tavallisia menetelmiä.

se on hajoavan koodin voitto. Ja se viittaa ajateltavaan asiaan: kun on vain metodeja, kannustetaan tekemään raskaansarjan superliigoja. Siksi niin monet kehykset pakottavat laajentamaan erikoisperusluokkia, kuten Model tai View.

mutta kun keksit tavan käyttää mixinsejä ja koristelutapoja, voit hajottaa tavarat pienemmiksi paloiksi ja levittää ne sinne, missä niitä tarvitaan. Tämä johtaa siihen, että kirjastokokoelmia käytetään raskaan kehyksen sijaan.

tiivistelmä

Getterit ja setterit antavat meille mahdollisuuden säilyttää perinteisen kirjoitustyylin, joka näyttää pääsevän suoraan ominaisuuksiin, samalla kun todellisuudessa välitetään tätä pääsyä menetelmillä. Huolellisesti, voimme päivittää työkaluja, jotta voimme koristella meidän getters ja setters, jakaa vastuuta kuin parhaaksi näemme ja vapauttaa meidät riippuvuudesta raskaan perus luokat.

(keskustele Hacker News)

Post Scriptum

tämä viesti käyttää kuuntelemalla omaisuuden setterit tekosyynä keskustella getter ja setter mekanismit, ja tapoja koristella niitä, jotta voimme järjestää koodin noin huolenaiheita.

muutosten lisääminen eksplisiittisellä ilmoituksella ei tietenkään ole ainoa tapa järjestää koodia, joka tarvitsee hallita riippuvuuksia tiedon muuttumisesta. Se on soveltamisalan ulkopuolella tämän viestin keskustella monista vaihtoehdoista, mutta lukijat ovat ehdottaneet tutkia objekti.tarkkaile ja työskentele muuttumattomien tietojen kanssa.

Flying saucers for everyone

one more thing

Rubyistit pilkkaavat:

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

Rubyistit käyttäisivät sisäänrakennettua luokkamenetelmää attr_accessor kirjoittaa ne meille. Huvin vuoksi kirjoitamme sisustajan, joka kirjoittaa gettereita ja settereitä. Raw-arvot tallennetaan attributes map:

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

nyt voidaan kirjoittaa:

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

attrAccessortakes a luettelo kiinteistöjen nimet ja palauttaa sisustaja luokan. Se kirjoittaa jokaiselle ominaisuudelle tavallisen getter-tai setter-funktion, ja kaikki määritellyt ominaisuudet tallennetaan .attributes hash. Tämä on erittäin kätevä sarjallistamiseen tai muihin persistance-mekanismeihin.

(on triviaalia tehdä myös attrReader ja attrWriter funktiot tätä mallia käyttäen. Täytyy vain jättää set kirjoitettaessa attrReader ja jättää get kirjoitettaessa attrWriter.)

  1. oli myös kieli nimeltä BCPL, ja muut sitä ennen, mutta meidän tarinamme on aloitettava jostain, ja se alkaa C. ↩

  2. ei myöskään mixin resepti olemme kehittyneet aiemmissa viesteissä kuten ES: n avulla.myöhemmät sisustajat Mixineinä. Sitä voidaan parantaa lisäämällä erikoistapaus getters, setters, ja muita huolenaiheita, kuten työskentely POJOs. Esimerkiksi Andrea Giammarchin Universal Mixin. ↩

Vastaa

Sähköpostiosoitettasi ei julkaista.