Getters, Setters en organiserende verantwoordelijkheid in JavaScript

Er was eens een taal genaamd C, 1 en deze taal had iets genaamd een struct, en je kon het gebruiken om heterogene geaggregeerde datastructuren te maken met leden. Het belangrijkste dat je moet weten over C is dat wanneer je een struct hebt genaamd currentUser, en een lid als id, en je iets schrijft als currentUser.id = 42, de C complier dit veranderde in extreem snelle assembler instructies. Hetzelfde voor int id = currentUser.id.

ook van belang was dat u pointers naar functies in structs kon hebben, zodat u dingen als currentUser->setId(42) kon schrijven als u er de voorkeur aan gaf om het instellen van een id een functie te maken, en dit werd ook vertaald in fast assembler.

en tot slot, C programmeren heeft een zeer sterke cultuur van de voorkeur “extreem snel” dan gewoon “snel,” en dus als je de aandacht van een C programmeur wilde, moest je ervoor zorgen dat je nooit iets doet dat gewoon snel is als je iets kon doen dat extreem snel is. Dit is een generalisatie, natuurlijk. Ik weet zeker dat als we rondvragen, we uiteindelijk beide C-programmeurs zullen ontmoeten die elegante abstracties verkiezen boven extreem snelle code.

Flathead Dragster

java en javascript

toen was er een taal genaamd Java, en het was ontworpen om te draaien in browsers, en draagbaar zijn over allerlei hardware en besturingssystemen, en een van de doelen was om C programmeurs Java-code te laten schrijven in de browser in plaats van C te schrijven die in een plugin leefde. Of beter gezegd, dat was een van de strategieën, het doel was voor Sun Microsystems om relevant te blijven in een wereld die Microsoft commoditizing, maar dat is een ander hoofdstuk van de geschiedenis boek.

dus de aardige mensen achter Java gaven het een C-achtige syntaxis met de accolades en de statement / expression dichotomie en de dot notatie. Ze hebben “objecten” in plaats van struct ‘s, en objecten hebben veel meer gaande dan struct’ s, maar de ontwerpers van Java maakten een onderscheid tussen currentUser.id = 42 en currentUser.setId(42), en zorgden ervoor dat de ene extreem snel was en de andere gewoon snel. Of beter gezegd, die ene was snel, en de andere was gewoon ok in vergelijking met C, maar C-programmeurs konden het gevoel hebben dat ze belangrijke gedachten deden bij de beslissing of id direct toegankelijk moest worden voor prestaties of indirect toegankelijk voor elegantie en flexibiliteit.

De geschiedenis heeft aangetoond dat dit de juiste manier was om een nieuwe taal te verkopen. De geschiedenis heeft ook aangetoond dat het feitelijke prestatieverschil voor bijna iedereen irrelevant was. Performance is alleen voor nu, Code flexibiliteit is voor altijd.

wel, het bleek dat Sun gelijk had over het krijgen van C programmeurs om Java te gebruiken (het werkte bij mij, ik dumpte CodeWarrior en Lightspeed C), maar verkeerd over het gebruik van Java in browsers. In plaats daarvan, mensen begonnen met behulp van een andere taal genaamd JavaScript om code te schrijven in browsers, en gebruikte Java om code te schrijven op servers.

zal het u verbazen dat JavaScript ook is ontworpen om C-programmeurs code te laten schrijven? En dat het ging met de C-achtige syntaxis met krullende beugels, de uitspraak / uitdrukking dichotomie, en dot notatie? En hoewel JavaScript een ding heeft dat een beetje lijkt op een Java object, en een beetje op een Smalltalk woordenboek, zal het je verbazen dat JavaScript ook een onderscheid maakt tussen currentUser.id = 42 en currentUser.setId(42)? Oorspronkelijk was de ene traag, en de andere hond-traag, maar programmeurs konden belangrijk nadenken over wanneer te optimaliseren voor prestaties en wanneer een giller geven over de gezond verstand van programmeurs?

Nee, Het zal je niet verbazen dat het een beetje als C werkt op dezelfde manier als Java kinda-sort werkt als C, en om precies dezelfde reden. En de reden doet er niet meer toe.

Professor Frink op Java

het probleem met directe toegang

zeer snel nadat mensen begonnen te werken met Java op schaal, leerden ze dat het direct benaderen van instantievariabelen een verschrikkelijk idee was. JIT-compilers versmalden het prestatieverschil tussen currentUser.id = 42 en currentUser.setId(42) tot bijna niets dat voor iedereen relevant is, en code met currentUser.id = 42 of int id = currentUser.id was opmerkelijk inflexibel.

Er was geen manier om dergelijke bewerkingen te verfraaien met horizontale problemen zoals logging of validatie. U kon het gedrag van het instellen of verkrijgen van een id in een subklasse niet overschrijven. (Java programmeurs houden van subklassen!)

ondertussen schreven JavaScript-programmeurs ook currentUser.id = 42, en uiteindelijk ontdekten ook zij dat dit een verschrikkelijk idee was. Een van de katalysatoren voor verandering was de komst van frameworks voor client-side JavaScript-toepassingen. Laten we zeggen dat we een belachelijk eenvoudige persoonsklasse hebben:

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

en een even belachelijk beeld:

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

elke keer dat we de persoonsklasse bijwerken, moeten we onthouden om de beeld:

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

Waarom is dit belangrijk?

Als u niet kunt bepalen waar bepaalde verantwoordelijkheden worden behandeld, kunt u uw programma niet echt organiseren. Subklassen, methoden, mixins en decorateurs zijn technieken: wat ze mogelijk maken is kiezen welke code verantwoordelijk is voor welke functionaliteit.

en dat is het hele ding over programmeren: het organiseren van de functionaliteit. Directe toegang staat je niet toe om de functionaliteit te organiseren die geassocieerd is met het verkrijgen en instellen van eigenschappen, het dwingt de code die het krijgen en instellen doet ook verantwoordelijk te zijn voor iets anders dat geassocieerd is met het krijgen en instellen.

magnetische Kerngeheugen

get en set

Het duurde niet lang voordat auteurs van de JavaScript-bibliotheek erachter kwamen hoe ze dit konden laten verdwijnen met behulp van een get en set methode. Gestript tot de kale essentie voor illustratieve doeleinden, kunnen we dit schrijven:

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

onze nieuwe Modelsuperklasse beheert het handmatig toestaan van objecten om te luisteren naar degetensetmethoden op een model. Als ze worden aangeroepen, worden de “luisteraars”aangemeld via de .notifyAll methode. We gebruiken dat om dePersonView te laten luisteren naar zijnPerson en zijn eigen.redraw methode aan te roepen wanneer een eigenschap is ingesteld via de.set methode.

zodat we kunnen schrijven:

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

en we hoeven currentUserView.redraw()niet aan te roepen, omdat de melding ingebouwd in.setdit voor ons doet.

We kunnen andere dingen doen met .get en .set, natuurlijk. Nu dat het methoden zijn, kunnen we ze versieren met logging of validatie als we kiezen. Methoden maken onze code flexibel en open voor uitbreiding. We kunnen bijvoorbeeld een ES gebruiken.latere decorateur om logboekadvies toe te voegen aan .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')}`; }};

terwijl we zoiets niet kunnen doen met directe eigenschapstoegang. Het bemiddelen van toegang tot onroerend goed met methoden is flexibeler dan het direct benaderen van onroerend goed, en dit stelt ons in staat om ons programma te organiseren en verantwoordelijkheid goed te verdelen.

Opmerking: Alle ES.latere klasse decorateurs kunnen worden gebruikt in vanilla ES 6 code als gewone functies. In plaats van de @after(LogSetter, 'set') class Person extends Model {...} schrijf const Person = after(LogSetter, 'set')(class Person extends Model {...})

Technieken

getters en setters in javascript

Het probleem met getters en setters werd goed begrepen, en de stewards achter JavaScript evolutie gereageerd door de invoering van een speciale manier om rechtstreeks toegang tot eigenschap in een soort van methode. Hier is hoe we onze Person klasse schrijven met behulp van “getters” en “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}`; }};

wanneer we een methode voorafgaan met het trefwoord get, definiëren we een getter, een methode die zal worden aangeroepen wanneer code probeert te lezen van de eigenschap. En als we een methode voorafgaan met set, definiëren we een setter, een methode die zal worden aangeroepen wanneer code probeert naar de eigenschap te schrijven.

Getters en setters hoeven eigenlijk geen eigenschappen te lezen of te schrijven, ze kunnen alles doen. Maar in dit essay zullen we het hebben over het gebruik ervan om toegang tot onroerend goed te bemiddelen. Met getters en setters kunnen we schrijven:

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

en alles werkt nog steeds alsof we currentUser.set('first', 'Ragnvald') met de .set-stijl code hadden geschreven.

Getters en setters staan ons toe om de semantiek van het gebruik van methoden te hebben, maar de syntaxis van directe toegang.

Keypunch

een nacombinator die getters en setters kan verwerken

Getters en setters lijken op het eerste gezicht een magische combinatie van bekende syntaxis en krachtige mogelijkheid om meta-programma. Echter, een getter of setter is niet een methode in de gebruikelijke zin. Dus we kunnen een setter niet versieren met exact dezelfde code die we zouden gebruiken om een gewone methode te versieren.

met de.set methode, kunnen we directModel.prototype.set benaderen en het in een andere functie wikkelen. Zo werken onze binnenhuisarchitecten. Maar er is geen Person.prototype.first methode. In plaats daarvan is er een eigenschap descriptor die we alleen kunnen introspect met Object.getOwnPropertyDescriptor() en update met Object.defineProperty().

om deze reden zal de naïeve after decorator hierboven niet werken voor getters en setters.2 We zouden een soort decorateur moeten gebruiken voor methoden, een andere voor getters, en een derde voor setters. Dat klinkt niet leuk, dus laten we onze after combinator aanpassen zodat je een enkele functie kunt gebruiken met methoden, getters en setters:

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 kunnen we schrijven:

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

we hebben nu de code voor het melden van luisteraars losgekoppeld van de code voor het verkrijgen en instellen van waarden. Wat een simpele vraag oproept: als de code die luisteraars traceert al is ontkoppeld in Model, waarom zou de code voor het activeren van meldingen dan niet in dezelfde entiteit zijn?

Er zijn een paar manieren om dat te doen. We zullen een universele mixin gebruiken in plaats van die logica in een superklasse te vullen:

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

Dit staat ons toe om te schrijven:

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

wat hebben we gedaan? We hebben getters en setters opgenomen in onze code, met behoud van de mogelijkheid om ze te versieren met toegevoegde functionaliteit alsof ze gewone methoden.

dat is een win voor het decomposeren van code. Het wijst op iets om over na te denken: als je alleen maar methoden hebt, word je aangemoedigd om zwaargewicht superklassen te maken. Daarom dwingen zoveel frameworks je om hun speciale basisklassen uit te breiden, zoals Model of View.

maar als je een manier vindt om mixins te gebruiken en methoden te versieren, kun je dingen in kleinere stukken ontbinden en toepassen waar ze nodig zijn. Dit leidt in de richting van het gebruik van collecties van bibliotheken in plaats van een zwaargewicht framework.

samenvatting

Getters en setters laten ons toe om de oude schrijfstijl te behouden die lijkt om direct toegang te krijgen tot eigenschappen, terwijl we die toegang feitelijk bemiddelen met methoden. Met zorg kunnen we onze tooling updaten om ons in staat te stellen onze getters en setters te versieren, verantwoordelijkheid te verdelen naar eigen goeddunken en ons te bevrijden van afhankelijkheid van zwaargewicht basisklassen.

(bespreken op Hacker News)

Post Scriptum

Dit bericht gebruikt luisteren naar eigenschappensetters als excuus om de getter-en settermechanismen te bespreken, en manieren om ze te versieren zodat we code rond zorgen kunnen organiseren.

natuurlijk is het doorgeven van wijzigingen door middel van expliciete notificatie niet de enige manier om code te organiseren die afhankelijkheden moet beheren als gegevens veranderen. Het is buiten het bereik van deze post om de vele alternatieven te bespreken, maar lezers hebben gesuggereerd het verkennen van Object.observeren en werken met onveranderlijke gegevens.

vliegende schotels voor iedereen

nog één ding

Rubyists spotten bij:

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

Rubyists zouden de ingebouwde klassemethode gebruiken attr_accessor om ze voor ons te schrijven. Voor de lol schrijven we een decorateur die getters en setters schrijft. De ruwe waarden worden opgeslagen in eenattributes kaart:

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 kunnen we schrijven:

@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 neemt een lijst met eigenschapsnamen en geeft een decorateur voor een klas. Het schrijft een gewone getter of setter functie voor elke eigenschap, en alle gedefinieerde eigenschappen worden opgeslagen in de .attributes hash. Dit is erg handig voor serialisatie of andere persistance mechanismen.

(Het is triviaal om ook attrReader en attrWriter functies te maken met behulp van dit sjabloon. We hoeven alleen maar set weg te laten bij het schrijven van attrReader en get bij het schrijven van attrWriter.)

  1. Er was ook een taal genaamd BCPL, en andere daarvoor, maar ons verhaal moet ergens beginnen, en het begint met C. ↩

  2. Evenmin zal hetmixin recept dat we hebben ontwikkeld in eerdere berichten zoals het gebruik van ES.later decorateurs als Mixins. Het kan worden verbeterd om een speciaal geval toe te voegen voor getters, setters, en andere zorgen zoals het werken met POJOs. Bijvoorbeeld Andrea Giammarchi ‘ s universele Mixin. ↩

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.