C’era una volta un linguaggio chiamato C, 1 e questo linguaggio aveva qualcosa chiamato struct
, e si poteva usare per creare strutture di dati aggregate in modo eterogeneo che avevano membri. La cosa fondamentale da sapere su C è che quando hai una struttura chiamata currentUser
, e un membro come id
, e scrivi qualcosa come currentUser.id = 42
, il complier C ha trasformato questo in istruzioni assembler estremamente veloci. Lo stesso vale per int id = currentUser.id
.
Anche importante era che si potevano avere puntatori alle funzioni nelle strutture, quindi si potevano scrivere cose comecurrentUser->setId(42)
se si preferiva rendere l’impostazione di unaid
una funzione, e questo è stato anche tradotto in assembler veloce.
E infine, la programmazione C ha una cultura molto forte di preferire “estremamente veloce” a solo “veloce”, e quindi se volevi l’attenzione di un programmatore C, dovevi assicurarti di non fare mai qualcosa che è solo veloce quando potevi fare qualcosa che è estremamente veloce. Questa è una generalizzazione, ovviamente. Sono sicuro che se chiediamo in giro, alla fine incontreremo entrambi i programmatori C che preferiscono astrazioni eleganti al codice estremamente veloce.
java e javascript
Poi c’era un linguaggio chiamato Java, ed è stato progettato per essere eseguito in un browser, e di essere portabile su tutti i tipi di hardware e sistemi operativi, e uno dei suoi obiettivi era quello di ottenere C ai programmatori di scrivere codice Java nel browser invece di scrivere C che ha vissuto in un plugin. O meglio, quella era una delle sue strategie, l’obiettivo era che Sun Microsystems rimanesse rilevante in un mondo che Microsoft stava mercificando, ma questo è un altro capitolo del libro di storia.
Quindi le persone simpatiche dietro Java gli hanno dato una sintassi simile a C con le parentesi graffe e la dicotomia istruzione / espressione e la notazione dot. Hanno “oggetti”invece di strutture e gli oggetti hanno molto più successo delle strutture, ma i progettisti di Java hanno fatto una distinzione tra currentUser.id = 42
e currentUser.setId(42)
, e si sono assicurati che uno fosse estremamente veloce e l’altro fosse solo veloce. O meglio, quello era veloce, e l’altro era solo ok rispetto a C, ma i programmatori C potevano sentirsi come se stessero facendo un pensiero importante al momento di decidere se id
dovrebbe essere accessibile direttamente per le prestazioni o indirettamente accessibile per eleganza e flessibilità.
La storia ha dimostrato che questo era il modo giusto per vendere una nuova lingua. La storia ha anche dimostrato che la distinzione effettiva delle prestazioni era irrilevante per quasi tutti. Le prestazioni sono solo per ora, la flessibilità del codice è per sempre.
Beh, si è scoperto che Sun aveva ragione a convincere i programmatori C a usare Java (ha funzionato su di me, ho abbandonato CodeWarrior e Lightspeed C), ma sbagliato sull’uso di Java nei browser. Invece, le persone hanno iniziato a utilizzare un’altra lingua chiamata JavaScript per scrivere codice nei browser e hanno utilizzato Java per scrivere codice sui server.
Ti sorprenderà apprendere che JavaScript è stato progettato anche per far scrivere codice ai programmatori C? E che è andato con la sintassi simile a C con parentesi graffe, la dicotomia istruzione / espressione e la notazione a punti? E sebbene JavaScript abbia una cosa che è un po ‘-sorta come un oggetto Java, e un po ‘ – sorta come un dizionario Smalltalk, ti sorprenderà sapere che JavaScript ha anche una distinzione tra currentUser.id = 42
e currentUser.setId(42)
? E che in origine, uno era lento, e l’altro cane-lento, ma i programmatori potrebbero fare importante pensare a quando ottimizzare per le prestazioni e quando dare uno spasso sulla sanità mentale del programmatore?
No, non ti sorprenderà sapere che funziona un po ‘ come C nello stesso modo in cui Java kinda-sort funziona come C, e per esattamente lo stesso motivo. E la ragione non ha più importanza.
il problema con l’accesso diretto
Molto presto dopo che le persone hanno iniziato a lavorare con Java su larga scala, hanno appreso che accedere direttamente alle variabili di istanza era una pessima idea. Compilatori JIT ridotto la differenza di prestazioni tra currentUser.id = 42
e currentUser.setId(42)
quasi niente di rilevanza per nessuno, e il codice utilizzando currentUser.id = 42
o int id = currentUser.id
era notevolmente inflessibile.
Non c’era modo di decorare tali operazioni con preoccupazioni trasversali come la registrazione o la convalida. Non è possibile ignorare il comportamento di impostazione o ottenere un id
in una sottoclasse. (Programmatori Java amano sottoclassi!)
Nel frattempo, anche i programmatori JavaScript stavano scrivendocurrentUser.id = 42
, e alla fine anche loro hanno scoperto che questa era una pessima idea. Uno dei catalizzatori per il cambiamento è stato l’arrivo di framework per le applicazioni JavaScript lato client. Diciamo che abbiamo una classe person ridicolmente semplice:
class Person { constructor (first, last) { this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};
E una vista altrettanto ridicola:
class PersonView { constructor (person) { this.model = person; } // ... redraw () { document .querySelector(`person-${person.id}`) .text(person.fullName()) }}
Ogni volta che aggiorniamo la classe person, dobbiamo ricordarci di ridisegnare la vista:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUserView.redraw();currentUser.first = 'Ragnvald';currentUserView.redraw();
Perché è importante?
Bene, se non riesci a controllare dove vengono gestite determinate responsabilità, non puoi davvero organizzare il tuo programma. Sottoclassi, metodi, mixin e decoratori sono tecniche: ciò che rendono possibile è scegliere quale codice è responsabile di quale funzionalità.
E questo è tutto sulla programmazione: organizzare la funzionalità. L’accesso diretto non consente di organizzare la funzionalità associata alle proprietà get e setting, ma costringe il codice che esegue get e setting a essere responsabile anche di qualsiasi altra cosa associata a get e setting.
get and set
Non ci è voluto molto per gli autori della libreria JavaScript per capire come farlo andare via usando unget
e set
metodo. Ridotto allo stretto necessario per scopi illustrativi, potremmo scrivere questo:
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()) }}
il nuovo Model
superclasse gestisce manualmente, permettendo oggetti per ascoltare il get
e set
metodi su un modello. Se vengono chiamati, i “listener”vengono notificati tramite il metodo .notifyAll
. Lo usiamo per il PersonView
ascoltare la sua Person
e chiamare il proprio .redraw
metodo quando una proprietà viene impostata tramite il .set
metodo.
Così possiamo scrivere:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.set('first', 'Ragnvald');
E non abbiamo bisogno di chiamare currentUserView.redraw()
, perché la notifica integrata in .set
lo fa per noi.
Possiamo fare altre cose con .get
e.set
, ovviamente. Ora che sono metodi, possiamo decorarli con la registrazione o la convalida se scegliamo. I metodi rendono il nostro codice flessibile e aperto all’estensione. Ad esempio, possiamo usare un ES.decoratore successivo per aggiungere consigli di registrazione a .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')}`; }};
Mentre non possiamo fare nulla del genere con accesso diretto alle proprietà. Mediare l’accesso alle proprietà con i metodi è più flessibile rispetto all’accesso diretto alle proprietà, e questo ci consente di organizzare il nostro programma e distribuire correttamente la responsabilità.
Nota: tutte le ES.i decoratori di classe successivi possono essere utilizzati nel codice vanilla ES 6 come funzioni ordinarie. Invece di
@after(LogSetter, 'set') class Person extends Model {...}
, basta scrivereconst Person = after(LogSetter, 'set')(class Person extends Model {...})
getter e setter in javascript
Il problema con i getter e i setter è stato ben compreso, e i custodi dietro di JavaScript evoluzione risposto con l’introduzione di un particolare modo di girare di proprietà diretta di accesso in una sorta di metodo. Ecco come scriveremmo la nostra classePerson
usando “getter” e ” setter:”
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}`; }};
Quando prefiguriamo un metodo con la parola chiave get
, stiamo definendo un getter, un metodo che verrà chiamato quando il codice tenta di leggere dalla proprietà. E quando prefiguriamo un metodo con set
, stiamo definendo un setter, un metodo che verrà chiamato quando il codice tenta di scrivere nella proprietà.
I getter e i setter non hanno bisogno di leggere o scrivere alcuna proprietà, possono fare qualsiasi cosa. Ma in questo saggio, parleremo di usarli per mediare l’accesso alla proprietà. Con getter e setter, possiamo scrivere:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.first = 'Ragnvald';
E tutto funziona ancora come se avessimo scritto currentUser.set('first', 'Ragnvald')
con il .set
-il codice di stile.
Getter e setter ci permettono di avere la semantica dell’uso dei metodi, ma la sintassi dell’accesso diretto.
un after combinator in grado di gestire getter e setter
Getter e setter sembrano a prima vista essere una combinazione magica di sintassi familiare e potente capacità di meta-programma. Tuttavia, un getter o setter non è un metodo nel senso comune. Quindi non possiamo decorare un setter usando lo stesso identico codice che useremmo per decorare un metodo ordinario.
Con il metodo .set
, potremmo accedere direttamente a Model.prototype.set
e avvolgerlo in un’altra funzione. E ‘cosi’ che lavorano i nostri decoratori. Ma non esiste un metodo Person.prototype.first
. Invece, c’è un descrittore di proprietà che possiamo solo introspettare usando Object.getOwnPropertyDescriptor()
e aggiornare usando Object.defineProperty()
.
Per questo motivo, l’ingenuoafter
decoratore sopra indicato non funzionerà per getter e setter.2 Dovremmo usare un tipo di decoratore per i metodi, un altro per i getter e un terzo per i setter. Non sembra divertente, quindi modifichiamo il nostroafter
combinator in modo da poter utilizzare una singola funzione con metodi, getter e setter:
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; }
Ora possiamo scrivere:
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}`; }};
Ora abbiamo disaccoppiato il codice per notificare gli ascoltatori dal codice per ottenere e impostare i valori. Che provoca una semplice domanda: se il codice che tiene traccia degli ascoltatori è già disaccoppiato in Model
, perché il codice per l’attivazione delle notifiche non dovrebbe essere nella stessa entità?
Ci sono alcuni modi per farlo. Useremo un mixin universale invece di inserire quella logica in una superclasse:
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); } }});
Questo ci permette di scrivere:
@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}`; }};
Cosa abbiamo fatto? Abbiamo incorporato getter e setter nel nostro codice, pur mantenendo la possibilità di decorarli con funzionalità aggiuntive come se fossero metodi ordinari.
Questa è una vittoria per la decomposizione del codice. E indica qualcosa a cui pensare: quando tutto ciò che hai sono metodi, sei incoraggiato a creare superclassi dei pesi massimi. Ecco perché così tanti framework ti costringono ad estendere le loro classi base per scopi speciali come Model
o View
.
Ma quando trovi un modo per usare i mixin e decorare i metodi, puoi scomporre le cose in pezzi più piccoli e applicarle dove sono necessarie. Ciò porta nella direzione di utilizzare raccolte di librerie invece di un framework pesante.
sommario
I getter e i setter ci consentono di mantenere lo stile legacy di scrittura del codice che sembra accedere direttamente alle proprietà, mentre in realtà mediano quell’accesso con i metodi. Con cura, possiamo aggiornare i nostri utensili per permetterci di decorare i nostri getter e setter, distribuendo la responsabilità come meglio crediamo e liberandoci dalla dipendenza dalle classi base dei pesi massimi.
(discuss on Hacker News)
Post Scriptum
Questo post utilizza l’ascolto di setter di proprietà come scusa per discutere i meccanismi getter e setter e modi per decorarli in modo da poter organizzare il codice intorno alle preoccupazioni.
Naturalmente, la propagazione delle modifiche tramite notifica esplicita non è l’unico modo per organizzare il codice che deve gestire le dipendenze sulla modifica dei dati. È oltre lo scopo di questo post discutere le molte alternative, ma i lettori hanno suggerito di esplorare l’oggetto.osservare e lavorare con dati immutabili.
un’altra cosa
I rubisti si fanno beffe di:
get first () { return this;}set first (value) { return this = value;}get last () { return this;}set last (value) { return this = value;}
I rubisti userebbero il metodo di classe incorporatoattr_accessor
per scriverli per noi. Quindi, solo per i calci, scriveremo un decoratore che scrive getter e setter. I valori grezzi verranno memorizzati in una mappaattributes
:
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; }}
Ora possiamo scrivere:
@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
prende una lista di nomi di proprietà e restituisce un decoratore per una classe. Scrive una semplice funzione getter o setter per ogni proprietà e tutte le proprietà definite sono memorizzate nell’hash.attributes
. Questo è molto conveniente per la serializzazione o altri meccanismi di persistenza.
(È banale creare anche attrReader
e attrWriter
funzioni usando questo modello. Abbiamo solo bisogno di omettere ilset
quando si scriveattrReader
e omettere ilget
quando si scriveattrWriter
.)
-
C’era anche un linguaggio chiamato BCPL, e altri prima, ma la nostra storia deve iniziare da qualche parte, e inizia con C. Neither
-
Né la ricetta
mixin
ci siamo evoluti in post precedenti come Usare ES.decoratori successivi come Mixins. Può essere migliorato per aggiungere un caso speciale per getter, setter e altre preoccupazioni come lavorare con POJO. Ad esempio, il Mixin universale di Andrea Giammarchi. ↩