Encadeamento de Métodos em JavaScript

Popular em diversas bibliotecas JavaScript, o encadeamento de métodos (“method chaining”) é uma técnica usada para invocar diversos métodos em um mesmo objeto.

Com o objetivo de melhorar a legibilidade do código, a técnica é vastamente utilizada na API da jQuery, o que certamente influenciou na popularidade da biblioteca.

Se você já utilizou jQuery, o estilo do código abaixo pode ser familiar:

$('#my-element')
  .css('background', 'purple')
  .height(100)
  .animate({ height: 250 })
  .find('input')
    .fadeIn(200)
    .val('')
    .end()

No exemplo acima, uma única declaração faz várias coisas. No entanto, uma boa prática com method chaining é fazer somente uma ação por declaração. 😉

Perceba que vários métodos são invocados no objeto $('#my-element'), sem a necessidade de repetí-lo. Já sem Method Chaining, é necessário fazer a referência diversas vezes:

const myElement = $('#my-element')
myElement.css('background', 'purple')
myElement.height(100)
myElement.fadeIn(200)

Exemplo

Vamos criar um contador Counter:

class Counter {
  constructor () {
    this.value = 0
  }

  increase () {
    this.value += 1
  }

  decrease () {
    this.value -= 1
  }

  log () {
    console.log(this.value)
  }
}

Agora, vamos instanciar um contador e usar seus métodos:

const counter = new Counter()
counter.increase()
counter.log() // => 1
counter.decrease()
counter.log() // => 0

Perceba que é necessário fazer várias declarações para interagir com a instância, o que prejudica a legibilidade do código.

E se tentarmos usar Method Chaining

new Counter().increase().log()
// > TypeError: Cannot read property 'log' of undefined

Perceba que log() está sendo executado em new Counter().increase(), que, por sua vez, está retornando undefined. Portanto, ainda não é possível interagir com Counter dessa forma.

Como Encadear Métodos

Para evitar a repetição do objeto, é necessário que seus métodos retornem o próprio objeto.

Veja este exemplo com Promises:

getJSON('users.json')
  .then(JSON.parse)
  .then(response => console.log("Olha o JSON!", response))
  .catch(error => console.log("Falhou!", error))

Isso só é possível pois os métodos then() and catch() sempre retornam outras promises. Assim, podemos dizer que as Promises são fluent APIs, tal como a jQuery.

Quem Lembra do this?

Para os métodos serem encadeados, será necessário retornar o contexto (this) em cada método.

Em JavaScript, this sempre se refere ao contexto de execução de função.

No caso de um método, que é uma função de um objeto, refere-se ao próprio objeto.

Exemplo com Method Chaining Pattern

Para implementar o encadeamento de métodos na classe Counter, apenas retornamos seu contexto a cada método:

class Counter {
  constructor () {
    this.value = 0
  }
  increase () {
    this.value += 1
    return this // Aqui!
  }
  decrease () {
    this.value -= 1
    return this // Aqui!
  }
  log () {
    console.log(this.value)
    return this // E aqui!
  }
}

Agora, ao executar new Counter().increase(), o retorno já não será mais undefined.

…E, portanto, é possível fazer method chaining!

new Counter()
  .increase()
  .log() // => 1
  .decrease()
  .log() // => 0

Conclusão

No universo de APIs orientadas a objetos, o encadeamento de métodos é uma técnica incrível se o seu objetivo é tornar o código mais expressivo e fluente.

No geral, fluent APIs são sim interessantes de se entender e implementar, e você pode ter certeza disso analisando o primeiro exemplo com jQuery do início deste artigo. É fantástico! Mas é importante entender que o encadeamento de métodos nem sempre tornará as coisas mais fáceis (debugar, por exemplo, se torna mais difícil), e, portanto, a maneira aparentemente “mágica” com que elas funcionam não deve ser sempre levada em consideração.