Tecnologia / Artigos / O Guia Definitivo do Design de Software /
Evite saladas de frutas

Cléber

![frutas para salada](/files/144) *Photo by [engin akyurt](https://unsplash.com/@enginakyurt) on [Unsplash](https://unsplash.com/s/photos/fruit-salad)* --- Um erro comum de desenvolvedores jovens é pensar que podem "cortar caminho" inserindo vários comportamentos distintos numa mesma função baseado nos argumentos passados para esta, sem considerar **o quão miseravelmente horrendo o código fica** e os problemas que isso acarreta. ## Uma situação de exemplo Até o mês passado, determinado projeto tinha uma função `get_user_data(user_id)`. Essa função busca no banco de dados os dados de determinado usuário e retorna-os para o chamador numa estrutura de dados nativa da linguagem. E ela é parte da engrenagem que atende o seguinte esquema GraphQL: ```graphql getUserData(id: String!) { id: String! name: String! email: String! } ``` ## Uma nova demanda Pois bem. Ontem surgiu uma demanda nova: agora precisamos buscar os dados do usuário usando **o endereço de e-mail** como chave. # O desenvolvedor experiente O que um desenvolvedor experiente faria? Ele criaria uma nova *query* com nome bem descritivo (e já aproveitaria para criar um novo tipo): ```graphql getUserDataByEmail(email: String!) : userData type userData { id: String! name: String! email: String! } ``` E se antes havia uma função `get_user_data(user_id)`, agora passaríamos a ter algo assim: ```python get_user_data(**kwargs) ``` (Eu puxo `**kwargs` de Python, como podem ver.) A ideia é que essa função aceite qualquer conjunto chave-valor e use-o para fazer a busca pelos dados do usuário. Dessa forma, `get_user_data({"id": the_informed_id})` e `get_user_data({"email": the_informed_email})` seriam dois usos possíveis. Preferencialmente, duas funções auxiliares seriam criadas, também: ```python get_user_data_by_id(id) get_user_data_by_email(email) ``` Isso para garantir que não estamos expondo para o usuário de nossa aplicação ou de nossa biblioteca uma função "problemática", em que há o "corner case" óbvio no qual nada é passado como parâmetro de busca e temos um comportamento inesperado e absolutamente desconectado do escopo em questão. Repare nisso: o erro "você precisa passa algum argumento" **não tem nada a ver com nada** a respeito de busca de dados em banco. Lembre-se: **é você quem expõe os botões e manivelas da tua aplicação ou biblioteca**. E uma interface bem feita é aquela que diminui ou elimina completamente a possibilidade de o usuário dela (provavelmente seu colega desenvolvedor) fazer alguma besteira. Se você usa uma linguagem tipada, já tem um método óbvio de melhorar sua interface: ```python get_user_data_by_id(id: string) get_user_data_by_email(email: string) ``` Se a linguagem possui "guards" (uma *feature* magnífica!), você também pode obrigar que o `id` conforme-se ao formato padrão de sua aplicação (por exemplo: representando um UUID). # O programador jovem Mas o jovem quer cortar caminho e entregar rápido. E, ao invés de escrever mais código desse jeito claro e conciso, ele decide escrever mais código de maneira obscura e confusa. **Ele decide tornar o código uma salada de frutas**. ## Saladas de frutas Saladas são "misturebas" e código com saladas de frutas é código que trabalha tanto com maçãs quanto com bananas. O código do desenvolvedor experiente lida **ou** com maçãs, **ou** com bananas, mas nunca mistura as coisas. Já o do programador jovem... O que ele decide é que a função `get_user_data` agora aceita tanto `id` quanto `email`. Afinal, colocaram o `if` na linguagem para isso mesmo, não? Veja: ```python get_user_data(id, email) ``` E a primeira versão da implementação é a seguinte: ```python def get_user_data(id, email): if id: query_params = {"id": id} elif email: query_params = {"email": email} return orm.do_the_search(query_params).first() ``` Já o schema do GraphQL passa a ser esse: ```graphql getUserData(id: String, email: String) { id: String! name: String! email: String! } ``` Satisfeito com o resultado, envia o código para *code review*. E aí começa **a dança da salada**. ## Iteração 1: e se não passar nenhum argumento? Um colega mais observador pergunta: e se não for passado `id` nem `email`, o que acontece? Provavelmente o ORM retornará o primeiro dentre **todos** os usuários do sistema, indistintamente. Logo, após escrever um teste para verificar essa situação, o programador jovem tem que alterar sua função: ```python def get_user_data(id, email): if id: query_params = {"id": id} elif email: query_params = {"email": email} else: raise Exception("You must pass an ID or an e-mail address") # Agora vai! return orm.do_the_search(query_params).first() ``` ## Iteração 2: e se passar ambos? Nesta nova tentativa, alguém percebeu que a função pode não somente receber ambos os parâmetros como estes podem ser **distintos**: o ID do usuário Alfa mais o e-mail do usuário Beta. E agora? Agora, mais alteração! ```python def get_user_data(id, email): if id and email: raise Exception("You must pass only one argument") # Agora vai! if id: query_params = {"id": id} elif email: query_params = {"email": email} else: raise Exception("You must pass an ID or an e-mail address") return orm.do_the_search(query_params).first() ``` ## Passou o Pull Request! Mas e agora? Repare que agora nosso schema GraphQL **perdeu completamente a capacidade de informar os clientes sobre como a query deve ser usada**. Agora temos os parâmetros `id` e `email` **opcionais** e não fica claro que tipo de comportamento a função tem: * Se não passar nenhum, retorna `null`? * Eu preciso passar ambos e os dois valores precisam ser do mesmo usuário? * Se passar ambos, mas de usuários distintos, retorna os dados de qual usuário? A clareza da API, essa foi *pras cucuias*! Erros simples, que poderiam ser barrados na *engine* que interpreta as requisições GraphQL, agora precisam necessariamente passar pelo teu código. **Por que escrever código para fazer algo que poderia ter sido feito "de graça"???** O cliente da API, agora, precisa lidar com **erros descontextualizados**. Até então, antes da salada de frutas, você tinha apenas dois tipos de erros: erros de uso da API (que tinha um schema muito claro) e erros fatais ("banco de dados caiu"). E agora temos mais um, que não pode ser classificado como "erro de uso da API" porque esta não está informando direito como deve ser usada (se um parâmetro é obrigatório, deveria ser mostrado como obrigatório). Na prática, é um "*erro por não entender a cabeça do programador*"! # As vantagens do jeito certo ## O schema ```graphql getUserData(id: String!) : userData getUserDataByEmail(email: String!) : userData type userData { id: String! name: String! email: String! } ``` Repare que agora temos um esquema claro, no qual a forma de uso correta é óbvia. Sua API, assim, torna-se fácil de aprender e sem nenhuma surpresa para quem vai usá-la. Ademais, você pode usar o gateway para analisar o tráfego e comparar dados das requisições a cada *query*: qual demora mais, qual tem mais falhas de atendimento, etc. ## O código Curiosamente, o código feito do jeito certo é muito menor: ```python def _get_user_data(**params): return orm.do_the_search(params).first() def get_user_data_by_id(id: str): return _get_user_data(id=id) def get_user_data_by_email(email: str): return _get_user_data(email=email) ``` Além disso, o tratamento de "corner cases" é feito pela linguagem sempre que possível -- em Python a vantagem é pouca, mas linguagens com tipagem estática já conseguem filtrar chamadas com tipos errados e linguagens com *guards* conseguem até verificar o formato dos argumentos. (A função cujo nome começa com *underscore*, em Python, é uma espécie de `private`: o desenvolvedor está dizendo que você não deve usá-la diretamente. A não ser que você saiba bem o que faz, claro, porque somos todos adultos.) E eu nem me dei ao trabalho de escrever os tratamentos de `if not id` ou `if not email` (ou seja: se `id` ou `email` forem valores "falsy"), **porque o schema já me dá essa garantia**. Se estou trabalhando em um microsserviço com escopo bem delimitado, pode ser uma política válida simplesmente confiar na *engine* de tratamento de GraphQL, que já vai barrar requisições que não se conformem à nossa definição **clara** de API. # Resumo * Não tente economizar fazendo saladas. No fim do dia, você escreverá mais código e o resultado será problemático.

Curti

39 visitantes curtiram esse Item.

Anterior: Artigos / XFCE: quase como o Lada | Próximo: Teste tudo