4 Lições Cruciais de Design de Software que Aprendi na Prática (e Como Evitar Erros Comuns)

Se houver duas fontes de verdade, provavelmente uma está errada. E sim, por favor, repita a si mesmo.

Recentemente, construí e projetei um serviço massivo que (finalmente) foi lançado com sucesso no mês passado. Durante o processo de design e implementação, percebi que a lista a seguir de "regras" aparecia repetidamente em vários cenários.

Essas regras são comuns o suficiente para que eu ouso dizer que pelo menos uma delas será útil para algum projeto que qualquer engenheiro de software que estiver lendo isso esteja trabalhando atualmente. Mas se você não puder aplicá-las diretamente agora, espero que esses princípios sirvam como um exercício de reflexão, e fique à vontade para comentar ou questioná-los.

Uma coisa que vale a pena notar é que, claro, cada "princípio" tem seu tempo e lugar. Como sempre, o contexto é necessário. Essas são conclusões às quais geralmente me inclino porque, com frequência, o oposto é o padrão que vejo ao revisar código ou ler documentos de design.

1. Mantenha uma fonte de verdade.
Se houver duas fontes de verdade, uma provavelmente está errada. Se não está errada, ainda não está... mas estará.

Basicamente, se você está tentando manter um estado em dois lugares diferentes dentro do mesmo serviço... simplesmente não faça. É melhor tentar referenciar o mesmo estado sempre que possível. Por exemplo, se você está mantendo um aplicativo frontend e tem um saldo bancário vindo do servidor, já vi muitos bugs de sincronização ao longo do tempo para sempre querer obter esse saldo diretamente do servidor. Se houver algum saldo derivado disso, como "saldo disponível" versus "saldo total" (por exemplo, alguns bancos exigem um saldo mínimo), então o "saldo disponível" deve ser calculado em tempo real, em vez de ser armazenado separadamente. Caso contrário, você terá que atualizar ambos os saldos sempre que uma transação acontecer.

De modo geral, se houver um dado que pode ser derivado de outro valor, então ele deve ser calculado em vez de armazenado. Armazenar esse valor leva a bugs de sincronização. (Sim, sei que isso nem sempre é possível. Haverá sempre outros fatores em jogo, como o custo da derivação. No final do dia, é uma questão de tradeoff.)

2. Sim, por favor, repita-se.
Já ouvimos falar de DRY (Don’t Repeat Yourself - Não se repita) e agora apresento a você PRY (Please Repeat Yourself - Por favor, repita-se).

Muitas vezes vejo código que é quase igual ser abstraído em uma classe "reutilizável". O problema é que essa classe "reutilizável" ganha um método a mais, depois um construtor especial, depois mais alguns métodos, até que se torna um monstro Frankenstein de código que serve a múltiplos propósitos, e o propósito original da abstração não existe mais.

Um pentágono pode ser semelhante a um hexágono, mas ainda há diferenças suficientes para que eles definitivamente não sejam a mesma coisa.

Também sou culpado de gastar muito tempo tentando fazer as coisas reutilizáveis, quando um pouco de duplicação de código funciona perfeitamente bem. (Sim, você precisa escrever mais testes, e isso não resolve a vontade de "refatorar", mas tudo bem.)

3. Não exagere nos mocks.
Mocks. Tenho uma relação de amor e ódio com eles. Minha frase favorita de uma discussão no Reddit sobre este post foi "com mocks, trocamos a fidelidade dos testes pela facilidade de testar."

Mocks são ótimos quando preciso escrever testes unitários rapidamente e não quero mexer com o código "de produção". Mocks não são ótimos quando a produção quebra porque, na verdade, algo que você mockou falhou mais profundamente na pilha, mesmo que essa parte "mais profunda" seja de responsabilidade de outra equipe. Não importa, porque foi o seu serviço que falhou, então é sua responsabilidade corrigir.

Escrever testes é difícil. A linha entre testes unitários e testes de integração é mais borrada do que você imagina. Saber o que mockar ou não é algo subjetivo.

É muito melhor encontrar problemas durante o desenvolvimento do que na produção. Conforme continuo escrevendo software, tento evitar mocks sempre que possível. Testes um pouco mais pesados são completamente aceitáveis quando resultam em muito mais confiabilidade. Se mocks forem realmente necessários, prefiro escrever mais (e talvez até testes redundantes) do que deixar de testar. Mesmo se eu não puder usar uma dependência real em um teste, ainda tento usar outras opções antes de recorrer a mocks, como um servidor local.

O “Testing on the Toilet” do Google tem uma boa observação sobre isso, de 2013. Eles observam que o uso excessivo de mocks causa:

  • Os testes podem ser mais difíceis de entender, porque agora há esse código extra que alguém precisa entender, além do código de produção real.
  • Os testes podem ser mais difíceis de manter, porque você precisa dizer a um mock como se comportar, o que significa que você vazou detalhes de implementação no seu teste.
  • No geral, os testes fornecem menos garantia, pois a confiabilidade do seu software agora só é garantida SE os mocks se comportarem exatamente como suas implementações reais (o que é difícil de garantir e muitas vezes acaba fora de sincronia).

4. Minimize o estado mutável.
Os computadores são MUITO rápidos. No jogo de otimização, é superpopular lançar caching e armazenar tudo em um banco de dados imediatamente. Acho que este é provavelmente o estado final da maioria dos produtos e serviços de software bem-sucedidos. Claro, a maioria dos serviços precisará de algum tipo de estado, mas é importante descobrir o que é realmente necessário em termos de armazenamento, em vez de derivar as informações em tempo real.

No “v1” de algo, descobri que minimizar o estado mutável o máximo possível te leva longe. Isso permite desenvolver mais rápido, porque você não precisa se preocupar com bugs de sincronização, dados conflitantes e estado desatualizado. Também permite desenvolver funcionalidades peça por peça, em vez de introduzir muitas de uma vez só. As máquinas são rápidas o suficiente hoje em dia para que fazer alguns cálculos redundantes seja totalmente aceitável. Se as máquinas supostamente “estão nos substituindo” em breve, então elas podem lidar com algumas unidades extras de cálculos.