Getter, Setter und Organisationsverantwortung in JavaScript

Es war einmal eine Sprache namens C,1 und diese Sprache hatte etwas namens a struct , und Sie könnten damit heterogen aggregierte Datenstrukturen erstellen, die Mitglieder hatten. Das Wichtigste, was Sie über C wissen sollten, ist, dass der C-Complier, wenn Sie eine Struktur namens currentUser und ein Mitglied wie id und etwas wie currentUser.id = 42 , dies in extrem schnelle Assembler-Anweisungen umgewandelt hat. Gleiches gilt für int id = currentUser.id.

Wichtig war auch, dass Sie Zeiger auf Funktionen in Strukturen haben konnten, so dass Sie Dinge wie currentUser->setId(42) schreiben konnten, wenn Sie es vorzogen, eine id eine Funktion zu setzen, und dies wurde auch in Fast Assembler übersetzt.Und schließlich hat die C-Programmierung eine sehr starke Kultur, „extrem schnell“ einfach „schnell“ zu bevorzugen, und wenn Sie also die Aufmerksamkeit eines C-Programmierers wollten, mussten Sie sicherstellen, dass Sie niemals etwas tun, das nur schnell ist, wenn Sie etwas tun könnten, das extrem schnell ist. Dies ist natürlich eine Verallgemeinerung. Ich bin mir sicher, dass wir, wenn wir herumfragen, irgendwann beide C-Programmierer treffen werden, die elegante Abstraktionen extrem schnellem Code vorziehen.

Flathead Dragster

java und javascript

Dann gab es eine Sprache namens Java, und sie wurde entwickelt, um in Browsern zu laufen und über alle Arten von Hardware und Betriebssystemen portierbar zu sein, und eines ihrer Ziele war es, C-Programmierer dazu zu bringen, Java-Code im Browser zu schreiben, anstatt C zu schreiben, das in einem Plugin lebte. Ziel war es, dass Sun Microsystems in einer Welt, die Microsoft zur Ware machte, relevant bleibt, aber das ist ein weiteres Kapitel des Geschichtsbuchs.

Also gaben die netten Leute hinter Java ihm eine C-ähnliche Syntax mit den Klammern und der Dichotomie zwischen Aussage und Ausdruck und der Punktnotation. Sie haben „Objekte“ anstelle von Strukturen, und Objekte haben viel mehr zu bieten als Strukturen, aber Javas Designer unterschieden zwischen currentUser.id = 42 und currentUser.setId(42) und stellten sicher, dass eines extrem schnell und das andere nur schnell war. Oder besser gesagt, das eine war schnell und das andere war im Vergleich zu C einfach in Ordnung, aber C-Programmierer konnten das Gefühl haben, dass sie wichtige Überlegungen anstellten, wenn sie entschieden, ob id sollte aus Gründen der Leistung direkt oder indirekt aus Gründen der Eleganz und Flexibilität aufgerufen werden.

Die Geschichte hat gezeigt, dass dies der richtige Weg war, um eine neue Sprache zu verkaufen. Die Geschichte hat auch gezeigt, dass die tatsächliche Leistungsunterscheidung für fast alle irrelevant war. Leistung ist nur für den Moment, Code-Flexibilität ist für immer.Nun, es stellte sich heraus, dass Sun Recht hatte, C-Programmierer dazu zu bringen, Java zu verwenden (es funktionierte bei mir, ich habe CodeWarrior und Lightspeed C aufgegeben), aber falsch, Java in Browsern zu verwenden. Stattdessen verwendeten die Leute eine andere Sprache namens JavaScript, um Code in Browsern zu schreiben, und Java, um Code auf Servern zu schreiben.

Wird es Sie überraschen zu erfahren, dass JavaScript auch entwickelt wurde, um C-Programmierer zum Schreiben von Code zu bewegen? Und dass es mit der C-ähnlichen Syntax mit geschweiften Klammern, der Dichotomie zwischen Aussage und Ausdruck und der Punktnotation ging? Und obwohl JavaScript etwas hat, das irgendwie wie ein Java-Objekt und irgendwie wie ein Smalltalk-Wörterbuch ist, wird es Sie überraschen zu erfahren, dass JavaScript auch einen Unterschied zwischen currentUser.id = 42 und currentUser.setId(42) ? Und das ursprünglich, einer war langsam, und der andere Hund-langsam, aber Programmierer konnten wichtige Gedanken darüber machen, wann man für die Leistung optimiert und wann man einen Schrei über die Vernunft des Programmierers gibt?

Nein, es wird Sie nicht überraschen zu erfahren, dass es irgendwie wie C funktioniert, genauso wie Java irgendwie wie C funktioniert, und aus genau dem gleichen Grund. Und der Grund spielt wirklich keine Rolle mehr.

Professor Frink über Java

das Problem mit dem Direktzugriff

Sehr bald nachdem die Leute angefangen hatten, mit Java in großem Maßstab zu arbeiten, erfuhren sie, dass der direkte Zugriff auf Instanzvariablen eine schreckliche Idee war. JIT-Compiler verengten den Leistungsunterschied zwischen currentUser.id = 42 und currentUser.setId(42) auf fast nichts von Relevanz für irgendjemanden, und Code mit currentUser.id = 42 oder int id = currentUser.id war bemerkenswert unflexibel.

Es gab keine Möglichkeit, solche Operationen mit Querschnittsthemen wie Protokollierung oder Validierung zu dekorieren. Sie konnten das Verhalten des Setzens oder Erhaltens einer id in einer Unterklasse nicht überschreiben. (Java-Programmierer lieben Unterklassen!)

In der Zwischenzeit schrieben auch JavaScript-Programmierer currentUser.id = 42, und schließlich entdeckten auch sie, dass dies eine schreckliche Idee war. Einer der Katalysatoren für Veränderungen war die Einführung von Frameworks für clientseitige JavaScript-Anwendungen. Nehmen wir an, wir haben eine lächerlich einfache Person-Klasse:

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

Und eine ebenso lächerliche Ansicht:

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

Jedes Mal, wenn wir die Person-Klasse aktualisieren, müssen wir daran denken, die Ansicht:

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

Warum ist das wichtig?

Nun, wenn Sie nicht kontrollieren können, wo bestimmte Verantwortlichkeiten gehandhabt werden, können Sie Ihr Programm nicht wirklich organisieren. Unterklassen, Methoden, Mixins und Dekoratoren sind Techniken: Sie ermöglichen die Auswahl, welcher Code für welche Funktionalität verantwortlich ist.

Und das ist das Ganze an der Programmierung: Die Organisation der Funktionalität. Direkter Zugriff ermöglicht es Ihnen nicht, die Funktionalität zu organisieren, die mit dem Abrufen und Festlegen von Eigenschaften verbunden ist, sondern zwingt den Code, der das Abrufen und Festlegen ausführt, auch für alles andere verantwortlich zu sein, was mit dem Abrufen und Festlegen verbunden ist.

Magnetkernspeicher

get und set

Die Autoren der JavaScript-Bibliothek brauchten nicht lange, um herauszufinden, wie dies mithilfe einer get und set Methode behoben werden kann. Zur Veranschaulichung auf das Wesentliche reduziert, könnten wir dies schreiben:

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

Unsere neue Model Superklasse verwaltet manuell, dass Objekte die get und set Methoden auf einem Modell abhören können. Wenn sie aufgerufen werden, werden die „Listener“ über die .notifyAll -Methode benachrichtigt. Wir verwenden das, um die PersonView auf ihre Person zu hören und ihre eigene .redraw Methode aufzurufen, wenn eine Eigenschaft über die .set Methode .

Damit wir schreiben können:

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

Und wir müssen currentUserView.redraw()nicht aufrufen, da die in .set integrierte Benachrichtigung dies für uns erledigt.

Mit .get und .set können wir natürlich auch andere Dinge tun. Jetzt, da es sich um Methoden handelt, können wir sie mit Protokollierung oder Validierung dekorieren, wenn wir möchten. Methoden machen unseren Code flexibel und erweiterbar. Zum Beispiel können wir eine ES verwenden.späterer Dekorateur zum Hinzufügen von Protokollierungshinweisen zu .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')}`; }};

Während wir so etwas mit direktem Eigenschaftszugriff nicht machen können. Die Vermittlung des Zugriffs auf Immobilien mit Methoden ist flexibler als der direkte Zugriff auf Immobilien, und dies ermöglicht es uns, unser Programm zu organisieren und die Verantwortung richtig zu verteilen.

Hinweis: Alle ES.spätere Klassendekoratoren können im Vanilla ES 6-Code als normale Funktionen verwendet werden. Anstelle von @after(LogSetter, 'set') class Person extends Model {...} schreiben Sie einfach const Person = after(LogSetter, 'set')(class Person extends Model {...})

Techniken

Getter und Setter in Javascript

Das Problem mit Gettern und setter war gut verstanden, und die Stewards hinter der Entwicklung von JavaScript reagierten mit der Einführung einer speziellen Methode, um den direkten Zugriff auf Eigenschaften in eine Art Methode umzuwandeln. So würden wir unsere Person Klasse mit „getters“ und „setter“ schreiben:“

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

Wenn wir einer Methode das Schlüsselwort getvoranstellen, definieren wir einen Getter, eine Methode, die aufgerufen wird, wenn Code versucht, aus der Eigenschaft zu lesen. Und wenn wir einer Methode mit set , definieren wir einen Setter, eine Methode, die aufgerufen wird, wenn Code versucht, in die Eigenschaft zu schreiben.

Getter und Setter müssen keine Eigenschaften lesen oder schreiben, sie können alles tun. Aber in diesem Aufsatz werden wir darüber sprechen, sie zu verwenden, um den Zugriff auf Eigentum zu vermitteln. Mit Gettern und Setzern können wir schreiben:

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

Und alles funktioniert immer noch so, als hätten wir currentUser.set('first', 'Ragnvald') mit dem .set -style-Code geschrieben.

Getter und Setter ermöglichen uns die Semantik der Verwendung von Methoden, aber die Syntax des direkten Zugriffs.

Keypunch

ein After-Kombinator, der Getter und Setter verarbeiten kann

Getter und Setter scheinen auf den ersten Blick eine magische Kombination aus vertrauter Syntax und leistungsstarker Fähigkeit zur Metaprogrammierung zu sein. Ein Getter oder Setter ist jedoch keine Methode im üblichen Sinne. Wir können also einen Setter nicht mit genau demselben Code dekorieren, mit dem wir eine gewöhnliche Methode dekorieren würden.

Mit der .set Methode könnten wir direkt auf Model.prototype.set zugreifen und es in eine andere Funktion einschließen. So arbeiten unsere Dekorateure. Es gibt jedoch keine Person.prototype.first -Methode. Stattdessen gibt es einen Eigenschaftsdeskriptor, den wir nur mit Object.getOwnPropertyDescriptor() und mit Object.defineProperty() aktualisieren können.

Aus diesem Grund funktioniert der oben angegebene Dekorator naïve after nicht für Getter und Setter.2 Wir müssten eine Art Dekorator für Methoden, eine andere für Getter und eine dritte für Setter verwenden. Das klingt nicht nach Spaß, also ändern wir unseren after Kombinator so, dass Sie eine einzelne Funktion mit Methoden, Gettern und Setzern verwenden können:

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

Jetzt können wir:

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

Wir haben nun den Code zum Benachrichtigen von Listenern vom Code zum Abrufen und Setzen von Werten entkoppelt. Was eine einfache Frage aufwirft: Wenn der Code, der Listener verfolgt, bereits in Model entkoppelt ist, warum sollte sich der Code zum Auslösen von Benachrichtigungen nicht in derselben Entität befinden?

Es gibt verschiedene Möglichkeiten, dies zu tun. Wir werden ein universelles Mixin verwenden, anstatt diese Logik in eine Superklasse zu stopfen:

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

Dies erlaubt uns zu schreiben:

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

Was haben wir getan? Wir haben Getter und Setter in unseren Code integriert, während wir die Möglichkeit beibehalten, sie mit zusätzlichen Funktionen zu dekorieren, als wären sie gewöhnliche Methoden.

Das ist ein Gewinn für das Zerlegen von Code. Und es weist auf etwas hin, worüber man nachdenken sollte: Wenn man nur Methoden hat, wird man ermutigt, schwergewichtige Superklassen zu bilden. Aus diesem Grund zwingen Sie so viele Frameworks dazu, ihre speziellen Basisklassen wie Model oder View .

Wenn Sie jedoch einen Weg finden, Mixins und Dekorationsmethoden zu verwenden, können Sie die Dinge in kleinere Teile zerlegen und dort anwenden, wo sie benötigt werden. Dies führt in die Richtung, Sammlungen von Bibliotheken anstelle eines schwergewichtigen Frameworks zu verwenden.

Zusammenfassung

Getter und Setter ermöglichen es uns, den Legacy-Stil des Schreibens von Code beizubehalten, der direkt auf Eigenschaften zuzugreifen scheint, während dieser Zugriff tatsächlich mit Methoden vermittelt wird. Mit Sorgfalt können wir unsere Werkzeuge aktualisieren, um unsere Getter und Setter zu dekorieren, die Verantwortung nach Belieben zu verteilen und uns von der Abhängigkeit von schwergewichtigen Basisklassen zu befreien.

(diskutieren Sie auf Hacker News)

Post Scriptum

Dieser Beitrag verwendet das Abhören von Eigenschaftssetzern als Vorwand, um die Getter- und Setter-Mechanismen zu diskutieren und sie so zu dekorieren, dass wir Code um sie herum organisieren können.

Natürlich ist die Weitergabe von Änderungen durch explizite Benachrichtigung nicht die einzige Möglichkeit, Code zu organisieren, der Abhängigkeiten von Datenänderungen verwalten muss. Es geht über den Rahmen dieses Beitrags hinaus, die vielen Alternativen zu diskutieren, aber die Leser haben vorgeschlagen, sie zu erkunden.beobachten und Arbeiten mit unveränderlichen Daten.

Fliegende Untertassen für alle

noch eine Sache

Rubyisten spotten über:

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

Rubyisten würden die eingebaute Klassenmethode verwenden attr_accessor um sie für uns zu schreiben. Also nur für Kicks, Wir werden einen Dekorateur schreiben, der Getter und Setter schreibt. Die Rohwerte werden in einer attributes Map gespeichert:

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

Jetzt können wir schreiben:

@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 nimmt eine Liste von Eigenschaftsnamen und gibt einen Dekorateur für eine Klasse zurück. Es schreibt eine einfache Getter- oder Setter-Funktion für jede Eigenschaft, und alle definierten Eigenschaften werden im .attributes -Hash gespeichert. Dies ist sehr praktisch für die Serialisierung oder andere Persistenzmechanismen.

(Es ist trivial, auch attrReader und attrWriter Funktionen mit dieser Vorlage zu erstellen. Wir müssen nur die set weglassen, wenn wir attrReader und die get weglassen, wenn wir attrWriter schreiben.)

  1. Es gab auch eine Sprache namens BCPL und andere davor, aber unsere Geschichte muss irgendwo anfangen und sie beginnt mit C. ↩

  2. Ebenso wenig wird das mixin Rezept, das wir in früheren Beiträgen entwickelt haben, wie die Verwendung von ES.später Dekorateure als Mixins. Es kann erweitert werden, um einen Sonderfall für Getter, Setter und andere Probleme wie die Arbeit mit POJOs hinzuzufügen. Zum Beispiel Andrea Giammarchis Universal Mixin. ↩

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.