Desenvolvimento

2 mar, 2015

Constructors não deveriam causar efeitos secundários

Publicidade

Eu vou falar sobre algo simples que demorou um pouco para que eu descobrisse o que estava fazendo de errado. Não sei quando foi que percebi que era um grande erro. Só sei que é algo que eu venho tentando evitar por um tempo.

Para começar, eu não sou o primeiro a chegar a essa conclusão. Muitas pessoas pensaram a mesma coisa antes. É, ainda, uma das regras do JSLint: Não use ‘new’ para efeitos secundários). O fato é que eu não acho que a maioria das pessoas sabe a razão pela qual isso deve ser evitado; então deixe-me explicar o porquê.

Constructors não são verbos

Eu acredito que os métodos devem ser nomeados como verbos, para deixar claro que eles executam ações.

new XMLHttpRequest não dispara o pedido, new HTMLDivElement() não deve anexá-lo ao documento – Elliott Sprehn.

Existe um princípio muito importante chamado Command-Query Separation. Esse princípio afirma que as funções devem ser tanto um comando que executa uma ação, quanto uma consulta que retorna dados para o caller. Há também outro princípio muito importante para o projeto SOLID, que é o princípio da responsabilidade única (Single Responsibility Principle). De acordo com o SRP, cada método, módulo e classe deve ter uma única responsabilidade, aumentando a coesão.

Então, por que estou falando sobre CQS, SRP e coesão? Porque os constructors têm uma responsabilidade muito específica. Eles não são nada mais do que uma abstração usada para criar uma nova instância. Ele deve fazer o mínimo de trabalho necessário para a configuração do objeto; basicamente, apenas configurar propriedades padrão e permitir algum tipo de configuração, só para que cada instância tenha seus próprios valores. Em muitos casos, os construtors devem estar vazios (sem qualquer lógica).

Mais difícil de testar

Se o seu constructor executa qualquer tipo de ação, torna-se muito difícil fazer um teste unitário nele. Imagine o seguinte cenário:

// WARNING: this is an anti-pattern!
function Dialog(opts) {
  this.msg = opts.msg;
  this.size = opts.size || 'small';
  // ... imagine a few more options
  this.element = document.createElement('div');
  // ... imagine a few more lines of code building the element structure
  this._setupEventListeners();
  // and automatically appending the element to the DOM
  document.body.appendChild(this.element);
}

Pense nisso por um momento. Como você faria o teste unitário nele?

Se você deixar o constructor com uma única responsabilidade, em vez disso:

function Dialog(opts) {
  // we also remove the "message" from the constructor, a dialog should be able
  // to display multiple messages.
  this.size = opts.size || 'small';
}

Testar se torna fácil, uma vez que você tem a certeza de que isso é usado apenas como uma forma de definir as opções.

describe('Dialog constructor', function() {
  it('should set default options if none provided', function() {
    var dialog = new Dialog();
    expect(dialog.size).toEqual('small');
  });

  it('should override default options if provided', function() {
    var dialog = new Dialog({ size: 'large' });
    // PS: notice that `size` could be set later since it's a public property,
    // so in many cases it's OK to have empty constructors.
    expect(dialog.size).toEqual('large');
  });
});

Isso também nos permite criar instâncias durante a configuração de testes, sem nos preocuparmos se o ambiente está no estado adequado.

Principalmente se a ação for assíncrona!

Não é incomum ter widgets que dependem de dados provenientes de um servidor. Isso é, definitivamente, a maior violação.

// WARNING: this is an anti-pattern!
function Weather(cityName) {
  request('http://example.com/?city='+ cityName, this._setup.bind(this));
}

Você agora, certamente, não pode testar o Weather sem mocking a função request e o consumidor da API não tem nenhuma ideia de quando o Weather está realmente pronto para ser usado. Você também não tem a opção de atrasar a solicitação AJAX. Isso é um desastre esperando para acontecer.

Não é possível transmitir uma referência à instância por aí e esperar que ela apenas funcione. Todos os hacks para contornar esse problema (eventos, fila de comando, safeguards…) conduzirão a um código que é difícil de manter, de reutilizar e de testar. Então, por favor, não faça isso!

A solução apropriada seria adicionar um segundo método que desencadeia realmente a solicitação AJAX quando necessário. Assim, o consumidor dessa API faria:

var saoPauloWeather = new Weather('sao-paulo');
// the `get` method can have a callback which makes intent way clearer and
// decouples the instantiation from the AJAX request, allowing us to defer the
// execution till needed. You could even cache the result so subsequent calls
// to `get` would execute on the next tick (callback should always be called
// asynchronously to avoid mistakes)
saoPauloWeather.get(onWeatherLoad);

Isso tem um efeito secundário muito bom que nos permite fazer mock no método get durante os testes para ignorar completamente o pedido AJAX se necessário.

Como detectar facilmente as violações

Se você simplesmente está criando uma instância, mas não chamando qualquer método na própria instância, isso significa que ela provavelmente não deve ser um constructor.

// WARNING: anti-pattern!
function showMessage() {
  new Dialog('this is an anti-pattern!');
}

Para realmente justificar o constructor, o seu código deve ser parecer mais com isto:

var dialog = new Dialog();
dialog.showMessage('this is the way to do it');
// ... later in the code
dialog.close();

Se sua classe só for usada para executar uma única ação e for imediatamente eliminada, então ela deve ser apenas uma função regular.

“Às vezes, a implementação elegante é uma função. Não é um método. Não é uma classe. Não é um framework. Só uma função”. John Carmack

É sempre bom lembrar que você pode utilizar closures para segurar estado/dados; você não precisa de instâncias em muitos casos.

function showDialog(msg) {
  var element = document.createElement('div');
  // ... imagine more code here to setup the dialog
  closeButton.addEventListener('click', function() {
    // because of the closure you still have access to the "element" variable,
    // this technique can be useful in many cases.
    element.parentNode.removeChild(element);
  });
}

Conclusão

Espero que esteja claro quais são alguns dos inconvenientes de adicionar muita lógica dentro de constructors e, também, alguns dos benefícios do parcelamento da lógica em métodos discretos. Pode parecer mais burocrático no início, mas no final ele vai recompensar, especialmente se você mantê-lo consistente.

Minha regra de ouro é: nunca executar o código automaticamente, deixe o consumidor da API chamar as coisas de forma explícita; isso aumenta a flexibilidade e permite mais controle.

Por hora, isso é tudo!

***

Miller Medeiros faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://blog.millermedeiros.com/constructors-should-not-cause-side-effects/