Evite saladas de frutas

Cléber Zavadniak
clebertech
Published in
6 min readAug 21, 2020
Photo by engin akyurt on Unsplash

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.

Você também pode ler este artigo lá no MyNotes.space, onde a série “O Guia Definitivo do Design de Software” é publicada primeiro.

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:

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):

getUserDataByEmail(email: String!) : userDatatype 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:

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:

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:

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:

get_user_data(id, email)

E a primeira versão da implementação é a seguinte:

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:

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:

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!

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

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:

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.

Curtiu? Então assine a minha newsletter e receba conteúdo interessante em pequenos drops direto na sua caixa de e-mails:

--

--