JavaScript Decoradores: O que são e quando usá-los
On Janeiro 26, 2022 by adminCom a introdução do ES2015+, e como a transposição se tornou comum, muitos de vocês terão se deparado com novas funcionalidades da linguagem, seja em código real ou em tutoriais. Uma destas funcionalidades que muitas vezes tem pessoas a coçar a cabeça quando se deparam pela primeira vez são os decoradores JavaScript.
Decoradores tornaram-se populares graças ao seu uso no Angular 2+. Em Angular, os decoradores estão disponíveis graças ao TypeScript, mas em JavaScript eles são atualmente uma proposta estágio 2, o que significa que eles devem ser parte de uma atualização futura da linguagem. Vamos ver o que são os decoradores, e como podem ser usados para tornar o seu código mais limpo e mais facilmente compreensível.
O que é um Decorador?
Na sua forma mais simples, um decorador é simplesmente uma forma de embrulhar uma peça de código com outra – literalmente “decorá-la”. Este é um conceito que já deve ter ouvido falar anteriormente como composição funcional, ou funções de ordem superior.
Isto já é possível no JavaScript padrão para muitos casos de uso, simplesmente chamando uma função para envolver outra:
function doSomething(name) { console.log('Hello, ' + name);}function loggingDecorator(wrapped) { return function() { console.log('Starting'); const result = wrapped.apply(this, arguments); console.log('Finished'); return result; }}const wrapped = loggingDecorator(doSomething);
Este exemplo produz uma nova função – na variável wrapped
– que pode ser chamada exactamente da mesma forma que a função doSomething
, e fará exactamente a mesma coisa. A diferença é que fará algum registo antes e depois da função embrulhada ser chamada:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
Como usar decoradores JavaScript
Decoradores usam uma sintaxe especial em JavaScript, onde são prefixados com um símbolo @
e colocados imediatamente antes do código ser decorado.
Nota: no momento da escrita, os decoradores estão actualmente em “Stage 2 Draft”, o que significa que estão na sua maioria acabados mas ainda sujeitos a alterações.
É possível utilizar tantos decoradores no mesmo código quantos desejar, e eles serão aplicados na ordem em que os declarar.
Por exemplo:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
Esta define uma classe e aplica três decoradores – dois à própria classe, e um a uma propriedade da classe:
-
@log
poderia registrar todo o acesso à classe -
@immutable
poderia tornar a classe imutável – talvez ela chameObject.freeze
em novas instâncias -
@time
irá registrar o tempo que um método leva para executar e registrar isso com uma tag única.
No momento, usar decoradores requer suporte a transpiler, já que nenhum navegador ou lançamento de Node atual tem suporte a eles ainda. Se você estiver usando Babel, isto é habilitado simplesmente usando o plugin transform-decoradores-legacy.
Note: o uso da palavra “legado” neste plugin é porque suporta a forma Babel 5 de lidar com decoradores, que pode muito bem ser diferente da forma final quando eles são padronizados.
Porquê usar decoradores?
Se já é possível a composição funcional em JavaScript, é significativamente mais difícil – ou mesmo impossível – aplicar as mesmas técnicas a outras peças de código (por exemplo, classes e propriedades das classes).
A proposta do decorador adiciona suporte para decoradores de classes e propriedades que podem ser usados para resolver estes problemas, e futuras versões JavaScript provavelmente adicionarão suporte para decoradores para outras áreas problemáticas do código.
Decoradores também permitem uma sintaxe mais limpa para aplicar estes wrappers à volta do seu código, resultando em algo que diminui menos a intenção real do que está a escrever.
Diferentes Tipos de Decoradores
No presente, os únicos tipos de decoradores que são suportados estão em classes e membros de classes. Isto inclui propriedades, métodos, getters e setters.
Decoradores nada mais são do que funções que retornam outra função, e que são chamadas com os detalhes apropriados do item sendo decorado. Estas funções de decoradores são avaliadas uma vez quando o programa é executado pela primeira vez, e o código decorado é substituído pelo valor de retorno.
Class member decorators
Property decorators are applied to a single member in a class – whether they are properties, methods, getters, or setters. Esta função decorador é chamada com três parâmetros:
-
target
: a classe em que o membro está. -
name
: o nome do membro na classe. -
descriptor
: o descritor de membro. Este é essencialmente o objeto que teria sido passado para Object.defineProperty.
: o exemplo clássico usado aqui é @readonly
. Isto é implementado tão simplesmente como:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Atualizando literalmente o descritor de propriedades para definir o flag “escrevível” como falso.
Isto é então usado em uma propriedade de classe como segue:
class Example { a() {} @readonly b() {}}const e = new Example();e.a = 1;e.b = 2;// TypeError: Cannot assign to read only property 'b' of object '#<Example>'
Mas podemos fazer melhor do que isto. Na verdade, podemos substituir a função decorada por um comportamento diferente. Por exemplo, vamos registrar todas as entradas e saídas:
function log(target, name, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments: ${args}`); try { const result = original.apply(this, args); console.log(`Result: ${result}`); return result; } catch (e) { console.log(`Error: ${e}`); throw e; } } } return descriptor;}
Isso substitui todo o método por um novo que registra os argumentos, chama o método original e então registra a saída.
Nota que usamos o operador de spread aqui para construir automaticamente um array a partir de todos os argumentos fornecidos, que é a alternativa mais moderna ao antigo arguments
valor.
Podemos ver isso em uso da seguinte forma:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Você vai notar que tivemos que usar uma sintaxe ligeiramente engraçada para executar o método decorado. Isto poderia cobrir um artigo inteiro, mas em resumo, a função apply
permite chamar a função, especificando o valor this
e os argumentos para a chamar com.
Acima de um entalhe, podemos arranjar para o nosso decorador levar alguns argumentos. Por exemplo, vamos reescrever o nosso log
decorador como se segue:
function log(name) { return function decorator(t, n, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments for ${name}: ${args}`); try { const result = original.apply(this, args); console.log(`Result from ${name}: ${result}`); return result; } catch (e) { console.log(`Error from ${name}: ${e}`); throw e; } } } return descriptor; };}
Isto está a ficar mais complexo agora, mas quando o decompomos temos isto:
- Uma função,
log
, que leva um único parâmetro:name
. - Esta função retorna então uma função que é ela própria um decorador.
Esta é idêntica à anterior log
decorador, excepto que faz uso do parâmetro name
da função externa.
Esta é então usada da seguinte forma:
class Example { @log('some tag') sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments for some tag: 1,2// Result from some tag: 3
Apartir de uma linha de log, podemos ver que isto nos permite distinguir entre diferentes linhas de log usando uma tag que nós próprios fornecemos.
>
Isto funciona porque a chamada de função log('some tag')
é avaliada imediatamente pelo tempo de execução do JavaScript, e depois a resposta a partir daí é usada como o decorador para o método sum
.
Class decorators
Class decorators are applied to the whole class definition all in one go. A função decorator é chamada com um único parâmetro que é a função construtor sendo decorada.
Note que isto é aplicado à função construtor e não a cada instância da classe que é criada. Isto significa que se você quiser manipular as instâncias você mesmo precisa fazê-lo retornando uma versão embrulhada do construtor.
Em geral, estas são menos úteis que os decoradores membros da classe, porque tudo que você pode fazer aqui você pode fazer com uma simples chamada de função exatamente da mesma forma. Qualquer coisa que você faça com elas precisa acabar retornando uma nova função de construtor para substituir o construtor da classe.
Voltando ao nosso exemplo de registro, vamos escrever uma que registre os parâmetros do construtor:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Aqui estamos aceitando uma classe como nosso argumento, e retornando uma nova função que atuará como o construtor. Isso simplesmente registra os argumentos e retorna uma nova instância da classe construída com esses argumentos.
Por exemplo:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
Vemos que a construção da nossa classe Exemplo irá logar os argumentos fornecidos e que o valor construído é de fato uma instância de Example
. Exatamente o que queríamos.
Passar parâmetros em decoradores de classe funciona exatamente como para os membros da classe:
function log(name) { return function decorator(Class) { return (...args) => { console.log(`Arguments for ${name}: args`); return new Class(...args); }; }}@log('Demo')class Example { constructor(name, age) {}}const e = new Example('Graham', 34);// Arguments for Demo: argsconsole.log(e);// Example {}
Exemplos do Mundo Real
Core decoradores
Existe uma biblioteca fantástica chamada Core Decorators que fornece alguns decoradores comuns muito úteis que estão prontos para usar no momento. Estes geralmente permitem funcionalidades comuns muito úteis (por exemplo, tempo de chamadas de métodos, avisos de depreciação, assegurando que um valor é apenas de leitura) mas utilizando a sintaxe muito mais limpa do decorador.
Reagir
A biblioteca Reagir faz muito bom uso do conceito de Componentes de Ordem Superior. Estes são simplesmente componentes React que são escritos como uma função, e que envolvem outro componente.
Comprar o nosso curso Premium: Reage The ES6 Way
Estes são um candidato ideal para usar como decorador, porque há muito pouco que você precisa mudar para fazer isso. Por exemplo, a biblioteca React redux tem uma função, connect
, que é usada para conectar um componente React a uma loja Redux.
Em geral, isto seria usado da seguinte forma:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
No entanto, devido à forma como a sintaxe do decorador funciona, isto pode ser substituído pelo seguinte código para obter exactamente a mesma funcionalidade:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
A biblioteca MobX faz um uso extensivo dos decoradores, permitindo-lhe marcar facilmente campos como Observáveis ou Computed, e marcar classes como Observadores.
Sumário
Os decoradores de membros de classe fornecem uma maneira muito boa de embrulhar o código dentro de uma classe de forma muito semelhante à forma como você já pode fazer isso para funções independentes. Isto fornece uma boa maneira de escrever algum código simples de ajuda que pode ser aplicado em muitos lugares de uma maneira muito limpa e fácil de entender.
O único limite para usar tal facilidade é a sua imaginação!
Deixe uma resposta