Getters, Setters y Responsabilidad de organización en JavaScript

Érase una vez,había un lenguaje llamado C, 1 y este lenguaje tenía algo llamado struct, y se podía usar para crear estructuras de datos heterogéneas agregadas que tenían miembros. La clave que debes saber sobre C es que cuando tienes una estructura llamada currentUser, y un miembro como id, y escribes algo como currentUser.id = 42, el complier de C convirtió esto en instrucciones de ensamblador extremadamente rápidas. Mismo para int id = currentUser.id.

También era importante que pudiera tener punteros a funciones en estructuras, por lo que podría escribir cosas como currentUser->setId(42) si prefería hacer que la configuración de un id fuera una función, y esto también se tradujo en ensamblador rápido.

Y finalmente, la programación en C tiene una cultura muy fuerte de preferir «extremadamente rápido» a simplemente «rápido», y por lo tanto, si quería la atención de un programador en C, tenía que asegurarse de no hacer nunca algo que sea simplemente rápido cuando pudiera hacer algo que sea extremadamente rápido. Esta es una generalización, por supuesto. Estoy seguro de que si preguntamos por ahí, eventualmente conoceremos a ambos programadores de C que prefieren abstracciones elegantes al código extremadamente rápido.

Flathead Dragster

java y javascript

Luego había un lenguaje llamado Java, y estaba diseñado para ejecutarse en navegadores, y ser portátil en todo tipo de hardware y sistemas operativos, y uno de sus objetivos era conseguir que los programadores de C escribieran código Java en el navegador en lugar de escribir C que viviera en un complemento. O mejor dicho, esa era una de sus estrategias, el objetivo era que Sun Microsystems siguiera siendo relevante en un mundo que Microsoft estaba mercantilizando, pero ese es otro capítulo del libro de historia.

Así que la buena gente detrás de Java le dio una sintaxis similar a C con las llaves y la dicotomía sentencia / expresión y la notación de puntos. Tienen «objetos»en lugar de estructuras, y los objetos tienen mucho más que estructuras, pero los diseñadores de Java hicieron una distinción entre currentUser.id = 42 y currentUser.setId(42), y se aseguraron de que una fuera extremadamente rápida y la otra simplemente rápida. O mejor dicho, uno era rápido, y el otro estaba bien comparado con C, pero los programadores de C podían sentir que estaban haciendo un pensamiento importante al decidir si id debía accederse directamente para obtener rendimiento o indirectamente para obtener elegancia y flexibilidad.

La historia ha demostrado que esta era la forma correcta de vender un nuevo idioma. La historia también ha demostrado que la distinción de rendimiento real era irrelevante para casi todo el mundo. El rendimiento es solo por ahora, la flexibilidad del código es para siempre.

Bueno, resultó que Sun tenía razón al hacer que los programadores de C usaran Java (funcionó conmigo, abandoné CodeWarrior y Lightspeed C), pero se equivocó al usar Java en los navegadores. En su lugar, la gente comenzó a usar otro lenguaje llamado JavaScript para escribir código en navegadores, y usó Java para escribir código en servidores.

¿Te sorprenderá saber que JavaScript también fue diseñado para que los programadores de C escribieran código? ¿Y que iba con la sintaxis tipo C con llaves, la dicotomía sentencia / expresión y la notación de puntos? Y aunque JavaScript tiene algo parecido a un objeto Java, y parecido a un diccionario Smalltalk, ¿te sorprenderá saber que JavaScript también tiene una distinción entre currentUser.id = 42y currentUser.setId(42)? Y que originalmente, uno era lento, y el otro lento, pero los programadores podían pensar en cuándo optimizar el rendimiento y cuándo preocuparse por la cordura del programador.

No, no te sorprenderá saber que funciona como C de la misma manera que Java funciona como C, y por exactamente la misma razón. Y la razón ya no importa.

Profesor Frink en Java

el problema con el acceso directo

Muy poco después de que la gente comenzara a trabajar con Java a escala, aprendieron que el acceso directo a las variables de instancia era una idea terrible. Los compiladores JIT redujeron la diferencia de rendimiento entre currentUser.id = 42 y currentUser.setId(42) a casi nada de relevancia para nadie, y el código usando currentUser.id = 42 o int id = currentUser.id fue notablemente inflexible.

No había forma de decorar tales operaciones con preocupaciones transversales como el registro o la validación. No se pudo anular el comportamiento de configurar o obtener un id en una subclase. (A los programadores de Java les encantan las subclases!)

Mientras tanto, los programadores de JavaScript también escribían currentUser.id = 42, y finalmente descubrieron que esta era una idea terrible. Uno de los catalizadores del cambio fue la llegada de frameworks para aplicaciones JavaScript del lado del cliente. Supongamos que tenemos un muy simple persona de clase:

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

Y una igualmente ridículo ver:

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

Cada vez que se actualiza la clase de persona, tenemos que recordar para volver a dibujar la vista:

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

¿por Qué importa esto?

Bueno, si no puedes controlar dónde se manejan ciertas responsabilidades, en realidad no puedes organizar tu programa. Subclases, métodos, mixins y decoradores son técnicas: Lo que hacen posible es elegir qué código es responsable de qué funcionalidad.

Y eso es todo sobre la programación: Organizar la funcionalidad. El acceso directo no le permite organizar la funcionalidad asociada con las propiedades de obtención y configuración, obliga al código que realiza la obtención y configuración a ser también responsable de cualquier otra cosa asociada con la obtención y configuración.

Memoria de núcleo magnético

obtener y establecer

No pasó mucho tiempo para que los autores de la biblioteca JavaScript descubrieran cómo hacer que esto desapareciera utilizando un método get y set. Despojados de lo esencial para fines ilustrativos, podríamos escribir esto:

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

Nuestro nuevo Model superclase manualmente gestiona permitiendo a los objetos para escuchar la etiqueta get y set métodos en un modelo. Si se llaman, los» oyentes»se notifican a través del método .notifyAll. Usamos esto para que PersonView escuche su método Persony llame a su propio método .redrawcuando se establece una propiedad a través del método .set.

Para que podamos escribir:

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

Y no necesitamos llamar a currentUserView.redraw(), debido a que la notificación construido en .set hace por nosotros.

podemos hacer otras cosas con la etiqueta .get y .set, por supuesto. Ahora que son métodos, podemos decorarlos con registro o validación si así lo decidimos. Los métodos hacen que nuestro código sea flexible y abierto a la extensión. Por ejemplo, podemos usar un ES.decorador posterior para agregar consejos de registro 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')}`; }};

Mientras que nosotros no podemos hacer nada de eso con directo acceso a la propiedad. Mediar el acceso a la propiedad con métodos es más flexible que acceder directamente a las propiedades, y esto nos permite organizar nuestro programa y distribuir la responsabilidad correctamente.

Nota: Todas las ES.los decoradores de clase posteriores se pueden usar en el código ES 6 de vainilla como funciones ordinarias. En lugar de @after(LogSetter, 'set') class Person extends Model {...}, simplemente escriba const Person = after(LogSetter, 'set')(class Person extends Model {...})

Técnicas

getters y setters en javascript

El problema con los getters y setters fue bien entendido, y los administradores detrás de la evolución de JavaScript respondieron introduciendo una forma especial de convertir el acceso directo a las propiedades en una especie de método. Así es como escribiríamos nuestra clase Person usando «getters» y » 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}`; }};

Cuando nos prefacio de un método con la palabra clave get, estamos definiendo un getter, un método que será llamado cuando el código intenta leer de la propiedad. Y cuando precedemos un método con set, estamos definiendo un setter, un método al que se llamará cuando el código intente escribir en la propiedad.

Los getters y setters no necesitan leer o escribir ninguna propiedad, pueden hacer cualquier cosa. Pero en este ensayo, hablaremos de usarlos para mediar en el acceso a la propiedad. Con getters y setters, podemos escribir:

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

Y todo lo que todavía funciona como si se hubiera escrito currentUser.set('first', 'Ragnvald').setestilo de código.

Los getters y setters nos permiten tener la semántica de usar métodos, pero la sintaxis del acceso directo.

Keypunch

un combinador after que puede manejar getters y setters

Getters y setters parecen a primera vista ser una combinación mágica de sintaxis familiar y poderosa capacidad de meta-programación. Sin embargo, un getter o setter no es un método en el sentido habitual. Así que no podemos decorar un setter usando exactamente el mismo código que usaríamos para decorar un método ordinario.

Con el método .set, podríamos acceder directamente a Model.prototype.set y envolverlo en otra función. Así es como trabajan nuestros decoradores. Pero no hay un método Person.prototype.first. En su lugar, hay un descriptor de propiedad que solo podemos hacer introspección usando Object.getOwnPropertyDescriptor() y actualizar usando Object.defineProperty().

Por esta razón, el decorador naïve after dado anteriormente no funcionará para getters y setters.2 Tendríamos que usar un tipo de decorador para los métodos, otro para los captadores y un tercero para los fijadores. Eso no suena como la diversión, así que vamos a modificar nuestro after combinador de modo que usted puede utilizar una sola función con métodos getters y 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; }

Ahora podemos escribir:

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

ahora Hemos desacoplado el código para notificar a los oyentes de el código para obtener y establecer valores. Lo que provoca una pregunta sencilla: Si el código que rastrea los oyentes ya está desacoplado en Model, ¿por qué el código para activar notificaciones no debería estar en la misma entidad?

Hay algunas maneras de hacerlo. Vamos a usar un universal mixin en lugar de rellenar esa lógica en una superclase:

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

Esto nos permite escribir:

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

¿Qué hemos hecho? Hemos incorporado getters y setters en nuestro código, manteniendo la capacidad de decorarlos con funcionalidad añadida como si fueran métodos ordinarios.

Eso es una victoria para descomponer código. Y apunta a algo en lo que pensar: Cuando todo lo que tienes son métodos, te animan a hacer superclases de peso pesado. Es por eso que muchos frameworks te obligan a extender sus clases base de propósito especial como Model o View.

Pero cuando encuentre una manera de usar mixins y métodos de decoración, puede descomponer las cosas en piezas más pequeñas y aplicarlas donde sea necesario. Esto conduce en la dirección de usar colecciones de bibliotecas en lugar de un marco de trabajo pesado.

resumen

Los getters y setters nos permiten mantener el estilo heredado de escribir código que parece acceder directamente a las propiedades, mientras que en realidad median ese acceso con métodos. Con cuidado, podemos actualizar nuestras herramientas para que podamos decorar a nuestros getters y setters, distribuyendo la responsabilidad como mejor nos parezca y liberándonos de la dependencia de las clases básicas de peso pesado.

(discuss on Hacker News)

Post Scriptum

Este post utiliza la escucha de los configuradores de propiedades como una excusa para discutir los mecanismos getter y setter, y las formas de decorarlos para que podamos organizar el código en torno a las preocupaciones.

Por supuesto, propagar cambios a través de notificaciones explícitas no es la única manera de organizar el código que necesita para administrar las dependencias en el cambio de datos. Está más allá del alcance de esta publicación discutir las muchas alternativas, pero los lectores han sugerido explorar Objetos.observar y trabajar con datos inmutables.

platillos Voladores para todos

una cosa más

Rubyists se burlan de:

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

Rubyists haría uso de la incorporada en el método de la clase attr_accessor escribir para nosotros. Así que, para divertirnos, escribiremos un decorador que escriba getters y setters. Los valores sin procesar se almacenarán en un mapa attributes :

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

Ahora podemos escribir:

@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 toma una lista de nombres de propiedad y devuelve un decorador para una clase. Escribe una función getter o setter simple para cada propiedad, y todas las propiedades definidas se almacenan en el hash .attributes. Esto es muy conveniente para la serialización u otros mecanismos de persistencia.

(Es trivial hacer también funciones attrReader y attrWriter usando esta plantilla. Sólo tenemos que omitir la etiqueta set al escribir attrReader y omitir la etiqueta get al escribir attrWriter.)

  1. También había un lenguaje llamado BCPL, y otros antes de eso, pero nuestra historia tiene que comenzar en algún lugar, y comienza con C. Neither

  2. Tampoco lo hará la receta mixin que hemos evolucionado en publicaciones anteriores como Usar ES.decoradores posteriores como Mixins. Se puede mejorar para agregar un caso especial para getters, setters y otras preocupaciones como trabajar con POJOs. Por ejemplo, la Mezcla Universal de Andrea Giammarchi. ↩

Deja una respuesta

Tu dirección de correo electrónico no será publicada.