Getters, Setters, și responsabilitatea de organizare în JavaScript

după ce, la un moment dat,a existat un limbaj numit C, 1 și acest limbaj a avut ceva numitstruct, și ai putea folosi pentru a face structuri de date agregate eterogen care au avut membri. Lucrul cheie de știut despre C este că atunci când aveți un struct numit currentUser și un membru ca id și scrieți ceva de genul currentUser.id = 42, c complier a transformat acest lucru în instrucțiuni de asamblare extrem de rapide. La fel pentru int id = currentUser.id.

de asemenea, de importanță a fost că ai putea avea indicii pentru funcții în structs, astfel încât ai putea scrie lucruri cacurrentUser->setId(42) dacă ați preferat să facă setarea unuiid o funcție, și acest lucru a fost, de asemenea, tradus în fast assembler.

și în cele din urmă, programarea C are o cultură foarte puternică de a prefera „extrem de rapid” decât „rapid” și, prin urmare, dacă doriți atenția unui programator C, trebuie să vă asigurați că nu faceți niciodată ceva care este doar rapid atunci când puteți face ceva extrem de rapid. Aceasta este o generalizare, desigur. Sunt sigur că, dacă întrebăm în jur, vom întâlni în cele din urmă ambii programatori C care preferă abstracții elegante decât codul extrem de rapid.

Flathead Dragster

java și javascript

apoi a existat un limbaj numit Java, și a fost proiectat pentru a rula în browsere, și să fie portabil pe tot felul de hardware și sisteme de operare, și unul dintre obiectivele sale a fost de a obține C programatori pentru a scrie cod Java în browser-ul în loc de a scrie C, care a trăit într-un plugin. Sau, mai degrabă, aceasta a fost una dintre strategiile sale, scopul a fost ca Sun Microsystems să rămână relevant într-o lume pe care Microsoft o comercializa, dar acesta este un alt capitol al cărții de istorie.

deci, oamenii drăguți din spatele Java i-au dat sintaxa c-like cu bretele și dihotomia enunțului/expresiei și notația dot. Au „obiecte”în loc de structuri, iar obiectele se întâmplă mult mai mult decât structuri, dar designerii Java au făcut o distincție între currentUser.id = 42 și currentUser.setId(42) și s-au asigurat că unul a fost extrem de rapid, iar celălalt a fost doar rapid. Sau, mai degrabă, că unul a fost rapid, iar celălalt a fost doar ok în comparație cu C, dar programatorii C ar putea simți că fac gândire importantă atunci când decid dacă id ar trebui să fie accesate direct pentru performanță sau accesate indirect pentru eleganță și flexibilitate.

istoria a arătat că acesta a fost modul corect de a vinde o nouă limbă. Istoria a arătat, de asemenea, că distincția reală de performanță a fost irelevantă pentru aproape toată lumea. Performanța este doar pentru moment, flexibilitatea codului este pentru totdeauna.Ei bine, sa dovedit că Sun a avut dreptate despre obtinerea C programatori pentru a utiliza Java (a lucrat pe mine, am renuntat CodeWarrior și Lightspeed C), dar greșit despre utilizarea Java în browsere. În schimb, oamenii au început să folosească o altă limbă numită JavaScript pentru a scrie cod în browsere și au folosit Java pentru a scrie cod pe servere.

te va surprinde să afli că JavaScript a fost conceput și pentru a determina programatorii C să scrie cod? Și că a mers cu sintaxa de tip C cu bretele curbate, dihotomia enunțului/expresiei și notația punctelor? Și, deși JavaScript are un lucru care este cam-sorta ca un obiect Java, și cam-sorta ca un dicționar Smalltalk, va surprinde să învețe că JavaScript are, de asemenea, o distincție între currentUser.id = 42 și currentUser.setId(42)? Și că inițial, unul a fost lent, iar celălalt câine-lent, dar programatorii ar putea face gândire importantă despre când să optimizeze pentru performanță și când să dea o huiduială despre bun-simț programator?

Nu, nu vă va surprinde să aflați că funcționează cam-sorta ca C în același mod în care Java kinda-sort funcționează ca C și din exact același motiv. Și motivul nu mai contează.

profesorul Frink pe Java

problema cu acces direct

foarte curând după ce oamenii au început să lucreze cu Java la scară, au aflat că accesarea directă a variabilelor de instanță a fost o idee teribilă. Compilatoarele JIT au redus diferența de performanță dintre currentUser.id = 42 și currentUser.setId(42) la aproape nimic relevant pentru nimeni, iar codul folosind currentUser.id = 42 sau int id = currentUser.id a fost remarcabil de inflexibil.

nu a existat nicio modalitate de a decora astfel de operațiuni cu preocupări transversale, cum ar fi logarea sau validarea. Nu puteți suprascrie comportamentul setării sau obținerii unui id într-o subclasă. (Programatorii Java iubesc subclasele!)

între timp, programatorii JavaScript scriau șicurrentUser.id = 42 și, în cele din urmă, și ei au descoperit că aceasta a fost o idee teribilă. Unul dintre catalizatorii schimbării a fost sosirea cadrelor pentru aplicațiile JavaScript din partea clientului. Să presupunem că avem o clasă de persoane ridicol de simplă:

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

și o viziune la fel de ridicolă:

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

de fiecare dată când actualizăm clasa de persoane, trebuie să ne amintim să Redesenați vederea:

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

De ce contează asta?Ei bine, dacă nu puteți controla unde sunt gestionate anumite responsabilități, nu vă puteți organiza cu adevărat programul. Subclasele, metodele, mixinele și decoratorii sunt tehnici: ceea ce fac posibil este alegerea codului care este responsabil pentru ce funcționalitate.

și asta e totul despre programare: organizarea funcționalității. Accesul Direct nu vă permite să organizați funcționalitatea asociată cu obținerea și setarea proprietăților, forțează codul care face obținerea și setarea să fie, de asemenea, responsabil pentru orice altceva asociat cu obținerea și setarea.

memorie cu miez Magnetic

obțineți și setați

nu a durat mult timp ca autorii Bibliotecii JavaScript să-și dea seama cum să facă acest lucru să dispară folosind o metodă get și set. Dezbrăcat până la elementele esențiale goale în scopuri ilustrative, am putea scrie acest lucru:

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

noul nostruModel superclasa gestionează manual permite obiectelor să asculteget șiset metode pe un model. Dacă sunt chemați,” ascultătorii”sunt notificați prin metoda .notifyAll. Folosim asta pentru a aveaPersonView ascultaPerson si apeleaza la propria metoda.redraw atunci cand o proprietate este setata prin metoda.set.

ca să putem scrie:

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

și nu trebuie să apelămcurrentUserView.redraw(), deoarece notificarea încorporată în.set o face pentru noi.

putem face alte lucruri cu.get și.set, desigur. Acum că sunt metode, le putem decora cu logare sau validare dacă alegem. Metodele fac codul nostru flexibil și deschis la extensie. De exemplu, putem folosi un ES.mai târziu decorator pentru a adăuga sfaturi de logare la .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')}`; }};

în timp ce nu putem face așa ceva cu acces direct la proprietate. Medierea accesului la proprietăți cu metode este mai flexibilă decât accesarea directă a proprietăților, iar acest lucru ne permite să ne organizăm programul și să distribuim responsabilitatea în mod corespunzător.

Notă: Toate ES.decoratorii de clasă ulterioară pot fi utilizați în codul vanilla ES 6 ca funcții obișnuite. În loc de @after(LogSetter, 'set') class Person extends Model {...}, pur și simplu scrie const Person = after(LogSetter, 'set')(class Person extends Model {...})

tehnici

getters și setteri în javascript

problema cu getters și setters a fost bine înțeleasă, iar administratorii din spatele evoluției javascript au răspuns prin introducerea unui mod special de a transforma accesul direct la proprietate într-un fel de metodă. Iată cum am scrie Person clasa noastră folosind „getters” și ” 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}`; }};

când prefațăm o metodă cu cuvântul cheieget, definim un getter, o metodă care va fi apelată atunci când codul încearcă să citească din proprietate. Și când prefațăm o metodă cu set, definim un setter, o metodă care va fi apelată atunci când codul încearcă să scrie în proprietate.

Getters și setters nu au nevoie de fapt citi sau scrie orice proprietăți, ei pot face orice. Dar în acest eseu, vom vorbi despre utilizarea lor pentru a Media accesul la proprietate. Cu getters și setters, putem scrie:

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

și totul încă funcționează la fel ca și cum am fi scris currentUser.set('first', 'Ragnvald') cu .set-cod de stil.

Getters și setters ne permit să avem semantica utilizării metodelor, dar sintaxa accesului direct.

Keypunch

un combinator după care se pot ocupa getters și setters

Getters și setters par la prima vedere a fi o combinație magică de sintaxă familiară și capacitatea de puternic pentru a meta-program. Cu toate acestea, un getter sau setter nu este o metodă în sensul obișnuit. Deci, nu putem decora un setter folosind exact același cod pe care l-am folosi pentru a decora o metodă obișnuită.

cu.set metoda, am putea accesa directModel.prototype.set și înfășurați-l într-o altă funcție. Așa funcționează decoratorii noștri. Dar nu există o metodăPerson.prototype.first. În schimb, există un descriptor de proprietate putem doar introspecție folosindObject.getOwnPropertyDescriptor() și actualizare folosindObject.defineProperty().

Din acest motiv, na oktivveafter decorator dat mai sus nu va funcționa pentru getters și setters.2 ar trebui să folosim un fel de decorator pentru metode, altul pentru getters și un al treilea pentru setteri. Asta nu sună distractiv, așa că haideți să modificămafter combinator astfel încât să puteți utiliza o singură funcție cu metode, getters și setteri:

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

acum putem scrie:

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

acum am decuplat codul pentru notificarea ascultătorilor de codul pentru obținerea și setarea valorilor. Ceea ce provoacă o întrebare simplă: dacă codul care urmărește ascultătorii este deja decuplat în Model, de ce codul pentru declanșarea notificărilor nu ar trebui să fie în aceeași entitate?

există câteva modalități de a face acest lucru. Vom folosi un mixin universal în loc să umplem acea logică într-o superclasă:

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

Acest lucru ne permite să scriem:

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

ce am făcut? Am încorporat getters și setteri în codul nostru, menținând în același timp capacitatea de a le decora cu funcționalitate adăugată ca și cum ar fi metode obișnuite.

asta e o victorie pentru descompunerea codului. Și indică ceva la care să te gândești: când tot ce ai sunt metode, ești încurajat să faci superclase la categoria grea. De aceea, atât de multe cadre vă obligă să extindeți clasele de bază cu scop special, cum ar fi Model sau View.

dar când găsești o modalitate de a folosi mixine și de a decora metode, poți descompune lucrurile în bucăți mai mici și le poți aplica acolo unde sunt necesare. Acest lucru duce în direcția utilizării colecțiilor de biblioteci în locul unui cadru greu.

rezumat

Getters și setters ne permit să menținem stilul vechi de scriere a codului care pare să acceseze direct proprietățile, în timp ce mediază efectiv acel acces cu metode. Cu grijă, ne putem actualiza instrumentele pentru a ne permite să ne decorăm getterii și setterii, distribuind responsabilitatea după cum considerăm de cuviință și eliberându-ne de dependența de clasele de bază grele.

(discutați pe Hacker News)

Post Scriptum

această postare folosește ascultarea setatorilor de proprietăți ca scuză pentru a discuta mecanismele getter și setter și modalități de a le decora, astfel încât să putem organiza codul în jurul preocupărilor.desigur, propagarea modificărilor prin notificare explicită nu este singura modalitate de a organiza codul care trebuie să gestioneze dependențele de schimbarea datelor. Este dincolo de sfera de aplicare a acestui post pentru a discuta multe alternative, dar cititorii au sugerat explorarea obiect.observați și lucrați cu date imuabile.

farfurii zburătoare pentru toată lumea

încă un lucru

Rubyiștii își bat joc de:

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

Rubyiștii ar folosi metoda clasei încorporate attr_accessor pentru a le scrie pentru noi. Deci, doar pentru lovituri, vom scrie un decorator care scrie getters și setters. Valorile brute vor fi stocate într-o attributes hartă:

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

acum putem scrie:

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

attrAccessorlista de nume de proprietate și returnează un decorator pentru o clasă. Se scrie un simplu getter sau funcția setter pentru fiecare proprietate, și toate proprietățile definite sunt stocate în .attributes hash. Acest lucru este foarte convenabil pentru serializare sau alte mecanisme de persistență.

(este banal de a face, de asemenea,attrReader șiattrWriter funcții folosind acest șablon. Trebuie doar să omitem set când scriem attrReader și omitem get când scriem attrWriter.)

  1. a existat și un limbaj numit BCPL, și altele înainte de asta, dar povestea noastră trebuie să înceapă undeva și începe cu C.

  2. nicimixin rețeta pe care am evoluat-o în postările anterioare, cum ar fi utilizarea ES.mai târziu Decoratori ca Mixins. Acesta poate fi îmbunătățită pentru a adăuga un caz special pentru getters, setteri, și alte preocupări, cum ar fi lucrul cu POJOs. De exemplu, Mixinul universal al lui Andrea Giammarchi.

Lasă un răspuns

Adresa ta de email nu va fi publicată.