Getters, Setters, e Organizar a Responsabilidade em JavaScript

era uma vez, havia uma linguagem chamada C,1 e esta linguagem tinha uma coisa chamada struct, e você pode usá-lo para fazer heterogeneously agregada estruturas de dados que tinha membros. A principal coisa a se saber sobre C é que quando você tem uma estrutura chamada de currentUser, e um membro como o id, e você escreve algo como currentUser.id = 42, o compilador C converteu em extremamente rápido de instruções assembler. O mesmo para int id = currentUser.id.

Também de importância foi a de que você poderia ter ponteiros para funções em estruturas, para que você pudesse escrever coisas como currentUser->setId(42) se você preferiu fazer a definição de uma id uma função, e esta foi também traduzido em rápida do integrador.

E, finalmente, de programação C tem uma forte cultura de preferindo “extremamente rápido” para apenas “rápido”, e assim se você queria um programador de C atenção, você tinha para se certificar de que você nunca fazer algo que é apenas rápido, quando você poderia fazer algo que é extremamente rápido. Isto é uma generalização, claro. Tenho certeza que se perguntarmos por aí, eventualmente conheceremos ambos os programadores C que preferem abstrações elegantes ao Código extremamente rápido.

Chave de Dragster

java e javascript

em Seguida, houve uma linguagem chamada de Java, e ele foi projetado para rodar em navegadores, e ser portátil em todos os tipos de hardware e sistemas operacionais, e um dos seus objetivos era conseguir programadores de C para escrever código Java no navegador, em vez de escrever C que viveu em um plugin. Ou melhor, essa era uma de suas estratégias, o objetivo era que a Sun Microsystems permanecesse relevante em um mundo que a Microsoft estava mercantilizando, mas esse é outro capítulo do livro de história.

assim, as pessoas simpáticas por trás de Java lhe deram sintaxe semelhante a C com os suspensórios e a dicotomia afirmação/expressão e a notação de ponto. Eles têm “objetos” em vez de estruturas, objetos e tem muito mais coisas acontecendo do que estruturas, mas o Java designers feita uma distinção entre currentUser.id = 42 e currentUser.setId(42), e a certeza de que um foi extremamente rápido e o outro era apenas rápido. Ou melhor, que um era rápido, e o outro era apenas ok em comparação com C, mas Programadores de C poderiam sentir que eles estavam fazendo um pensamento importante ao decidir se id deveria ser acessado diretamente para o desempenho ou indiretamente acessado para elegância e flexibilidade.

A história mostrou que esta era a maneira certa de vender uma nova língua. A história também mostrou que a distinção de desempenho real era irrelevante para quase todos. O desempenho é apenas por agora, a flexibilidade do código é para sempre.

Bem, descobriu-se que o Sol estava certo sobre a obtenção de programadores C usar Java (funcionou em mim, abandonei CodeWarrior e Lightspeed C), mas errado sobre o uso do Java em navegadores. Em vez disso, as pessoas começaram a usar outra linguagem chamada JavaScript para escrever código em navegadores, e usaram Java para escrever código em servidores.

irá surpreendê-lo saber que JavaScript também foi projetado para fazer com que os programadores C escrevam código? E que foi com a sintaxe de Tipo C com Chavetas, a dicotomia afirmação / expressão, e notação de ponto? E embora o JavaScript tem uma coisa que é meio-sorta como um objeto Java, meio-sorta como um Smalltalk dicionário, será que você se surpreenda ao saber que o JavaScript também tem uma distinção entre currentUser.id = 42 e currentUser.setId(42)? E que originalmente, um era lento, e o outro cão-lento, mas programadores poderiam fazer um pensamento importante sobre quando otimizar para o desempenho e quando dar uma piada sobre a sanidade programadora?

Não, Não vai surpreender você aprender que ele funciona mais ou menos como C, da mesma forma que Java tipo-sort funciona como C, e exatamente pela mesma razão. E a razão já não interessa.

Professor Frink on Java

the problem with direct access

muito logo após as pessoas começarem a trabalhar com Java em escala, eles aprenderam que acessar diretamente variáveis de instância era uma idéia terrível. Compiladores JIT estreitou a diferença de desempenho entre currentUser.id = 42 e currentUser.setId(42) quase nada de relevância para ninguém, e de código usando currentUser.id = 42 ou int id = currentUser.id foi extremamente inflexível.

não havia nenhuma maneira de decorar tais operações com preocupações transversais como o registro ou validação. Não foi possível sobrepor o comportamento da configuração ou obter umid numa subclasse. (Programadores Java adoram subclasses!)

entretanto, programadores JavaScript também estavam escrevendo currentUser.id = 42, e eventualmente eles também descobriram que esta era uma idéia terrível. Um dos catalisadores para a mudança foi a chegada de frameworks para aplicações JavaScript lado cliente. Vamos dizer que temos um ridiculamente simples classe pessoa:

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

, E um igualmente ridículo ver:

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

Toda vez que atualizar a classe pessoa, temos de nos lembrar para redesenhar o visualizar:

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

Por que isso importa?

bem, se você não pode controlar onde certas responsabilidades são tratadas, você não pode realmente organizar o seu programa. Subclasses, métodos, mixinas e decoradores são técnicas: o que eles tornam possível é escolher qual o código responsável por qual funcionalidade.

E isso é a coisa toda sobre programação: organizar a funcionalidade. O acesso direto não permite que você organize a funcionalidade associada com a obtenção e configuração de propriedades, ele força o código fazendo a obtenção e configuração para também ser responsável por qualquer outra coisa associada com a obtenção e configuração.

Núcleo Magnético de Memória

get e set

não demorou muito para a biblioteca JavaScript autores para descobrir como fazer isso ir longe, usando um get e set método. Despojados do essencial para fins ilustrativos, podíamos escrever isto.:

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

a Nossa nova Model superclasse manualmente permitindo que gerencia objetos para ouvir o get e set métodos em um modelo. Se forem chamados, os” ouvintes”são notificados através do método .notifyAll. Nós usamos e que para ter o PersonView ouvir o seu Person e chame seu próprio .redraw método quando uma propriedade é definida através de .set método.para que possamos escrever:

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

E não precisamos chamar currentUserView.redraw(), porque a notificação foi construída em .set faz isso por nós.

Podemos fazer outras coisas com .gete.set, claro. Agora que são métodos, podemos decorá-los com registo ou validação se escolhermos. Os métodos tornam o nosso código flexível e aberto à Extensão. Por exemplo, podemos usar um E.mais tarde decorador para adicionar conselhos de registo 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')}`; }};

Considerando que não podemos fazer nada assim com acesso directo à propriedade. Mediar o acesso à propriedade com métodos é mais flexível do que acessar diretamente propriedades, e isso nos permite organizar nosso programa e distribuir a responsabilidade corretamente.

Nota: Todas as ES.mais tarde, os decoradores de classe podem ser usados no código baunilha ES 6 como funções ordinárias. Em vez de @after(LogSetter, 'set') class Person extends Model {...}, basta escrever const Person = after(LogSetter, 'set')(class Person extends Model {...})

Técnicas

getters e setters em javascript

O problema com getters e setters foi bem compreendido, e os administradores atrás de JavaScript da evolução respondeu através da introdução de uma maneira especial para ligar direto propriedade de acesso em uma espécie de método. Aqui está como nós escreveríamos nosso Person classe usando “getters” e “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}`; }};

Quando nós prefácio de um método com a palavra-chave get, estamos definindo um getter, um método que será chamado quando o código tenta ler a partir da propriedade. E quando nós prefácio de um método com set, estamos definindo um setter, um método que será chamado quando o código tenta escrever para a propriedade.

Getters e setters não precisam realmente ler ou escrever quaisquer propriedades, eles podem fazer qualquer coisa. Mas neste ensaio, vamos falar sobre usá-los para mediar o acesso à propriedade. Com getters e setters, podemos escrever:

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

E tudo continua a funcionar como se tivéssemos escrito currentUser.set('first', 'Ragnvald') com o .setestilo de código.

Getters and setters allow us to have the semantics of using methods, but the syntax of direct access.

Keypunch

após combinator que pode lidar com getters e setters

de Getters e setters parecer à primeira vista ser uma combinação mágica de familiares de sintaxe e poderosa capacidade de meta-programa. No entanto, um getter ou setter não é um método no sentido usual. Então não podemos decorar um setter usando o mesmo código que usaríamos para decorar um método comum.

With The .set method, we could directly access Model.prototype.set and wrap it in another function. É assim que os nossos decoradores funcionam. But there is no Person.prototype.first method. Em vez disso, há um descritor de propriedade que só podemos introspeccionar usando Object.getOwnPropertyDescriptor() e atualizar usando Object.defineProperty().

Por esta razão, o decorador acima indicado não funcionará para getters e setters.2 teríamos que usar um tipo de decorador para métodos, outro para getters, e um terceiro para setters. Isso não soa como diversão, então vamos modificar o nosso after combinator, de modo que você pode usar uma única função com métodos getters e 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; }

Agora podemos escrever:

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

temos agora dissociado do código de notificação de ouvintes do código para obter e definir valores. O que provoca uma pergunta simples: se o código que rastreia os ouvintes já está dissociado em Model, por que o código para desencadear notificações não deveria estar na mesma entidade?

existem algumas maneiras de fazer isso. Usaremos uma mixina universal em vez de colocar essa lógica numa 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); } }});

Isto permite-nos escrever:

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

o que fizemos? Incorporamos getters e setters em nosso código, mantendo a capacidade de decorá-los com funcionalidade adicionada como se fossem métodos comuns.

é uma vitória para o código de decomposição. E aponta para algo em que pensar: quando tudo o que você tem são métodos, você é encorajado a fazer superclasses de pesos pesados. É por isso que tantos frameworks o obrigam a estender suas classes de base de propósito especial como Model ou View.

mas quando você encontrar uma maneira de usar mixinas e decorar métodos, você pode decompor as coisas em pedaços menores e aplicá-los onde eles são necessários. Isto conduz à utilização de colecções de bibliotecas em vez de um quadro pesado.

resumo

Getters e setters permitem-nos manter o estilo legado de escrever código que parece aceder directamente às propriedades, ao mesmo tempo que mediam esse acesso com métodos. Com cuidado, podemos atualizar nossa ferramenta para nos permitir decorar nossos getters e setters, distribuindo a responsabilidade como nos parece adequado e nos libertando da dependência de classes de pesos pesados.

(discuss on Hacker News)

Post Scriptum

Este post usa ouvir os setters de propriedades como uma desculpa para discutir os mecanismos de getter e setter, e formas de decorá-los para que possamos organizar o código em torno de preocupações.

é claro, a propagação de alterações através de notificação explícita não é a única maneira de organizar o código que precisa gerenciar dependências de mudança de dados. Está além do escopo deste post para discutir as muitas alternativas, mas os leitores têm sugerido explorar objeto.observar e trabalhar com dados imutáveis.

discos Voadores para todos

mais uma coisa

Rubyists zombam:

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

Rubyists seria usar o built-in de método de classe attr_accessor para escrevê-los para nós. Por isso, só por Diversão, escrevemos um decorador que escreve getters e setters. Os valores brutos serão armazenados num 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; }}

Agora, podemos escrever:

@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 tem uma lista de nomes de propriedade e retorna um decorador para uma classe. Ele escreve uma função getter ou setter simples para cada propriedade, e todas as propriedades definidas são armazenadas no .attributes hash. Isto é muito conveniente para serialização ou outros mecanismos de persistência.

(é trivial também fazerattrReader eattrWriter funções usando este modelo. Só precisamos omitir o set ao escrever attrReader e omitir o get ao escrever attrWriter.)

  1. houve também uma linguagem chamada BCPL, e outros antes, mas a nossa história tem que começar em algum lugar, e ele começa com C. ↩

  2. Nem mixin receita evoluímos em posts anteriores, como o Uso de ES.mais tarde decoradores como Mixins. Ele pode ser melhorado para adicionar um caso especial para getters, setters, e outras preocupações como trabalhar com POJOs. Por exemplo, o Mixin Universal de Andrea Giammarchi. ↩

Deixe uma resposta

O seu endereço de email não será publicado.