0% acharam este documento útil (0 voto)
2K visualizações

Asynchronous Refactoring - Refactoring JavaScript

Enviado por

Marcus Passos
Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
2K visualizações

Asynchronous Refactoring - Refactoring JavaScript

Enviado por

Marcus Passos
Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 29

Capítulo 10.

Refatoração Assíncrona

Neste capítulo, discutiremos a programação assíncrona (também conhecida como “as-


síncrona”)em JavaScript, cobrindoos seguintes tópicos:

Por que assíncrono?


Consertando a “pirâmide da desgraça”
Testando código assíncrono
Promessas

Por que assíncrono?

Antes de entrarmos em como melhorar o JavaScript assíncrono por meio da refatora-


ção, vale a pena discutir por que precisamos dele.Por que não devemos simplesmente
usar um estilo síncrono “mais simples” e não nos preocuparmos com o assíncrono?

Como uma preocupação prática, queremos que nossosprogramas a serem executados.


Apesar de nosso foco neste livro ser interfaces em vez de desempenho, há outro pro-
blema, mesmo que pensássemos que não havia problema em suspender todo o nosso
programa para uma solicitação da Web ou tarefa de processamento de dados que po-
deria levar segundos, minutos ou até mais: às vezes async é a única opção para um
determinado módulo ou biblioteca.

Async tornou-se a norma para muitas APIs.Por exemplo, um vindo deum paradigma
principalmente síncrono (linguagem ou estilo) pode esperar que o http módulo do
nó se comporte assim:

const http = require('http');

const response = http.get('http://refactoringjs.com');

console.log(response.body);

Mas isso será impresso undefined . A razão é que nossa response constante é, na
verdade, nomeada com um pouco de otimismo. O valor de retorno de http.get é um
ClientRequest objeto, não uma resposta. Ele descreve a solicitação, mas não o
resultado.
Há boas razões para usar chamadas HTTP remotas assíncronas. Se nossa chamada re-
mota fosse síncrona, ela necessariamente “pararia o mundo” (STW) e manteria nosso
programa esperando. Ainda assim, quando olhamos para a alternativa imediata, esse
compromisso pode parecer frustrantemente complexo:

http.get('http://refactoringjs.com', (result) => {

  result.on('data', (chunk) => {

    console.log(chunk.toString());

  });

});

P O R Q U E TO S T R I N G ( ) ?

chunk é um Buffer dos personagens. Tente deixar o toString() , e você verá algo como
<Buffer 3c 21 44 4f 43 ... > .

Este é claramente um processo mais complicado do que nossa ideia de como uma API
HTTP síncrona funcionaria. Ele força não apenas o estilo assíncrono, mas também o
paradigma funcional.Temos duas funções assíncronas internas. Se você tentar se ape-
gar ao uso de codificação de estilo síncrono além dessa chamada inicial, sua frustra-
ção não vai parar:

let theResult = [];

http.get('http://refactoringjs.com', (result) => {

  result.on('data', (chunk) => {

    theResult.push(chunk.toString());

  });

});

console.log(theResult);

Agora podemos obter uma matriz dos pedaços da resposta, certo? Não. Isso imprime
uma matriz vazia: [] .

A razão é que a http.get função retorna imediatamente e console.log é avaliada


antes que os pedaços sejam enviados para a matriz no retorno de chamada. Em ou-
tras palavras:

http.get('http://refactoringjs.com', (result) => {

  result.on('data', (chunk) => {

    console.log('this prints after (also twice)');

  });

});

console.log('this prints first');

A última linha é impressa antes que a função mais interna tenha a chance de executar
a terceira linha (aliás, ela imprime essa instrução de log duas vezes). Então, se é ape-
nas uma questão de esperar , e queremos fazer algo com os pedaços, devemos ser ca-
pazes de esperar, certo?Mas quanto tempo esperamos? 500 milissegundos é tempo
suficiente?

let theResult = [];

http.get('http://refactoringjs.com', (result) => {

  result.on('data', (chunk) => {

    theResult.push(chunk.toString());

  });

});

setTimeout(function(){console.log(theResult)}, 500);

É difícil dizer. Usando essa abordagem, podemos acabar com um array vazio ou um
array com um ou mais elementos. Se realmente queremos ter certeza de que os dados
estão no lugar antes de registrá-los (ou fazer qualquer outra coisa com eles), acabare-
mos esperando demais e amarrando nosso programa também. Se esperarmos muito
pouco, perderemos alguns dados. Portanto, esta solução não é muito boa. Não só é im-
previsível, mas envolve definir o estado através de um efeito colateral.
SETTIMEOUT E O LOOP DE EVENTOS

Vale ressaltar que setTimeout(myFunction, 300) nãonecessariamenteexecutar


myFunction após 300 milissegundos. O que ele faz é primeiro retornar (no nó, se
você atribuir com x = setTimeout(myFunction, 300) , verá que ele retorna um
Timeout objeto) e, em seguida, adicionar a função ao loop de eventos a ser executado
após 300 milissegundos.

Há duas questões a ter em mente com este tipo de situação. Primeiro, o loop de even-
tos ficará preso fazendo outra coisa em 300 milissegundos? Pode ser.

Segundo, o código é executado imediatamente quando é dado um tempo limite de 0


milissegundos? Em outras palavras, o que é executado primeiro?

setTimeout(() => {console.log('the chicken')}, 0);

console.log('the egg');

Neste caso, "the egg" será impresso primeiro.

O que dizer disso?

setTimeout(() => {console.log('the chicken')}, 2);

setTimeout(() => {console.log('the chicken 2')}, 0);

setTimeout(() => {console.log('the chicken 3')}, 1);

setTimeout(() => {console.log('the chicken 4')}, 1);

setTimeout(() => {console.log('the chicken 5')}, 1);

setTimeout(() => {console.log('the chicken 6')}, 1);

setTimeout(() => {console.log('the chicken 7')}, 0);

setTimeout(() => {console.log('the chicken 8')}, 2);

console.log('the egg');

O ovo vence novamente, mas as galinhas estão por toda parte. Tente executá-lo em di-
ferentes consoles do navegador e no node. No momento da redação deste artigo, a or-
dem no Chrome e no Firefox é diferente, mas consistente. A ordem no nó varia de
execução para execução.

Com os problemas das setTimeout abordagens, parece que é melhor voltarmos ao


nosso primeiro método assíncrono (o trecho de código diretamente antes de “ Por que
toString()? ”), mas essa é realmente a melhor maneira de escrevê-lo?
Consertando a Pirâmide da Perdição

Se você não está familiarizadocom o termo “pirâmide da desgraça” ouo "inferno de


retorno de chamada", frequentemente relacionado, ambos podem se referir ao
códigona seguinte forma:

levelOne(function(){

levelTwo(function(){

levelThree(function(){

levelFour(function(){

// some code here

     });

});

});    

});

A “pirâmide da desgraça” refere-se à forma em que o código se arrasta para a direita


com muitos níveis de recuo. “Callback hell” é menos sobre a forma do código e mais
sobre uma descrição do código que segue muitas camadas de funções de callback.

Extraindo funções em um objeto contendo

Vamos voltar ao código da última seção. Claramente, vamos querer alguma forma as-
síncrona aqui, mas como gerenciamos a complexidade e o aninhamento necessários
com algo assim?

const http = require('http');

http.get('http://refactoringjs.com', (result) => {

  result.on('data', (chunk) => {

    console.log(chunk.toString());

  });

});

Observe que isso pode ser muito mais complicado com muito mais níveis de aninha-
mento. Este é o inferno de retorno e a pirâmide da destruição em ação. Não há uma
linha estrita para quanto recuo constitui uma pirâmide de destruição ou quantos re-
tornos de chamada colocam você no “inferno”.

Já temos uma estratégia para isso das partes anteriores do livro. Nós simplesmente
desanonimizamos e extraímos uma função como esta:
const http = require('http');

function printBody(chunk){

  console.log(chunk.toString());

};

http.get('http://refactoringjs.com', (result) => {

  result.on('data', printBody);

});

E poderíamos até nomear e extrair outro:

const http = require('http');

function printBody(chunk){

  console.log(chunk.toString());

};

function getResults(result){

  result.on('data', printBody);

};

http.get('http://refactoringjs.com', getResults);

Agora ficamos com duas funções nomeadas e a última linha como um pedaçode cli-
ente ou código  de chamada .

Porqueesta é uma API de streaming, entregando “pedaços” de dados em vez de todo o


corpo HTML de uma só vez, nosso código atualmente colocaria uma quebra de linha
entre os pedaços. Vamos evitar isso com um array para capturar os resultados:

const http = require('http');

let bodyArray = [];

const saveBody = function(chunk){

  bodyArray.push(chunk);

};

const printBody = function(){

  console.log(bodyArray.join(''))

};

const getResults = function(result){

  result.on('data', saveBody);

  result.on('end', printBody);

};

http.get('http://refactoringjs.com', getResults);

Observe que tivemos que adicionar um novo manipulador de eventos para o


'end' evento e não precisamos mais dele toString porque nossa join função é in-
teligente o suficiente para forçar os buffers em uma string.

Agora que extraímos nossas funções e estamos imprimindo corretamente, podemos


ficar tentados a alterar ainda mais o código movendo isso para um objeto, exportando
um módulo e definindo nossa interface pública por alguma combinação de funções
pseudoprivadas (prefixadas com um sublinhado), classes, funções de fábrica ou fun-
ções de construtor. Dependendo do que você gostou nos capítulos anteriores (especi-
almente os Capítulos 5 , 6 e 7 ) e seu próprio estilo, qualquer uma dessas opções pode
parecer exagerada ou prudente.

Se você decidir mover as coisas para um objeto, uma coisa a ter em mente é a facili-
dade com que o this contexto será descartado:

const http = require('http');

const getBody = {

  bodyArray: [],

  saveBody: function(chunk){

    this.bodyArray.push(chunk);

  },

  printBody: function(){

    console.log(this.bodyArray.join(''))

  },

  getResult: function(result){

    result.on('data', this.saveBody);

    result.on('end', this.printBody);

  }

};

http.get('http://refactoringjs.com', getBody.getResult);

Este código levará ao seguinte erro:

TypeError: o argumento "ouvinte" deve ser uma função

Isso significa que na última linha, getBody.getResult não é uma função. Alterar
essa última linha nos leva um pouco mais longe:
http.get('http://refactoringjs.com', getBody.getResult.bind(getBody));

Mas ainda recebemos um erro ao enviar para o bodyArray :

TypeError: Cannot read property 'push' of undefined

Para fazer tudo passarcorretamente para this os retornos de chamada, precisare-


mos do nosso código bind this para oretornos de chamadados eventos
getResult também:

const http = require('http');

const getBody = {

  bodyArray: [],

  saveBody: function(chunk){

    this.bodyArray.push(chunk);

  },

  printBody: function(){

    console.log(this.bodyArray.join(''))

  },

  getResult: function(result){

    result.on('data', this.saveBody.bind(this));

    result.on('end', this.printBody.bind(this));

  }

};

http.get('http://refactoringjs.com', getBody.getResult.bind(getBody));

Vale a pena colocar isso em um objeto? Vale a pena desativar o que era uma pirâmide
de destruição bastante rasa? Eliminamos o inferno de callback? Talvez não, mas é
bom ter opções. Vamos ficar com este formulário para iniciar a próxima seção.

Antes de prosseguir, porém, há duas coisas críticas para observarmos.

Primeiro, contando com retornos de chamada para fazer o trabalho real do nosso pro-
grama, também contamos com os efeitos colaterais. Saímos do mundo simples de va-
lores de retorno. Não estamos apenas retornando valores de funções. Estamos execu-
tando-os (e retornando imediatamente sem nada de valor ) , mas os retornos de cha-
mada serão executados em algum momento . Nossa base fundamental para alcançar a
confiança depende de saber o que está acontecendo em nosso código, e a assíncrona
em JavaScript, como a conhecemos até agora, mina completamente isso.
Segundo, e relacionado ao primeiro ponto, não temos testes! Mas o que testaríamos
afinal? Vamos começar com nossas antigas suposições sobre como o teste funciona e
tentar testar algum valor conhecido. Sabemos que depois que a função for executada,
bodyArray ela deverá conter alguns dados. Em outras palavras, seu comprimento
não deve ser igual a zero.

Testando nosso programa assíncrono

Com isso em mente, vamos trabalhar como testebibliotecado Capítulo 9 chamado fita.
É um pouco mais simples que o mocha, e você pode executá-lo apenas executando .
Você pode instalá-lo com . node whatever-you-call-the-file.js npm install
tape

O seguinte teste falhará:

const http = require('http');

const getBody = {

...

const test = require('tape');

test('our async routine', function(assert){

  http.get('http://refactoringjs.com',

           getBody.getResult.bind(getBody));

  assert.notEqual(getBody.bodyArray.length, 0);

assert.end();

});

Por quê? Por ser executado antes bodyArray tem chance de ser atualizado!

Você pode instintivamente querer rastejar de volta para um mundo síncrono confor-
tável com uma atualização para o teste como esta:

test('our async routine', function(assert){

  http.get('http://refactoringjs.com',

           getBody.getResult.bind(getBody));

  setTimeout(() => {

    assert.notEqual(getBody.bodyArray.length, 0);

    assert.end();

  }, 3000);

});

Aqui temos um teste de aprovação, mas leva 3 segundos para executar.

Então, como podemos adiar nossa afirmação até que bodyArray seja preenchido?

Como estamos testando um efeito colateral e nosso código não é muito “amigável para
retorno de chamada”, ficamos presos a setTimeout menos que reescrevamos o có-
digo ou adicionemos algumas maquinações estranhas aos nossos testes. Em um caso
ideal, printBody receberia um retorno de chamada que seria executado para indicar
que tudo está pronto.

Trocando uma ferramenta cega por outra, podemos eliminar nossa dependência
setTimeout substituindo a função existente que indica quando as coisas são feitas:

test('our async routine', function (assert) {

  getBody.printBody = function(){

    assert.notEqual(getBody.bodyArray.length, 0);

    assert.end();

  }

  http.get('http://refactoringjs.com',

getBody.getResult.bind(getBody));

});

Isso pode parecer ultrajante, por algumas razões. Primeiro, ele sobrescreve uma fun-
ção que podemos querer testar mais tarde (teremos que restaurar a implementação
original?). Em segundo lugar, as mudanças na printBody implementação do 's po-
dem levar a alguma complexidade mais tarde. Sem introduzir zombaria ou tentar ali-
mentar um retorno de chamada em cada função e evento, poderíamos fazer um
pouco melhor:

const getBody = {

...

  printBody: function(){

    console.log(this.bodyArray.join(''))

    this.allDone();

  },

  allDone: function(){}

test('our async routine', function (assert) {

  getBody.allDone = function(){

    assert.equal(getBody.bodyArray.length, 2);

    assert.end();

  }

  http.get('http://refactoringjs.com',

getBody.getResult.bind(getBody));

});

Aqui, criamos uma função cuja única responsabilidade é rodar quando


printBody roda. Como não há implementação padrão, sobrescrevê-la para o teste
não é grande coisa. Só precisaremos redefini-lo em testes futuros. Aqui está um teste
adicional que garante que definir o bodyArray para [] permite uma lousa limpa:

test('our async routine', function (assert) {

  getBody.allDone = function(){

    assert.equal(getBody.bodyArray.length, 2);

    assert.end();

  }

  http.get('http://refactoringjs.com',

getBody.getResult.bind(getBody));

});

test('our async routine two', function (assert) {

getBody.bodyArray = [];

  getBody.allDone = function(){ };

  http.get('http://refactoringjs.com',

getBody.getResult.bind(getBody));

  assert.equal(getBody.bodyArray.length, 0);

  assert.end();

});

Considerações de teste adicionais

Considerando  que também precisamos redefinir nosso bodyArray para um array


vazio (e reverter quaisquer outros efeitos colaterais, como apareceriam em um banco
de dados), uma etapa adicional de manutenção não deve nos incomodar muito. Pode-
mos até refatorar essas etapas em funções simples :

function setup(){

  getBody.bodyArray = [];

function teardown(){

  getBody.allDone = function(){ };

test('our async routine', function (assert) {

  setup();

  getBody.allDone = function(){

    assert.equal(getBody.bodyArray.length, 2);

    teardown();

    assert.end();

  }

  http.get('http://refactoringjs.com',

getBody.getResult.bind(getBody));

});

test('our async routine two', function (assert) {

  setup();

  http.get('http://refactoringjs.com',

getBody.getResult.bind(getBody));

  assert.equal(getBody.bodyArray.length, 0);

  teardown();

  assert.end();

});

Observe que o mocha e outras estruturas mais completas tentam lidar com a configu-
ração e a desmontagem em seu nome. Eles funcionam bem na maioria das vezes, mas
ter funções setup e explícitas teardown (como no último exemplo) lhe dá mais
controle.

T E S TA R PA R A L E L I Z A Ç Ã O

Nenhuma estrutura irá salvá-lo de testes executados em paralelo e sobrecarregar o estado com-
partilhado.A solução para isso é executar testes que compartilham o estado em série (como um
arquivo de teste de fita fará). E para aspectos díspares do código, dividi-los em módulos e dar a
cada um sua própria chance de executar de forma independente e em paralelo ainda permitirá
que você tenha execuções de teste paralelas e rápidas.

Arquitetonicamente, dividir seu código em módulos é provavelmente o que você queria fazer
de qualquer maneira, certo?

Se isso soa como muito trabalho, vá com mocha ou outra coisa que lide com a
configuração/desmontagem. Mas não se surpreenda se você ainda tiver um problema de para-
lelização ocasional (provavelmente resultando em falhas de teste).

Vamos cuidar dissoreatribuição de funçãousando a biblioteca testdouble ( npm


install testdouble ):
const testDouble = require('testdouble');

function setup(){

  getBody.bodyArray = [];

function teardown(){

  getBody.allDone = function(){ };

test('our async routine', function (assert) {

  getBody.allDone = testDouble.function();

  testDouble.when(getBody.allDone()).thenDo(function(){

    assert.notEqual(getBody.bodyArray.length, 0)

    assert.end()

  });

  http.get('http://refactoringjs.com',

getBody.getResult.bind(getBody));

});

Quando fazemos um teste duplo como este para nossa função (também podemos fa-
zer isso para objetos inteiros), nosso código agora pode apenas “falsificar” a chamada
para allDone . Mais tipicamente, doubles são usados ​para evitar a execução de ope-
rações caras ou lentas (como chamar uma API externa), mas tenha cuidado ao usar
muito essa técnica, pois é possível falsificar tudo, o que resulta em testes inúteis. Uma
coisa a notar é o quão conveniente teardown é o nosso. É fácil reatribuir essa função
vazia, mas se estivéssemos criando duplicatas de mais funções (zombaria, stub, espio-
nagem, etc.), teardown isso poderia ficar bem complicado.

Que tal isso para o isolamento?

function setup(){

  return Object.create(getBody);

};

test('our async routine', function (assert) {

  const newBody = setup();

  newBody.allDone = testDouble.function();

  testDouble.when(newBody.allDone()).thenDo(function(){

    assert.notEqual(newBody.bodyArray.length, 0)

    assert.end()

  });

  http.get('http://refactoringjs.com',

newBody.getResult.bind(newBody));

});

Em vez de ter que redefinir nosso objeto, podemos simplesmente usar um novo com
nossas execuções de teste. Nossa setup função pode não ser específica o suficiente
para cada situação, mas é perfeita para isso.

T E S TA M O S O S U F I C I E N T E ?

Dependendo da nossa confiança, podemos sempre adicionar mais testes. Nesse caso, podería-
mos ter optado por retornar a string HTML printBody e testá-la (provavelmente com uma re-
gex em vez de uma correspondência completa). Poderíamos ter feito um duplo para esta
chamada:

result.on('data', this.saveBody.bind(this));

E fez sempre produzir um fragmento HTML simples.

Além disso, poderíamos testar o fato de que uma função foi chamada ou que foi chamada e não
produziu um erro .

Em muitos códigos assíncronos, os valores de retorno não são tão interessantes (ou geradores
de confiança) quanto saber quais funções foram chamadas e quais outros efeitos colaterais
ocorreram.

Nesta seção, criamos objetos e exploramos algumas opções de teste leves para código
assíncrono. Você pode se inclinar para ferramentas mais pesadas como mocha (como
usamos anteriormente) para testes e Sinon.JS (que não analisamos) para duplas. Ou
você pode experimentar ferramentas mais simples, como fita e testdouble.Você pode
até querer apenas raspar com setTimeout e assert ou wish declarações de tempos
em tempos.

Conforme discutimos no Capítulo 3 , você tem muitas opções para ferramentas e tes-
tes. Se algo parecer excessivamente complexo ou não fizer o que você precisa, você
sempre pode diminuir ou aumentar conforme necessário.Flexibilidade e clareza são
mais importantes do que bater em um parafuso com um martelo até que ele atinja a
parede.

Retornos de chamada e testes


Da última seção, temos uma nova abordagem para consertar a pirâmide da destrui-
ção, mas isso não nos tira do inferno de retorno de chamada.Na verdade, ao nomear e
extrair funções, podemos reduzir a clareza do nosso código em alguns casos. Em vez
de os retornos de chamada serem aninhados, eles podem ser espalhados pelo arquivo
ou por vários arquivos.

Criar um objeto ajudou a manter os retornos de chamada organizados, mas essa não é
nossa única opção. Parte do que nos levou a essa solução foi a utilidade de ter um con-
têiner para armazenar nosso array de efeitos colaterais.Tivemos que fazer alguma
agregação com base na natureza de transmissão/emissão de eventos da http biblio-
teca do nó e nosso desejo de imprimir todo o corpo HTML de uma só vez. Se estivésse-
mos adicionando algo a uma página ou salvando um arquivo, poderíamos considerar
permitir que o arquivo ou DOM seja o próprio agregado e apenas passar os resultados
do fluxo para ele em vez de colocá-los em um formato intermediário (o array).

Vimos que passar uma função (retorno de chamada) para outra função é útil para
programação assíncrona, mas muda completamente a forma como trabalhamos nas
partes anteriores deste livro. Em vez de retornar algo valioso e agir sobre isso, esta-
mos deixando a função interna tomar as decisões (e sua função interna [e sua função
interna]). Esta é uma propriedadedo estilo de passagem de continuação (CPS)chamado
de inversão de controle (IoC) e, embora útil, tem algumas desvantagens:

É confuso. Você tem que pensar para trás até se acostumar.


Isso torna as assinaturas de funções complexas. Em vez de parâmetros de função
atuando como “entradas”, eles agora também podem ser responsáveis ​pelas
saídas.
O inferno de retorno de chamada e a pirâmide da destruição são prováveis ​sem or-
ganizar o código em objetos ou outros contêineres de alto nível.
O tratamento de erros é mais complicado.

Além disso, o código assíncrono em geral é difícil:

Isso torna o teste mais difícil (embora parte disso seja apenas a natureza da pro-
gramação assíncrona).
É difícil misturar com código síncrono.
Os valores de retorno provavelmente não são mais importantes em toda a sequên-
cia de retornos de chamada. Isso nos faz confiar no teste de argumentos de retorno
de chamada para determinar valores intermediários.

CPS e IoC básicos


Vejamos um exemplo dos maisuso básico de callbacks em uma função, simplesmente
para veressa inversão do controle em ação. Nem precisa ser assíncrono. Aqui está um
exemplo de uma versão sem retorno de chamada (também conhecida como “estilo di-
reto”) de nossa função:

function addOne(addend){

console.log(addend + 1);

};

addOne(2);

E quando usamos callbacks para fazer a mesma coisa:

function two(callback){

  callback(2);

};

two((addend) => console.log(addend + 1));

O peso do algoritmo está agora no retorno de chamada, em vez da two função, que
simplesmente abre mão do controle e passa a 2 para o retorno de chamada. Como fi-
zemos antes, podemos nomear e extrair a função anônima, dando-nos isto:

function two(callback){

  callback(2);

};

function addOne(addend){

  console.log(addend + 1);

};

two(addOne);

O valor da função de chamada ( two ) é que ela fornece uma variável para o retorno
de chamada. Neste caso, isso é tudo o que faz. Se a two função precisasse que seu va-
lor viesse de alguma tarefa de execução mais longa, gostaríamos de uma versão de re-
torno de chamada (CPS). No entanto, porque two pode retornar imediatamente, o es-
tilo direto é bom, se não for preferível.

Vamos adicionar uma three função que precisa funcionar de forma assíncrona:

function three(callback){

  setTimeout(function(){

    callback(3);

    },

  500);

};

three(addOne);

Se tentarmos fazer o mesmo de forma síncrona:

function three(){

  setTimeout(function(){

    return 3

    },

    500);

function addOne(addend){

  console.log(addend + 1);

};

addOne(three());

Acabamos imprimindo NaN (“Not a Number”) porque addOne termina de executar


antes three de ter a chance de retornar. Em outras palavras, estamos tentando adici-
onar 1 ( undefined o addend in addOne ), resultando em NaN . Então, precisaremos
voltar à nossa versão anterior:

function addOne(addend){

  console.log(addend + 1);

};

function three(callback){

  setTimeout(function(){

    callback(3);

    },

  500);

};

three(addOne);

Observe que também poderíamos ter escrito nossa addOne função para receber um
retorno de chamada, assim:

function addOne(addend, callback){

  callback(addend + 1);

};

function three(callback){

  setTimeout(function(){

    callback(3, console.log);

    },

    500);

};

three(addOne);

Vamos ficar com este formulário para os testes.

Teste de estilo de retorno de chamada

O exemplo da última seção pode parecer redundante com nosso uso anterior da
http.get função, mas há quatro razões pelas quais a introduzimos:

A motivação na primeira parte deste capítulo foi a necessidade de trabalhar com


uma biblioteca assíncrona . Neste exemplo, a motivação é que precisamos traba-
lhar com uma função assíncrona (apenas uma).
O exemplo anterior é mais complexo porque o retorno de chamada de get leva a
outros retornos de chamada em várias result.on funções.
Nosso exemplo anterior não usou o CPS por completo. Contamos com um objeto
não local para fazer parte do trabalho sujo.
Um exemplo simples , onde escrevemos tanto a interface quanto o código de imple-
mentação, é necessário porque aumentaremos a complexidade quando introduzir-
mos promessas .

Antes de chegarmos às promessas, precisamos de testes para este código. Anterior-


mente, trapaceamos um pouco confiando em uma variável global para o valor que es-
távamos testando. Poderíamos fazer o mesmo aqui, ou confiar em algum tipo de teste
duplo para console.log verificar se ele é chamado com o argumento correto (uma
abordagem viável para um teste de ponta a ponta), mas não é para onde estamos
indo. Desta vez, tentaremos fazer as coisas um pouco mais assíncronas, contando ape-
nas com os parâmetros resultantes de nossos retornos de chamada. Como addOne é
mais simples, vamos começar por aí:

const test = require('tape');

test('our addOne function', (assert) => {

  addOne(3, (result) => {

    assert.equal(result, 4);

  assert.end();

  });

});

Para fins de teste, estamos basicamente tratando result como se fosse um valor de
retorno. Em vez de testar o valor de retorno, estamos testando o parâmetro que é pas-
sado para o retorno de chamada. Quanto à leitura, poderíamos dizer o seguinte:

1. addOne recebe dois argumentos: um número e um retorno de chamada.


2. Estamos passando um retorno de chamada como o segundo argumento (real) . É
uma função anônima.
3. Essa função anônima tem um parâmetro (formal) que chamamos de result . Es-
tamos declarando a função no teste.
4. Essa função anônima é chamada dentro de addOne , com o argumento ( result )
sendo a adição de 1 e o que for passado como o primeiro argumento para addOne .
5. Testamos esse resultado contra o literal numérico 4 .
6. Terminamos o teste.

Veja como a addOne função e seu teste seriam diferentes se estivéssemos apenas re-
tornando o resultado:

function addOneSync(addend){

  return addend + 1;

};

...

test('our addOneSync function', (assert) => {

  assert.equal(addOneSync(3), 4);

  assert.end();

});

Aqui nós:

1. Passe 3 para a addOne função e obtenha o valor de retorno.


2. Teste esse valor de retorno em relação ao literal numérico 4 .
3. Finalize o teste.

Um desses processos é muito mais simples, mas estamos vivendo em um mundo as-
síncrono a maior parte do tempo. Além disso, como vimos anteriormente, nossa
three função não tem o luxo de ter um analógico síncrono utilizável. Segue nosso
teste:

test('our three function', (assert) => {

  three((result, callback) => {

    assert.equal(result, 3);

    assert.equal(callback, console.log);

  });

  assert.end();

});

A three função recebe apenas um argumento. Essa é uma função, que deixamos anô-
nima. Essa função anônima recebe dois parâmetros, que são fornecidos como argu-
mentos quando a função anônima é chamada dentro de three . Um é o result e o
outro é o callback . Nossos testes confirmam que result é 3 e callback é
console.log .

Se queremos um teste de ponta a ponta, nossa melhor apostaestá usandoa biblioteca


testdouble para ver se console.log é chamada com 4 :

const testDouble = require('testdouble');

test('our end-to-end test', (assert) => {

  testDouble.replace(console, 'log')

  three((result, callback) => {

    addOne(result, callback)

    testDouble.verify(console.log(4));

    testDouble.reset();

    assert.end();

  });

});

Há algumas coisas dignas de nota aqui. Primeiro, a testdouble.replace função


substitui a console.log função por um double que podemos verificar mais tarde
quando chamamos verify . Depois disso, testdouble.reset restaura
console.log ao seu antigo eu. Lembre-se anteriormente quando estávamos falando
sobre a criação de uma teardown função. Poderíamos usar testdouble.reset para
colocar nossos duplos de volta, o que significa que depois de fazermos isso, podería-
mos usar console.log normalmente.

Então, agora que temos testes em vigor, vamos começar a prometer.

Promessas

Se você gosta de escrever JavaScript assíncrono, mas não gosta da bagunça que vem
com a inversão de controle, as promessas são para você.Vamos recapitular o que vi-
mos até agora.No estilo direto, você retorna valores de funções e usa esses valores de
retorno em outras funções.No CPS (estilo de passagem de continuação), você inverte o
controle pelo código de chamada fornecendo (e geralmente definindo inline) um re-
torno de chamada a ser executado pela função que é chamada. Os valores de retorno
tornam-se pouco melhores do que sem sentido e, em vez disso, os argumentos passa-
dos ​do retorno de chamada (um é convencionalmente chamado result ) tornam-se o
foco dos testes e dos retornos de chamada subsequentes.

Embora o uso de callbacks abra a possibilidadede código assíncrono (sem usar algum
tipo de polling), introduzimos alguma complexidade tanto na maneira como estrutu-
ramos nossas funções quanto na maneira como as chamamos.

As promessas mudam essa complexidade para o lado da definição de função, dei-


xando o código de chamada de função com uma API relativamente simples. Para a
maioria das situações, as promessas são uma escolha melhor do que o CPS. E onde
eles não são a escolha certa, o CPS provavelmente também não é. Nesses casos, você
pode estar procurando por manipulação de fluxo, observáveis ​ou algum outro padrão
de alto nível.

A Interface de Promessa Básica

Então, como usamos promessas?Abordaremos sua implementação em breve, mas, por


enquanto, vamos ver como é a interface da promessa:

// promises

four()

.then(addOne)

.then(console.log);

Isso é bem direto. Temos uma função que retorna a 4 (envolvido em uma promessa),
atuado por addOne (que por sua vez retorna uma promessa), que por sua vez é atu-
ado por console.log .

Se estamos procurando compor funções, as promessas são muito mais fáceis de traba-
lhar. Os retornos de chamada nos prendem a nomes de função de codificação (e/ou
literais de função para retornos de chamada) nas declarações de função, passando-os
como parâmetros extras na função ou usando algumas outras alternativas não triviais
e um tanto confusas.
Com promessas, estamos apenas encadeando valores.Temos um valor (embrulhado
em uma promessa) e o then desembrulha esperando (quando necessário) e, em se-
guida, passamos esse valor como parâmetro para a promessa ou função. Para ilustrar
um pouco mais da interface, também poderíamos escrever o formulário 2:

// form 1

four()

.then(addOne)

.then(console.log);

// form 2

four()

.then((valueFromFour) => addOne(valueFromFour))

.then((valueFromAddOne) => console.log(valueFromAddOne));

Nessas formas, temos uma função literal ou referência. Tanto as definições de função
quanto as chamadas de função addOne e console.log acontecem em outros luga-
res. A Forma 1 é preferível quando possível (também conhecida como “sem pontos”,
um estilo que discutiremos mais no próximo capítulo).

Mover do formulário 2 para o formulário 1 tem uma sensação semelhante à nomea-


ção e extração de uma função anônima. Em ambos os casos, as chamadas de função
acontecem em outro lugar, e podem até estar implícitas ou fora de sua base de código
(ou seja, você não poderá “grep” para elas). No caso de passar do formulário 2 para o
formulário 1, no entanto, as definições das funções (junto com seus nomes) já existem,
portanto, precisamos apenas descartar a função de encapsulamento anônima .
A FLEXIBILIDADE DAS PROMESSAS

Se você ainda não tem certeza sobre a utilidade depromessas maisretornos de chamada, dê
uma olhada nisso:

four()

.then(addOne)

.then(addOne)

.then(addOne)

.then(addOne)

.then(addOne)

.then(console.log);

Podemos encadear quantos addOne s quisermos.É basicamente uma interface fluente se você
ignorar as then chamadas e é amigável para assíncronas.Você pode fazer isso com o CPS, mas
está indo para a pirâmide da desgraça (e resultados intermediários difíceis de testar).

Criando e usando promessas

Agora que temos uma boa ideia do porquêpromessas costumam ser uma boa escolha
em vez de retornos de chamada, vamos dar uma olhada em como realmente as
implementamos:

four()

.then(addOne)

.then(console.log);

function addOne(addend){

  return Promise.resolve(addend + 1);

};

function four(){

  return new Promise((resolve, _reject) => {

    setTimeout(() => resolve(4), 500);

  });

};

As três primeiras linhas devem ser muito familiares agora. Então, como funcionam as
novas funções? Pode parecer complexo dentro dos corpos das funções, mas observe
que não estamos mais passando callbacks, o que pode ficar muito confuso. Além
disso, recebemos nossas return declarações de volta!
Infelizmente, o que estamos devolvendo são promessas, que podem parecer difíceis
de entender. Mas eles não são. É como fazer torradas:

1. Você começa com uma torradeira.


2. Você coloca seu pão nele junto com algumas informações sobre como torrar.
3. A torradeira determina quando o pão está suficientemente torrado e o abre.
4. Depois de pronto, você pega sua torrada e consome como achar melhor.

Os mesmos quatro passos são verdadeiros para promessas:

1. Você começa com uma promessa (geralmente criada com new Promise , mas o
exemplo anterior também mostra que você pode criar uma com
Promise.resolve ).
2. Você coloca um valor ou o processo para criar um valor (que pode ser assíncrono)
na promessa.
3. Após um timer ou receber o resultado de alguma função assíncrona, o valor é defi-
nido por resolve(someValue) .
4. Esse valor é retornado em uma promessa. Você o tira da torradeira – er, prometo –
com a then função e consome o valor como achar melhor.

VOCÊ ODEIA METÁFORAS?

Não? Bom. Discutiremos burritos no Capítulo 11 . Eles são como fazer torradas.

Mas uma promessa não é uma estrutura funcional de alto nível como um functor ou
mônada ou similar? Talvez, mas esse é um tópico enorme, e não podemos entrar em
tudo isso neste livro. O Capítulo 11 oferece uma boa introdução à codificação funcio-
nal prática, mas deixaremos a teoria de fora e focaremos no uso de boas interfaces de
codificação.

De volta ao nosso exemplo, nossa addOne função retorna uma promessa criada com
Promise.resolve(addend + 1) . Isso é bom para casos em que precisamos apenas
de um valor, mas usando a new Promise função construtora e fornecendoum re-
torno de chamada (o executor ) que chama resolve ou reject (as duas funções no-
meadas pela assinatura do retorno de chamada do construtor, o executor) oferece
mais flexibilidade.
ALGUMAS CONSIDERAÇÕES SOBRE ENTÃO

Há algo a ser observado sobre nossa addOne função:

function addOne(addend){

  return Promise.resolve(addend + 1);

Funcionaria tão bem se a segunda linha fosse esta:

  return addend + 1;

Por quê? Porque then vai aceitar uma promessa ou uma função (ou duas, na ver-
dade: a primeira para cumprimento e a segunda para rejeição). Tente isto:

four()

.then(() => 6)

.then(console.log);

Neste caso, 6 será impresso. O then retorno de chamada do primeiro joga fora o 4 e
apenas passa adiante 6 .

No entanto, observe que four não pode ser uma função simples retornando um valor
simples, mesmo sem considerar o setTimeout aspecto da mesma. A primeira função
em uma cadeia de promessas deve ser “ then -able”—isto é, retornar um objeto que
suporte a interface. .then(fulfillment, rejection) Dito isso, você pode começar
apenas com uma promessa resolvida:

Promise.resolve()

.then(() => 4)

.then(() => 6)

.then(console.log);

A resolve disponibilizará o valor dentro da then função. A reject função criará


um objeto de promessa rejeitado . Existe uma catch função que vai pegar alguns er-
ros (e deixar passar outros, então tome cuidado). Existem também funções
Promise.all e Promise.race para, respectivamente, retornar quando todas as
promessas forem concluídas e retornar a primeira promessa que for concluída.
De certa forma, é uma API bastante pequena, mas as variações em torno do trata-
mento de erros e da configuração de promessas podem tornar a experiência compli-
cada. Ainda assim, a interface que ele fornece faz o trabalho inicial valer a pena.

Promessas de teste

Para terminar as coisas perfeitamente, vamos vercomo os testesadapte a esta nova


interface:

function addOne(addend){

  return Promise.resolve(addend + 1);

function four(){

  return new Promise((resolve, _reject) => {

    setTimeout(() => resolve(4), 500);

  })

const test = require('tape');

const testdouble = require('testdouble');

test('our addOne function', (assert) => {

  addOne(3).then((result) => {

    assert.equal(result, 4);

    assert.end();

  });

});

test('our four function', (assert) => {

  four().then((result) => {

    assert.equal(result, 4);

    assert.end();

  });

});

test('our end-to-end test', (assert) => {

  testdouble.replace(console, 'log')

  four()

  .then(addOne)

  .then(console.log)

  .then(() => {

    testdouble.verify(console.log(5));

    assert.pass();

    testdouble.reset();

    assert.end();

  }).catch((e) => {

    testdouble.reset();

    console.log(e);

  })

});

Os dois primeiros testes, que são de baixo nível, permanecem relativamente inaltera-
dos. O teste de ponta a ponta mudou bastante. Depois de substituir
console.log para que possamos monitorá-lo, iniciamos a cadeia de promessas com
nossa four função de retorno de promessa. Nós encadeamos nossos addOne e
console.log callbacks com then funções. E então temos outro then , com uma fun-
ção anônima como único argumento. Dentro dessa função anônima, nós verify que
console.log foi chamado com 5 . Depois disso, chamamos assert.pass para que
nossa saída de teste confirme três em vez de dois testes aprovados. Precisamos disso
porque verify não faz parte tape e não produz uma afirmação passageira. Então
fazemos a desmontagem com testdouble.reset e assert.end .

Você pode estar se perguntando o que estamos fazendo com o catch . Bem, infeliz-
mente, depois de substituirmos console.log , nossos erros não imprimirão mais
nada! catch nos permite colocar de console.log volta testdouble.replace an-
tes de imprimir o erro com console.log(e) .
A ALTERAÇÃO DO CÓDIGO DE ESTILO DE RETORNO DE CHAMADA EM PROMESSAS ESTÁ
“REFATORANDO?”

Provavelmente não, a menos que você não esteja preocupado com testes de unidade e
considere sua “interface” como algumas interações de alto nível com o código.O valor
nas promessas é que elas alteram as interfaces, e essas são provavelmente onde você
gostaria que seus testes estivessem.

Então, por que gastar tanto tempo com promessas na refatoração de JavaScript ?

Existem três razões. Primeiro, você provavelmente ouvirá alguém em algum mo-
mento falar sobre “refatorar para usar promessas”. Você saberá que isso realmente
significa suportar novas interfaces e escrever novo código com novos testes. Veja o di-
agrama no Capítulo 5 sobre quais testes escrever ( Figura 5-1 ). A segunda razão é que
saber onde você está no ciclo de teste (testando antes do novo código, aumentando a
cobertura ou refatorando) é a coisa mais importante para se ter em mãos ao desenvol-
ver confiança em uma base de código. Terceiro, há toneladas de coisas legais em Ja-
vaScript (canvas, webvr, webgl, etc.) que tornam possíveis aplicativos de nicho, mas a
programação assíncrona com promessas é cada vez mais importante para desenvol-
vedores de JavaScript de todos os tipos.

Empacotando

Async é um enorme e ativoárea dedesenvolvimento em JavaScript, e nós apenas arra-


nhamos a superfície. Outras coisas que vale a pena explorar são web workers, stre-
ams, observáveis, geradores, async / await e utilitários de promessa/assíncrona não
nativos.

Na maioria das vezes, o uso de qualquer um desses recursos envolverá uma mudança
drástica no código e não a refatoração conforme o definimos. Mas ter um conheci-
mento básico das interfaces envolvidas em seu código, bem como testá-las, é crucial
antes que qualquer refatoração possa ocorrer.

Apesar de “refatorar para usar promessas” não se encaixar muito em nosso conceito
de refatoração, a interface é aquela que devemos preferir (pelo menos até async e
await se tornar mais amplamente disponível), porque gera valores de retorno úteis
em vez de depender da chamada de outras funções (efeitos colaterais ). Da mesma
forma, async e await são interessantes porque nos permitem escrever código de
aparência síncrona apenas adicionando algumas palavras-chave. No entanto, no mo-
mento da redação deste artigo, suas especificações e implementações ainda não fo-
ram realizadas .

Em Design Patterns: Elements of Reusable Object-Oriented Software , o conselho da


Gangue dos Quatro de “codificar para uma interface, não para uma implementação”
deve ser baseado na capacidade de escolher qual interface você deseja. Depois disso,
você pode desenvolver confiança escrevendo testes e refatorando. Aqui, exploramos
mais algumas opções de interfaces.

Apoiar Sair

© 2022 O'REILLY MEDIA, INC.  TERMOS DE SERVIÇO POLÍTICA DE PRIVACIDADE

Você também pode gostar