Go to English Blog

Design Patterns no JavaScript – Decorator

Leia em 4 minutos

O Decorator Pattern é um mecanismo que permite estender o comportamento de um objeto em tempo de execução, adicionando novas funcionalidades sem ter que criar uma nova classe ou, no caso do JavaScript, função construtora.

Neste exemplo, vamos criar uma função construtora que permite filtrar termos de acordo com as regras definidas por um decorator: é possível remover palavrões ou stop words, além de ser possível adicionar regras personalizadas. Este tipo de aplicação pode ser interessante em um programa de processamento de texto para indexação, por exemplo.

function TextFilter(text) {
  this.text = text;
}

var text = "What the fuck? I thought programming was an easy task! :/\n\nBut maybe I'm wrong! :(";
var filter = new TextFilter(text);

filter = filter.decorate("Cursing");
filter = filter.decorate("StopWord");
filter = filter.decorate("Emoticon");
filter = filter.decorate("Punctuation");
filter = filter.decorate("Spacing");

filter.apply();
// thought programming easy task maybe wrong

O primeiro passo é criar nossa função construtora. Ela deve receber um único argumento que irá representar o texto a ser filtrado.

function TextFilter(text) {
  this.text = text;
}

// Will perform the text filtering. Return the raw
// text by default. This function will be overridden by
// the decorator.
TextFilter.prototype.apply = function() {
  return this.text;
};

// Decorate the current instance with the specified decorator.
TextFilter.prototype.decorate = function(name) {
  // ???
};

A ideia é que a função TextFilter.prototype.decorate crie um novo objeto que representará o filtro e retorne-o para que possa ser atribuído a uma variável.

Primeiro vamos criar o decorator TextFilter.Decorator.Cursing.

// Initialize the namespace.
TextFilter.Decorator = {};

// Define the Cursing decorator.
TextFilter.Decorator.Cursing = function() {
  // Save the previous apply definition.
  var apply = this.apply;

  // The regular expression with prohibited terms.
  var regex = /fuck|cunt|asshole|motherfucker|son of a bitch/gim;

  // Override the apply function with Cursing's filtering.
  this.apply = function() {
    var text = apply.call(this);
    return text.replace(regex, "");
  };
};

A implementação parte do princípio que o texto filtrado será retornado através da função apply(). Por isso, podemos guardar uma referência para a implementação anterior e usamos seu retorno como o texto que deverá ser filtrado naquele decorator em particular.

Já a função TextFilter.prototype.decorate irá usar o nome passado como argumento para inicializar um novo filtro.

TextFilter.prototype.decorate = function(name) {
  // Retrieve the decorator by its name.
  var decorator = TextFilter.Decorator[name];

  // Create a new object because we're going to modify it.
  var filter = Object.create(this);

  // Apply the decorator behavior.
  decorator.call(filter);

  // Return the decorated filter.
  return filter;
};

O próximo decorator é StopWord. Nós iremos usar a lista utilizada pelo PostgreSQL, disponível neste Gist.

TextFilter.Decorator.StopWord = function() {
  var apply = this.apply;
  var regex = /\b(i'm|i|me|my|myself|we|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his|himself|she|her|hers|herself|it|its|itself|they|them|their|theirs|themselves|what|which|who|whom|this|that|these|those|am|is|are|was|were|be|been|being|have|has|had|having|do|does|did|doing|a|an|the|and|but|if|or|because|as|until|while|of|at|by|for|with|about|against|between|into|through|during|before|after|above|below|to|from|up|down|in|out|on|off|over|under|again|further|then|once|here|there|when|where|why|how|all|any|both|each|few|more|most|other|some|such|no|nor|not|only|own|same|so|than|too|very|s|t|can|will|just|don|should|now)\b/gim;

  this.apply = function() {
    var text = apply.call(this);
    return text.replace(regex, "");
  };
};

O decorator de emoticons pode ser bastante grande, mas vamos filtrar apenas alguns deles.

TextFilter.Decorator.escapeForRegex = function(text) {
  return text.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");
};

TextFilter.Decorator.Emoticon = function() {
  var apply = this.apply;

  // Convert the array into a list of valid regex strings.
  var emoticons = [
    ":)", ":(", ";)", "<3", "(Y)",
    "\m/", ":*", "\o/", ":/", ":D"
  ].map(function(emoticon){
    return emoticon.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");
  });

  var regex = new RegExp(emoticons.join("|"), "gim");

  this.apply = function() {
    var text = apply.call(this);
    return text.replace(regex, "");
  };
};

E agora, o filtro de pontuações.

TextFilter.Decorator.Punctuation = function() {
  var apply = this.apply;
  var regex = /[!?.;,{}\[\]\/\()]/g;

  this.apply = function() {
    var text = apply.call(this);
    return text.replace(regex, "");
  };
};

O filtro de normalização de espaços pode utilizar uma expressão regular para normalizar múltiplos espaços, combinado com a função String.prototype.trim, que irá remover os espaços no começo e fim da linha.

TextFilter.Decorator.Spacing = function() {
  var apply = this.apply;
  var regex = /\s+/g;

  this.apply = function() {
    var text = apply.call(this);
    text = text.replace(regex, " ");
    text = text.trim();

    return text;
  };
};

Com esta implementação, fica fácil adicionar novos filtros a qualquer momento. Essa pode ser, inclusive, uma boa estratégia de plugins para bibliotecas que agem como uma pipeline.

Uma característica interessante do Decorator Pattern é que sua implementação pode variar dependendo das características da linguagem e, em uma mesma linguagem, podem existir diferentes possibilidades de implementação.

Function Decorator

Uma outra aplicação de Decorators no JavaScript é específica de funções e tem o nome de Function Decorator. Esse pattern diz respeito a estender o comportamento de uma função, utilizando o escopo léxico de variáveis (escopo de variáveis por função, não por bloco) e funções como objetos de primeira classe (funções são apenas objetos, como strings e arrays) para atingir o seu objetivo.

Imagine que você queira estender o comportamento de uma função existente, garantindo que ela seja executada uma única vez; qualquer chamada subsequente deve ser ignorada.

function init() {
  console.log("Initializing system");
}

init();
// Initializing system

init();
// Initializing system

No exemplo acima, a função init será executada quantas vezes for chamada. Uma solução seria adicionar uma variável de controle que permite saber se ela já foi executada ou não.

var ran = false;

function init() {
  if (!ran) {
    ran = true;
    console.log("Initializing system");
  }
}

O grande problema nesta implementação está no uso de uma variável global para guardar o estado de execução da função. Uma alternativa seria criar uma função que envelopa a função init, fazendo este controle internamente.

function init() {
  console.log("Initializing system");
}

function once(callback, context) {
  var ran = false;

  return function() {
    if (!ran) {
      ran = true;
      callback.apply(context, arguments);
    }
  };
}

init = once(init);

A função Function.prototype.apply recebe dois argumentos: um contexto que será retornado ao acessarmos o this e um array de argumentos. No nosso exemplo, estamos definindo o contexto para o valor passado como o argumento context, que será undefined por padrão. Como argumentos, estamos passando tudo o que foi passado para função retornada.

A grande vantagem desta abordagem é que a função init só precisa se preocupar com o que ela tem que fazer. Isso sem contar que a função once também pode ser reutilizada em qualquer ponto do código.

Finalizando

O Decorator Pattern permite criar objetos extremamente configuráveis sem ter que usar herança de classes. No entanto, ele pode não ser a melhor solução para a maioria dos casos. Em muitas situações, usar o Adapter pattern, que será o assunto do próximo artigo, pode ser uma saída mais simples de entender e implementar, facilitando a manutenção do código.