Princípios fundamentais do desenvolvimento de software
Desenvolvimento de software é uma atividade complexa que envolve análise, design, codificação, testes e manutenção. Para lidar com essa complexidade, foram estabelecidos padrões que norteiam a construção de sistemas de qualidade. Neste artigo, vamos abordar cinco desses princípios: DRY, KISS, YAGNI, POLS e CoC.
Antes de tudo: Não se projeta um carro de Fórmula 1 para ir ao supermercado. Princípios são ferramentas de raciocínio, não regras absolutas. Aplicá-los sem julgamento é tão nocivo quanto não aplicá-los. O desafio está em entender os trade-offs e encontrar o equilíbrio, adapte cada conceito às complexidades e aos desafios do seu projeto.
Equilíbrio e Tensão
Nenhum princípio opera no vácuo. A competência no desenvolvimento de software nasce da habilidade de lidar com os conflitos que existem entre eles.
-
DRY vs. YAGNI: Uma tentativa de aplicar DRY de forma prematura, criando uma abstração para duas funcionalidades que parecem iguais, viola diretamente o YAGNI. Sua bússola é: a duplicação acidental é muito mais barata de corrigir do que a abstração errada.
-
KISS vs. CoC: Frameworks que adotam “Convention over Configuration” (CoC) oferecem uma simplicidade (KISS) incrível para quem já domina suas convenções. No entanto, a mágica por trás pode parecer complexa e violar o KISS para um iniciante. A simplicidade, aqui, é uma consequência da complexidade abstraída pelo framework.
Manter esses conflitos em mente transforma os princípios de uma lista de verificação em um framework para a tomada de decisões.
DRY (“Don’t Repeat Yourself”)
Não se repita-se.
Estabelece uma representação única para cada regra de negócio no sistema. No entanto, seu conceito é frequentemente mal interpretado. DRY não é sobre eliminar a repetição de código, é sobre eliminar a repetição de conhecimento.
Imagine um sistema com duas validações de endereço, uma para faturamento e outra para entrega.
Talvez te pareça uma boa prática criar uma função ValidarEndereco() genérica, certo?
Errado, ao unir faturamento e entrega em uma função ValidarEndereco() genérica, você otimiza o código, mas vitimiza o domínio. Esta falsa aplicação de DRY cria acoplamento indevido. A consequência é que requisitos que mudam por razões diferentes (ex: regra fiscal vs. regra logística) ficam presos na mesma função.
Em vez de secar tudo numa função genérica, preserve o sentido do domínio, mantenha validadores separados (ValidarEnderecoDeCobranca e validarEnderecoDeEntrega) e compartilhe apenas utilidades neutras (ex.: normalizar CEP). Regras fiscais vivem no validador de faturamento, regras logísticas no de entrega. Se um dia mudarem por motivos distintos (e vão), você altera cada uma sem cascatas de ifs nem efeitos colaterais.
Duplicar implementação é aceitável quando representa conhecimentos diferentes. Escolha coesão do domínio em vez de economia de linhas.
KISS (“Keep It Simple, Stupid”)
Mantenha isso estupidamente simples!
Toda complexidade desnecessária é um débito técnico que será pago com juros na manutenção. Devemos priorizar designs simples e diretos para garantir que o sistema possa ser entendido e evoluído.
A simplicidade não significa menos linhas de código. Prefira um código verboso, mas fácil de entender, a um código enxuto e difícil de depurar. Como Martin Fowler disse: “Qualquer um pode escrever um código que um computador entenda. Bons programadores escrevem código que humanos entendem.” Lembre-se, você passará mais tempo lendo do que escrevendo software.
É aqui que muitos erram: o simples não é simplório. A simplicidade intencional requer padrões claros, a falta deles não é simplicidade, é apenas caos.
O desafio é aplicar padrões sem cair no over-engineering. Aplicar um zoológico de patterns complexos no dia 0 é exatamente o tipo de complexidade desnecessária que o KISS tenta evitar.
A solução é usar a evolução guiada pela dor real, comece com o mínimo necessário e aplique padrões mais robustos apenas quando a complexidade justificar.
YAGNI (“You Ain’t Gonna Need It”)
Você (ainda) não vai precisar disso.
Você é desenvolvedor, não trading, então não faça especulação. Não se deve implementar funcionalidades ou abstrações que não são exigidas por uma necessidade presente e concreta. O custo de manter uma funcionalidade desnecessária é pago todos os dias, enquanto o custo de adicioná-la quando for realmente necessária é pago apenas uma vez.
O Perigo das abstrações prematuras
Uma das violações mais comuns do YAGNI é a criação de abstrações para cenários hipotéticos.
Exemplo: Cadastro de Usuário
Dado um novo usuário, Quando ele preenche seu email e senha e clica em “Registrar”, Então uma conta deve ser criada no banco de dados PostgreSQL.
A especificação é clara sobre a tecnologia. Mesmo assim, uma equipe pode decidir: “Vamos criar uma camada de abstração genérica (Repository Pattern) para que, se um dia precisarmos trocar o PostgreSQL pelo MongoDB, seja mais fácil.”
Isso é uma perfeita violação do YAGNI. Não há nenhuma especificação que peça suporte a múltiplos bancos de dados. A equipe está pagando hoje (com maior complexidade, mais código, mais testes) por um benefício futuro que provavelmente nunca se materializará. Pior, muitas vezes, essa abstração malfeita acaba dificultando a utilização de recursos específicos do PostgreSQL e, ironicamente, a troca se torna necessária por causa dos problemas que a própria abstração criou.
POLS (“Principle Of Least Surprise”)
Seu código tem que ser chato.
Também conhecido como Princípio da Menor Surpresa, dita que o comportamento de um componente deve ser intuitivo e previsível, seguindo as convenções e expectativas de quem o utiliza. Um sistema que adere ao POLS é aquele cujo comportamento corresponde exatamente à sua especificação, sem efeitos colaterais ocultos.
Exemplo: Cálculo do total do carrinho
Dado que um carrinho de compras contém itens que somam R$ 120, Quando a função calcularTotal() é chamada, Então o valor 120 deve ser retornado.
Violação do POLS: A função calcularTotal() não só retorna o valor, mas, como um efeito colateral oculto, verifica se o total ultrapassa R$ 100 e, em caso afirmativo, aplica um desconto de 10%, modificando o estado interno do carrinho.
Isso é uma surpresa. O nome da função implica uma consulta (Query) sobre o estado atual, mas ela realiza uma modificação (Command) nesse estado. Quem a utiliza espera apenas ler um valor, sem saber que a primeira chamada da função pode alterar o resultado de chamadas futuras. Um código previsível e “chato” separaria isso em funções distintas com responsabilidades claras: getSubtotal() e aplicarDescontos().
CoC (“Convention over Configuration”)
** Defina convenções e regras em seus projetos.**
Promove a padronização para reduzir o número de decisões que um desenvolvedor precisa tomar. Ao estabelecer convenções bem definidas, a necessidade de configuração manual diminui consideravelmente, acelerando o desenvolvimento e garantindo consistência.
Exemplo: Criar um endpoint que retorna uma lista de produtos
Dado que o sistema precisa expor uma lista de produtos, Quando uma requisição GET for feita para products, Então o método responsável por listar produtos deve ser executado.
- Abordagem por Configuração: O framework não faz suposições. O desenvolvedor precisa conectar manualmente cada peça, especificando o caminho do arquivo do controller e associando-o explicitamente à rota.
// Em um arquivo de rotas (ex: routes.js)
const express = require('express');
const router = express.Router();
// 1. Importação explícita do arquivo do controller
const ProductsController = require('../controllers/ProductsController');
// 2. Mapeamento explícito da rota para a função
router.get('/products', ProductsController.listAll);
module.exports = router;
Isso é uma surpresa em potencial a cada novo arquivo. Onde o controller está? Qual o seu nome exato? ProductsController? product.controller? A falta de uma regra obriga o desenvolvedor a tomar (e documentar) essas microdecisões repetidamente, aumentando a carga cognitiva e a chance de inconsistências.
- Aplicação do CoC: O framework estabelece uma regra: a string ‘ProductsController.index’ será automaticamente resolvida. Ele convenciona que deve procurar por um arquivo chamado ProductsController.ts em uma pasta específica (app/Controllers/Http/) e executar o método index dentro dele. O desenvolvedor apenas segue o mapa.
O AdonisJS oferece ferramentas de linha de comando (ace) para criar arquivos nos lugares certos
node ace make:controller Product
Este comando não apenas cria um arquivo, ele o cria no local que a convenção dita: app/Controllers/Http/ProductsController.ts. O nome é automaticamente convertido para PascalCase e sufixado com Controller.
O arquivo gerado terá uma estrutura como esta:
// app/Controllers/Http/ProductsController.ts
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class ProductsController {
// O método 'index' é, por convenção, usado para listar recursos.
public async index({ response }: HttpContextContract) {
const products = [
{ id: 1, name: 'Teclado Mecânico' },
{ id: 2, name: 'Monitor Ultrawide' },
];
return response.json(products);
}
}
O framework se encarrega de localizar, importar e instanciar a classe para você. Sua única tarefa é seguir a convenção de nomenclatura e estrutura de pastas.
Isto é particularmente interessante em projetos de grande escala, onde a consistência imposta pelas convenções economiza tempo e carga cognitiva.
Bônus: Object Calisthenics
Diferente do que foi apresentado até agora, o Object Calisthenics não é um princípio, mas sim um conjunto de nove exercícios. Seu principal valor não está em ser seguido cegamente, mas em provocar a reflexão sobre hábitos de codificação.
- Um nível de indentação por método: Força a extração de métodos menores e com responsabilidade única.
-
Não use
else: Incentiva o uso de early returns (Guard Clauses) ou polimorfismo, tornando os fluxos mais lineares. -
Envolva primitivos e strings: Transforma dados em Objetos de Valor, agregando comportamento e significado (ex:
Emailem vez deString). -
Coleções de primeira classe: Encapsula uma coleção em sua própria classe, garantindo que as regras de negócio sobre ela (ex: um
Pedidonão pode ter itens duplicados) fiquem protegidas. - Um ponto por linha: Favorece a clareza ao evitar encadeamentos longos e complexos (Lei de Demeter).
- Não abrevie: Nomes descritivos e explícitos melhoram a legibilidade do código.
- Mantenha todas as entidades pequenas: Classes e pacotes devem ser enxutos e focados em uma única responsabilidade.
- Sem classes com mais de duas variáveis de instância: Limita o estado que uma classe gerencia, forçando maior coesão.
- Sem getters/setters/properties: Incentiva o padrão “Tell, Don’t Ask”, em que você manda um objeto fazer algo em vez de pedir seus dados para operar sobre eles externamente.
Princípios são o meio, não o fim. O objetivo é entregar valor de forma sustentável. Busque sempre o equilíbrio entre teoria e prática, usando as especificações e desafios do seu projeto como a bússola que guia suas decisões de design.
