Melhorando seu código: Padrão por ação! Action Pattern - limpo, óbvio e testável! - Parte 1
Desenvolvido por: Ryan Glover
Em primeiro lugar quando li este pattern eu e identifiquei. Pois meio que casa com meu pensamento de dev iniciante :)
Se você conhecer este pattern por outro nome, por favor, coloque nos comentários suas fontes pois quero devora-las :)
A tradução pode não estar muito boa. Mas irei me esforçar. Você pode e deve sugerir melhorias!
Tradução do texto original:
Vamos converter um endpoint de uma API simulada que inscreve novos usuários para o padrão de ação.
Quando comecei a escrever software para web, meu código era uma confusão. Cada projeto foi carregado com arquivos desnecessariamente longos e código comentado, jogado para o lado da estrada como um veículo abandonado. O tema do dia foi: imprevisibilidade.
Sob condições ideais - o caminho feliz - consegui fazer meu código funcionar. Mas o que eu não consegui fazer foi fazer meu código funcionar de maneira consistente. Uma vez, meu código funcionava, depois, na próxima, um anônimo "500 Internal Server Error" me deixava em uma espiral por dias.
Consegui passar despercebido, mas pensar em continuar respondendo e-mails de clientes que diziam “isso não está funcionando ...” era uma vida que eu não queria levar.
Tirando meu chapéu de iniciante, comecei a ver o que outros programadores mais experientes estavam fazendo. Eu tinha ouvido falar de Bob “Tio Bob” Martin de passagem, eventualmente descobrindo sua série Código limpo.
Estava preso. Pela primeira vez, ele estava respondendo a perguntas que outras pessoas no meu caminho não tinham.
Minha pergunta principal? “Como organizo código complexo?” No que diz respeito às perguntas, isso foi um novelo de lã, mas ao longo de vários vídeos ele explicou as partes que eu estava perdendo:
-
Usar nomes explícitos que não podem ser confundidos.
-
Quebrar seu código em funções que fazem uma coisa.
-
Usar TDD (desenvolvimento orientado a testes) para orientar seu trabalho.
Eu ainda verde, parte disso fazia sentido e outra parte não.
O outro problema era que a linguagem de escolha de Bob era Java, não JavaScript. Isso significava que eu era capaz de entender o que ele estava dizendo em alto nível, mas na parte pratica ainda estava perplexo.
##Várias iterações depois ...
Eventualmente, o que Bob ensinou começou a ser absorvido. Conforme ganhei experiência, comecei lentamente a organizar meu código em um padrão (apoiado por uma pequena lista de regras):
-
Qualquer código que envolva várias etapas deve ser movido para seu próprio arquivo / módulo.
-
Esse arquivo / módulo deve receber um nome que descreva a que essas etapas conduzem.
-
Cada etapa desse código deve ser uma única função com um nome que descreve exatamente o que ela faz (mesmo que seja mais longo do que preferimos).
-
Se o código falhar, deve ser fácil ver exatamente onde ele falhou, sem muitos passos para trás.
O que começou como um conjunto informal de regras para mim, acabou evoluindo para um padrão concreto.
Depois de anos de iteração e colocando-o à prova em projetos de clientes e pessoais, em 2017 o padrão de ação foi batizado.
Como funcionam as ações...
Para o restante deste tutorial, Vamos converter um endpoint de uma API simulada que inscreve novos usuários para o padrão de ação.
Nossos objetivos:
- Compreender a estrutura de uma ação.
- Aprender a usar JavaScript Promises com ações.
- Encontrar um “porquê” maior para o uso de ações.
- Entender como a escrita de testes é simplificada pelo uso de ações.
Convertendo Nosso Endpoint
Nosso aplicativo, Doodler (uma rede social paga para artistas), lida com suas inscrições por meio de uma API existente baseada no Express. Quando um novo usuário se inscreve no aplicativo, uma solicitação é feita para sua API em https://doodler.fake/api/v1/users/signup.
Nesse ponto de extremidade, ocorrem as seguintes etapas:
- Um novo usuário é criado na coleção de usuários.
- Um novo cliente é criado no Stripe(sistema de pagamentos).
- Um cliente é criado na coleção de clientes.
- Um e-mail de boas-vindas é gerado.
- Uma mensagem de “novo usuário” é enviada ao Slack da empresa.
Juntas, essas cinco etapas representam a ação de inscrever um novo usuário. Como algumas das etapas dependem das etapas anteriores, queremos ter uma maneira de “parar” nosso código se as etapas anteriores falharem. Antes de entrarmos nas ervas daninhas, vamos dar uma olhada no código que temos agora:
/* eslint-disable */
import mongodb from '/path/to/mongodb';
import settings from '/path/to/settings';
import stripe from '/path/to/stripe/api';
import imaginaryEmailService from '/path/to/imaginaryEmailService';
import slackLog from '/path/to/slackLog';
export default {
v1: {
'/users/signup': (request, response) => {
mongodb.connect(settings.mongodb.url, function (error, client) {
const db = client.db('production');
const users = db.collection('users');
const customers = db.collection('customers');
users.insert({ email: request.body.email, password: request.body.password, profile: request.body.profile }, async function (error, insertedUser) {
if (error) {
throw new Error(error);
} else {
const [user] = insertedUser;
const userId = user._id;
const customerOnStripe = await stripe.customers.create({
email: request.body.email,
});
customers.insert({ userId, stripeCustomerId: customerOnStripe.id }, async function (error, insertedCustomer) {
if (error) {
throw new Error(error);
} else {
imaginaryEmailService.send({ to: request.body.email, template: 'welcome' });
slackLog.success({
message: 'New Customer',
metadata: {
emailAddress: request.body.email,
},
});
response.end();
}
});
}
});
});
},
},
};
Olhando para esse código, supondo que todas as partes funcionem por conta própria, é plausível que esse código funcione. O que é diferente sobre esse código, no entanto, é que ele não é terrivelmente organizado. Ele contém muitas chamadas aninhadas e não muito controle de fluxo (ou seja, se algo falhar, todo o castelo de cartas cai).
É aqui que começamos a andar na ponta dos pés até o abismo do "funciona" vs. "funciona bem". Infelizmente, é um código como esse que leva à perda de muito tempo perseguindo e corrigindo bugs. Não é que o código não funcione, é que ele funciona de forma imprevisível.
Você provavelmente está dizendo "bem, sim, todo código é imprevisível". Você não está errado. Mas, se formos inteligentes, podemos reduzir significativamente a quantidade de imprevisibilidade, dando-nos mais tempo para nos concentrarmos nas coisas divertidas - não em consertar erros do passado (cometidos por nós mesmos ou por alguém de nossa equipe).
Apresentando o padrão de ação
Em primeiro lugar, é importante entender que o padrão de ação é o JavaScript vanilla. É um padrão a seguir, não uma biblioteca ou estrutura a ser implementada. Isso significa que o uso de ações requer um certo nível de disciplina (a maioria dos quais pode ser automatizada por meio de fragmentos em seu IDE).
Para começar a nossa conversão, vamos olhar para uma versão do esqueleto de uma ação e, em seguida, construí-la para lidar com a nossa inscrição de novo usuário.
/* eslint-disable consistent-return */
const actionMethod = (someOption) => {
try {
console.log('Do something with someOption', someOption);
// Perform a single step in your action here.
} catch (exception) {
throw new Error(`[actionName.actionMethod] ${exception.message}`);
}
};
const validateOptions = (options) => {
try {
if (!options) throw new Error('options object is required.');
if (!options.someOption) throw new Error('options.someOption is required.');
} catch (exception) {
throw new Error(`[actionName.validateOptions] ${exception.message}`);
}
};
export default (options) => {
try {
validateOptions(options);
actionMethod(options.someOption);
// Call action methods in sequence here.
} catch (exception) {
throw new Error(`[actionName] ${exception.message}`);
}
};
As ações são projetadas para serem lidas de baixo para cima. Na parte inferior de nosso arquivo, exportamos uma função conhecida como nosso manipulador. Esta função é responsável por chamar todas as outras etapas de nossa ação. Isso nos ajuda a realizar algumas coisas:
- Centralize todas as nossas chamadas para outro código em um só lugar.
- Compartilhe os valores de resposta de cada etapa com outras etapas.
- Delineie claramente a ordem das etapas em nosso código.
- Torne nosso código mais sustentável e extensível, evitando código espaguete aninhado.
Dentro dessa função, a primeira coisa que fazemos é chamar a validateOptions passando do options como argumento, assumido passado para a função de tratamento (ou, o que exportamos de nosso arquivo como nossa ação).
Com validateOptions começamos a ver alguns outros subpadrões de ações aparecerem. Especificamente, o nome da função validateOptions é exatamente o que ela faz
. Não é nem vldOpts nem validateOps, nada que deixa espaço para confusão. Se eu colocasse outro desenvolvedor neste código e perguntasse "o que essa função faz?" ele provavelmente responderia sarcasticamente com "uhh, valida as opções?"
A próxima coisa que você notará é a estrutura de validateOptions. Imediatamente dentro do corpo da função, uma try/catch instrução é adicionada, com a catch pegando exception e throw usando o Error construtor JavaScript.
Note, também, que quando este erro é lançado, dizemos a nós mesmos exatamente onde o erro está acontecendo com [actionName.validateOptions]seguido pela mensagem de erro específica.
No try, fazemos o que nosso código diz: validar nosso options! A lógica aqui é mantida simples de propósito. Se nossa ação requer que options seja passado e requer que propriedades específicas sejam definidas no options, lançamos um erro se elas não existirem. Para deixar isso claro, se chamássemos essa ação agora desta forma:
actionName()// sem passar nada;
Obteríamos o seguinte erro em resposta:
[actionName.validateOptions] options object is required.
Esta é uma grande vantagem para o desenvolvimento. Estamos dizendo a nós mesmos exatamente o que precisamos desde o início, para que possamos pular a roleta do "o que eu esqueci de passar agora?".
Se voltarmos para nossa função de manipulador, veremos que, depois que nossas opções foram validadas com validateOptions, nosso próximo passo é chamar actionMethod, passando options.someOptions.
É aqui que entramos nas etapas reais ou na funcionalidade de nossa ação. Aqui, actionMethod pega options.someOption. Observe que, por ser a segunda etapa chamada em nosso manipulador, ela está definida acima de validateOptions (nossa primeira etapa).
Se olharmos para funcção actionMethod, deveria - propositalmente - parecer muito familiar. Aqui, repetimos o mesmo padrão: dar um nome claro para nossa função, executar nosso código em um bloco try/catch e, se nosso código falhar, throw error dizendo a nós mesmos de que veio [actionName.actionMethod].
Refatorando nossa inscrição
Sentindo-se indeciso? Excelente! É isso que procuramos. Escrever código limpo não deve ser difícil ou excessivamente esotérico.
Agora, vamos começar a refatorar nosso endpoint de inscrição em uma ação. Vamos limpar nosso esqueleto, adicionando algumas verificações legítimas para validateOptions:
const actionMethod = (someOption) => {
try {
console.log('Do something with someOption', someOption);
// Perform a single step in your action here.
} catch (exception) {
throw new Error(`[signup.actionMethod] ${exception.message}`);
}
};
const validateOptions = (options) => {
try {
if (!options) throw new Error('options object is required.');
if (!options.body) throw new Error('options.body is required.');
if (!options.body.email) throw new Error('options.body.email is required.');
if (!options.body.password) throw new Error('options.body.password is required.');
if (!options.body.profile) throw new Error('options.body.profile is required.');
if (!options.response) throw new Error('options.response is required.');
} catch (exception) {
throw new Error(`[signup.validateOptions] ${exception.message}`);
}
};
export default (options) => {
try {
validateOptions(options);
// Call action methods in sequence here.
options.response.end();
} catch (exception) {
throw new Error(`[signup] ${exception.message}`);
}
};
Algumas coisas mudaram. Observe que, em vez de actionName, nossa ação tem um nome: signup.
No interior validateOptions, também definimos algumas expectativas reais. Lembre-se de que em nosso código original, reutilizamos o request.body várias vezes. Aqui, pensamos no futuro e presumimos que apenas passaremos o body da solicitação (a única parte que utilizamos). Também nos certificamos de validar se cada uma das propriedades do body está presente.
Por fim, também queremos validar se o objeto
response de nosso terminal é passado para que possamos responder à solicitação em nossa ação.
Os detalhes disso são em sua maioria arbitrários; o ponto aqui é que estamos garantindo que temos o que precisamos antes de colocá-lo em uso. Isso ajuda a eliminar o inevitável "já passei nisso?", bem como o tempo subsequente desperdiçado na depuração para descobri-la.
OBS do tradutor: usando console.log em N cantos.
Segunda parte > https://www.tabnews.com.br/uriel/melhorando-seu-codigo-padrao-por-acao-action-pattern-limpo-obvio-e-testavel-parte-2
Isto é uma tradução não muito bem feita deste artigo > https://ponyfoo.com/articles/action-pattern-clean-obvious-testable-code