A Arte da Simplicidade no Desenvolvimento de Software: DRY, KISS, YAGNI e TDA
"A simplicidade é o último grau de sofisticação." - Leonardo da Vinci Índice Introdução O que são Princípios de Desenvolvimento de Software? Por que seguir Princípios de Desenvolvimento é importante? DRY - Don't Repeat Yourself (Não Se Repita) DRY e o Padrão Factory KISS - Keep It Simple, Stupid (Mantenha Simples, Estúpido) YAGNI - You Aren't Gonna Need It (Você Não Vai Precisar Disso) TDA - Tell, Don't Ask (Diga, Não Pergunte) TDA e a Lei de Demeter Críticas e Limitações dos Princípios Integrando os Princípios em seu Desenvolvimento Conclusão Referências Bibliográficas Introdução No cenário atual do desenvolvimento de software, onde a complexidade dos sistemas cresce exponencialmente, a busca por clareza e simplicidade tornou-se uma necessidade fundamental. Desenvolvedores experientes reconhecem que código complicado não é sinônimo de código sofisticado - pelo contrário, a verdadeira elegância reside na simplicidade. Este artigo explora quatro princípios fundamentais que transformaram o desenvolvimento de software: DRY (Don't Repeat Yourself), KISS (Keep It Simple, Stupid), YAGNI (You Aren't Gonna Need It) e TDA (Tell, Don't Ask). Estes princípios não são apenas técnicas isoladas, mas filosofias de design que, quando aplicadas corretamente, resultam em código mais limpo, manutenível e, paradoxalmente, mais poderoso. O que são Princípios de Desenvolvimento de Software? Princípios de desenvolvimento de software são diretrizes conceituais que orientam as decisões de design e implementação durante o processo de criação de software. Diferentemente de padrões de design específicos ou frameworks, estes princípios transcendem linguagens de programação e paradigmas, servindo como uma bússola para navegação no vasto campo de possibilidades do desenvolvimento de software. Estes princípios representam o conhecimento acumulado ao longo de décadas pela comunidade global de desenvolvimento, capturando lições frequentemente aprendidas da maneira mais difícil: através de projetos malsucedidos, manutenções onerosas e bugs persistentes. Por que seguir Princípios de Desenvolvimento é importante? A importância de seguir princípios de desenvolvimento transcende preferências estéticas de código. Considere as seguintes vantagens: Redução de custos a longo prazo: Sistemas bem projetados são consideravelmente mais econômicos para manter e estender. Escalabilidade: Código que segue princípios sólidos adapta-se melhor ao crescimento da aplicação. Facilidade de colaboração: Princípios compartilhados estabelecem uma linguagem comum entre desenvolvedores. Resiliência: Sistemas bem projetados respondem melhor a mudanças nas especificações. Qualidade perceptível: Usuários finais identificam indiretamente a qualidade do design através da confiabilidade e responsividade do sistema. Examinemos agora os quatro princípios que são o foco deste artigo, explorando sua origem, aplicação e impacto. DRY - Don't Repeat Yourself (Não Se Repita) Origem e História O princípio DRY foi formulado por Andy Hunt e Dave Thomas em seu influente livro "The Pragmatic Programmer", publicado em 1999. A definição original é direta e poderosa: "Todo conhecimento deve ter uma representação única, inequívoca e autoritativa dentro de um sistema." Embora Hunt e Thomas tenham cunhado o termo, o conceito de evitar repetição em código já era uma prática valorizada anteriormente. O mérito deles foi articular e formalizar este conhecimento tácito que já circulava entre desenvolvedores experientes. Por que é importante? A importância do DRY reside em sua capacidade de reduzir significativamente a dívida técnica. Quando o mesmo código ou lógica é repetido em múltiplos lugares, qualquer alteração necessária deve ser implementada em todos esses locais. Inevitavelmente, alguns desses pontos serão esquecidos, resultando em inconsistências e bugs difíceis de rastrear. Além disso, o DRY: Reduz o tamanho do código, facilitando sua compreensão Concentra a lógica em locais específicos, facilitando otimizações Melhora a coesão do código, agrupando funcionalidades relacionadas Minimiza a ocorrência de erros durante atualizações e manutenções Exemplos Práticos em Java Sem aplicar o princípio DRY: @Service public class ProdutoService { @Autowired private ProdutoRepository produtoRepository; public ProdutoResponseDTO buscarProdutoPorId(Long id) { Produto produto = produtoRepository.findById(id) .orElseThrow(() -> new RecursoNaoEncontradoException("Produto não encontrado")); ProdutoResponseDTO responseDTO = new ProdutoResponseDTO(); responseDTO.setId(produto.getId()); responseDTO.setNome(produto.getNome()); responseDTO.setDescricao(produto.getDescricao()); responseDTO.setPreco(produto.getPreco()); responseDTO.setCategoria(produto.getCategoria()); responseDTO.setDisponivel(produto.isDisponivel(

"A simplicidade é o último grau de sofisticação." - Leonardo da Vinci
Índice
- Introdução
- O que são Princípios de Desenvolvimento de Software?
- Por que seguir Princípios de Desenvolvimento é importante?
- DRY - Don't Repeat Yourself (Não Se Repita)
- DRY e o Padrão Factory
- KISS - Keep It Simple, Stupid (Mantenha Simples, Estúpido)
- YAGNI - You Aren't Gonna Need It (Você Não Vai Precisar Disso)
- TDA - Tell, Don't Ask (Diga, Não Pergunte)
- TDA e a Lei de Demeter
- Críticas e Limitações dos Princípios
- Integrando os Princípios em seu Desenvolvimento
- Conclusão
- Referências Bibliográficas
Introdução
No cenário atual do desenvolvimento de software, onde a complexidade dos sistemas cresce exponencialmente, a busca por clareza e simplicidade tornou-se uma necessidade fundamental. Desenvolvedores experientes reconhecem que código complicado não é sinônimo de código sofisticado - pelo contrário, a verdadeira elegância reside na simplicidade.
Este artigo explora quatro princípios fundamentais que transformaram o desenvolvimento de software: DRY (Don't Repeat Yourself), KISS (Keep It Simple, Stupid), YAGNI (You Aren't Gonna Need It) e TDA (Tell, Don't Ask). Estes princípios não são apenas técnicas isoladas, mas filosofias de design que, quando aplicadas corretamente, resultam em código mais limpo, manutenível e, paradoxalmente, mais poderoso.
O que são Princípios de Desenvolvimento de Software?
Princípios de desenvolvimento de software são diretrizes conceituais que orientam as decisões de design e implementação durante o processo de criação de software. Diferentemente de padrões de design específicos ou frameworks, estes princípios transcendem linguagens de programação e paradigmas, servindo como uma bússola para navegação no vasto campo de possibilidades do desenvolvimento de software.
Estes princípios representam o conhecimento acumulado ao longo de décadas pela comunidade global de desenvolvimento, capturando lições frequentemente aprendidas da maneira mais difícil: através de projetos malsucedidos, manutenções onerosas e bugs persistentes.
Por que seguir Princípios de Desenvolvimento é importante?
A importância de seguir princípios de desenvolvimento transcende preferências estéticas de código. Considere as seguintes vantagens:
Redução de custos a longo prazo: Sistemas bem projetados são consideravelmente mais econômicos para manter e estender.
Escalabilidade: Código que segue princípios sólidos adapta-se melhor ao crescimento da aplicação.
Facilidade de colaboração: Princípios compartilhados estabelecem uma linguagem comum entre desenvolvedores.
Resiliência: Sistemas bem projetados respondem melhor a mudanças nas especificações.
Qualidade perceptível: Usuários finais identificam indiretamente a qualidade do design através da confiabilidade e responsividade do sistema.
Examinemos agora os quatro princípios que são o foco deste artigo, explorando sua origem, aplicação e impacto.
DRY - Don't Repeat Yourself (Não Se Repita)
Origem e História
O princípio DRY foi formulado por Andy Hunt e Dave Thomas em seu influente livro "The Pragmatic Programmer", publicado em 1999. A definição original é direta e poderosa: "Todo conhecimento deve ter uma representação única, inequívoca e autoritativa dentro de um sistema."
Embora Hunt e Thomas tenham cunhado o termo, o conceito de evitar repetição em código já era uma prática valorizada anteriormente. O mérito deles foi articular e formalizar este conhecimento tácito que já circulava entre desenvolvedores experientes.
Por que é importante?
A importância do DRY reside em sua capacidade de reduzir significativamente a dívida técnica. Quando o mesmo código ou lógica é repetido em múltiplos lugares, qualquer alteração necessária deve ser implementada em todos esses locais. Inevitavelmente, alguns desses pontos serão esquecidos, resultando em inconsistências e bugs difíceis de rastrear.
Além disso, o DRY:
- Reduz o tamanho do código, facilitando sua compreensão
- Concentra a lógica em locais específicos, facilitando otimizações
- Melhora a coesão do código, agrupando funcionalidades relacionadas
- Minimiza a ocorrência de erros durante atualizações e manutenções
Exemplos Práticos em Java
Sem aplicar o princípio DRY:
@Service
public class ProdutoService {
@Autowired
private ProdutoRepository produtoRepository;
public ProdutoResponseDTO buscarProdutoPorId(Long id) {
Produto produto = produtoRepository.findById(id)
.orElseThrow(() -> new RecursoNaoEncontradoException("Produto não encontrado"));
ProdutoResponseDTO responseDTO = new ProdutoResponseDTO();
responseDTO.setId(produto.getId());
responseDTO.setNome(produto.getNome());
responseDTO.setDescricao(produto.getDescricao());
responseDTO.setPreco(produto.getPreco());
responseDTO.setCategoria(produto.getCategoria());
responseDTO.setDisponivel(produto.isDisponivel());
return responseDTO;
}
public List<ProdutoResponseDTO> buscarTodosProdutos() {
List<Produto> produtos = produtoRepository.findAll();
List<ProdutoResponseDTO> responseDTOs = new ArrayList<>();
for (Produto produto : produtos) {
ProdutoResponseDTO responseDTO = new ProdutoResponseDTO();
responseDTO.setId(produto.getId());
responseDTO.setNome(produto.getNome());
responseDTO.setDescricao(produto.getDescricao());
responseDTO.setPreco(produto.getPreco());
responseDTO.setCategoria(produto.getCategoria());
responseDTO.setDisponivel(produto.isDisponivel());
responseDTOs.add(responseDTO);
}
return responseDTOs;
}
public List<ProdutoResponseDTO> buscarProdutosPorCategoria(String categoria) {
List<Produto> produtos = produtoRepository.findByCategoria(categoria);
List<ProdutoResponseDTO> responseDTOs = new ArrayList<>();
for (Produto produto : produtos) {
ProdutoResponseDTO responseDTO = new ProdutoResponseDTO();
responseDTO.setId(produto.getId());
responseDTO.setNome(produto.getNome());
responseDTO.setDescricao(produto.getDescricao());
responseDTO.setPreco(produto.getPreco());
responseDTO.setCategoria(produto.getCategoria());
responseDTO.setDisponivel(produto.isDisponivel());
responseDTOs.add(responseDTO);
}
return responseDTOs;
}
}
Aplicando o princípio DRY:
@Service
public class ProdutoService {
@Autowired
private ProdutoRepository produtoRepository;
public ProdutoResponseDTO buscarProdutoPorId(Long id) {
Produto produto = produtoRepository.findById(id)
.orElseThrow(() -> new RecursoNaoEncontradoException("Produto não encontrado"));
return converterParaDTO(produto);
}
public List<ProdutoResponseDTO> buscarTodosProdutos() {
List<Produto> produtos = produtoRepository.findAll();
return produtos.stream()
.map(this::converterParaDTO)
.collect(Collectors.toList());
}
public List<ProdutoResponseDTO> buscarProdutosPorCategoria(String categoria) {
List<Produto> produtos = produtoRepository.findByCategoria(categoria);
return produtos.stream()
.map(this::converterParaDTO)
.collect(Collectors.toList());
}
private ProdutoResponseDTO converterParaDTO(Produto produto) {
ProdutoResponseDTO responseDTO = new ProdutoResponseDTO();
responseDTO.setId(produto.getId());
responseDTO.setNome(produto.getNome());
responseDTO.setDescricao(produto.getDescricao());
responseDTO.setPreco(produto.getPreco());
responseDTO.setCategoria(produto.getCategoria());
responseDTO.setDisponivel(produto.isDisponivel());
return responseDTO;
}
}
Na versão DRY, extraímos a lógica de conversão da entidade para DTO em um método separado converterParaDTO()
, que é reutilizado em todos os métodos que precisam dessa funcionalidade. Se a estrutura do DTO mudar ou se novos campos forem adicionados, precisamos alterar apenas esse método, não todos os lugares onde a conversão é feita.
Aplicando o princípio DRY com MapStruct:
Uma maneira ainda mais elegante de aplicar o princípio DRY para mapeamento entre objetos é utilizando a biblioteca MapStruct. Esta abordagem elimina completamente a necessidade de escrever código de mapeamento manual, reduzindo ainda mais a duplicação e o potencial para erros:
// 1. Definir a interface do mapper
@Mapper(componentModel = "spring")
public interface ProdutoMapper {
ProdutoResponseDTO toDTO(Produto produto);
List<ProdutoResponseDTO> toDTOList(List<Produto> produtos);
}
// 2. Refatorar o serviço para usar o mapper
@Service
public class ProdutoService {
@Autowired
private ProdutoRepository produtoRepository;
@Autowired
private ProdutoMapper produtoMapper;
public ProdutoResponseDTO buscarProdutoPorId(Long id) {
Produto produto = produtoRepository.findById(id)
.orElseThrow(() -> new RecursoNaoEncontradoException("Produto não encontrado"));
return produtoMapper.toDTO(produto);
}
public List<ProdutoResponseDTO> buscarTodosProdutos() {
List<Produto> produtos = produtoRepository.findAll();
return produtoMapper.toDTOList(produtos);
}
public List<ProdutoResponseDTO> buscarProdutosPorCategoria(String categoria) {
List<Produto> produtos = produtoRepository.findByCategoria(categoria);
return produtoMapper.toDTOList(produtos);
}
}
O MapStruct gera automaticamente a implementação da interface ProdutoMapper
durante o processo de compilação, eliminando toda a duplicação de código de mapeamento. Se a estrutura do DTO ou da entidade mudar, basta atualizar a interface do mapper e a implementação será regenerada automaticamente.
Esta abordagem oferece várias vantagens sobre o mapeamento manual:
- Elimina completamente o código repetitivo: Não precisamos escrever nenhum código de mapeamento.
- Alta performance: O código gerado pelo MapStruct é muito eficiente, similar ao que escreveríamos manualmente.
- Menos propensão a erros: Se novos campos forem adicionados à entidade ou ao DTO, o compilador gerará avisos se o mapeamento não estiver completo.
- Configurações avançadas: MapStruct oferece suporte a mapeamentos complexos, conversões personalizadas e herança.
Este é um exemplo perfeito da aplicação do princípio DRY levado ao seu máximo potencial, onde eliminamos não apenas a duplicação direta, mas também a necessidade de escrever código repetitivo em primeiro lugar.
DRY e o Padrão Factory
É importante destacar a relação sinérgica entre o princípio DRY e o padrão de design Factory. O padrão Factory é um dos padrões de criação mais utilizados na programação orientada a objetos e apresenta particular afinidade com o princípio DRY.
Quando aplicamos DRY para eliminar duplicação de código relacionado à criação de objetos complexos, frequentemente implementamos alguma variação do padrão Factory. Observe o exemplo com um cenário típico de API REST:
Sem Factory (violando DRY):
@RestController
@RequestMapping("/api/pedidos")
public class PedidoController {
@Autowired
private PedidoService pedidoService;
@PostMapping
public ResponseEntity<PedidoDTO> criarPedido(@RequestBody PedidoRequest request) {
// Criação do cliente manualmente
Cliente cliente = new Cliente();
cliente.setId(request.getClienteId());
cliente.setTipoCliente(TipoCliente.valueOf(request.getTipoCliente()));
if (TipoCliente.PREMIUM.equals(cliente.getTipoCliente())) {
cliente.setLimiteCredito(10000.0);
cliente.setBeneficioFrete(true);
cliente.setPrazoEntregaDias(2);
} else if (TipoCliente.REGULAR.equals(cliente.getTipoCliente())) {
cliente.setLimiteCredito(5000.0);
cliente.setBeneficioFrete(false);
cliente.setPrazoEntregaDias(5);
} else { // NOVO
cliente.setLimiteCredito(1000.0);
cliente.setBeneficioFrete(false);
cliente.setPrazoEntregaDias(7);
}
Pedido pedido = pedidoService.criarPedido(request, cliente);
return ResponseEntity.ok(convertToDTO(pedido));
}
}
@RestController
@RequestMapping("/api/clientes")
public class ClienteController {
@Autowired
private ClienteService clienteService;
@PostMapping
public ResponseEntity<ClienteDTO> cadastrarCliente(@RequestBody ClienteRequest request) {
// Criação do cliente manualmente (duplicação de código)
Cliente cliente = new Cliente();
cliente.setNome(request.getNome());
cliente.setEmail(request.getEmail());
cliente.setTipoCliente(TipoCliente.valueOf(request.getTipoCliente()));
if (TipoCliente.PREMIUM.equals(cliente.getTipoCliente())) {
cliente.setLimiteCredito(10000.0);
cliente.setBeneficioFrete(true);
cliente.setPrazoEntregaDias(2);
} else if (TipoCliente.REGULAR.equals(cliente.getTipoCliente())) {
cliente.setLimiteCredito(5000.0);
cliente.setBeneficioFrete(false);
cliente.setPrazoEntregaDias(5);
} else { // NOVO
cliente.setLimiteCredito(1000.0);
cliente.setBeneficioFrete(false);
cliente.setPrazoEntregaDias(7);
}
Cliente clienteSalvo = clienteService.salvarCliente(cliente);
return ResponseEntity.ok(convertToDTO(clienteSalvo));
}
}
Com Factory (aplicando DRY):
// Factory para criação de clientes
@Component
public class ClienteFactory {
public Cliente criarCliente(Long id, String tipoClienteStr) {
Cliente cliente = new Cliente();
cliente.setId(id);
TipoCliente tipoCliente = TipoCliente.valueOf(tipoClienteStr);
cliente.setTipoCliente(tipoCliente);
// Configuração baseada no tipo de cliente
switch (tipoCliente) {
case PREMIUM:
configurarClientePremium(cliente);
break;
case REGULAR:
configurarClienteRegular(cliente);
break;
case NOVO:
configurarClienteNovo(cliente);
break;
}
return cliente;
}
public Cliente criarCliente(ClienteRequest request) {
Cliente cliente = new Cliente();
cliente.setNome(request.getNome());
cliente.setEmail(request.getEmail());
TipoCliente tipoCliente = TipoCliente.valueOf(request.getTipoCliente());
cliente.setTipoCliente(tipoCliente);
// Configuração baseada no tipo de cliente
switch (tipoCliente) {
case PREMIUM:
configurarClientePremium(cliente);
break;
case REGULAR:
configurarClienteRegular(cliente);
break;
case NOVO:
configurarClienteNovo(cliente);
break;
}
return cliente;
}
private void configurarClientePremium(Cliente cliente) {
cliente.setLimiteCredito(10000.0);
cliente.setBeneficioFrete(true);
cliente.setPrazoEntregaDias(2);
}
private void configurarClienteRegular(Cliente cliente) {
cliente.setLimiteCredito(5000.0);
cliente.setBeneficioFrete(false);
cliente.setPrazoEntregaDias(5);
}
private void configurarClienteNovo(Cliente cliente) {
cliente.setLimiteCredito(1000.0);
cliente.setBeneficioFrete(false);
cliente.setPrazoEntregaDias(7);
}
}
// Usando a Factory nos controllers
@RestController
@RequestMapping("/api/pedidos")
public class PedidoController {
@Autowired
private PedidoService pedidoService;
@Autowired
private ClienteFactory clienteFactory;
@PostMapping
public ResponseEntity<PedidoDTO> criarPedido(@RequestBody PedidoRequest request) {
// Criação do cliente através da factory
Cliente cliente = clienteFactory.criarCliente(
request.getClienteId(),
request.getTipoCliente()
);
Pedido pedido = pedidoService.criarPedido(request, cliente);
return ResponseEntity.ok(convertToDTO(pedido));
}
}
@RestController
@RequestMapping("/api/clientes")
public class ClienteController {
@Autowired
private ClienteService clienteService;
@Autowired
private ClienteFactory clienteFactory;
@PostMapping
public ResponseEntity<ClienteDTO> cadastrarCliente(@RequestBody ClienteRequest request) {
// Criação do cliente através da factory
Cliente cliente = clienteFactory.criarCliente(request);
Cliente clienteSalvo = clienteService.salvarCliente(cliente);
return ResponseEntity.ok(convertToDTO(clienteSalvo));
}
}
Este padrão não apenas elimina a duplicação, mas também centraliza a lógica de criação e configuração de clientes, facilitando futuras modificações e manutenção. Se as regras de negócio para um tipo específico de cliente mudarem, apenas os métodos relevantes na factory precisarão ser atualizados, em vez de buscar e alterar essa lógica em múltiplos controllers e serviços.
KISS - Keep It Simple, Stupid (Mantenha Simples, Estúpido)
Origem e História
O princípio KISS tem uma origem interessante no campo da engenharia aeroespacial. Foi atribuído a Kelly Johnson, engenheiro da Lockheed Skunk Works, responsável pelo design de aeronaves como o U-2 e o SR-71 Blackbird durante as décadas de 1960 e 1970. Johnson instruía seus engenheiros que seus sistemas deveriam ser simples o suficiente para serem reparados por um mecânico comum em condições de campo com ferramentas básicas.
O acrônimo "Keep It Simple, Stupid" foi posteriormente adotado pela Marinha dos EUA em 1960. O conceito rapidamente transcendeu o contexto militar e encontrou aplicação em diversas áreas, incluindo o desenvolvimento de software.
Por que é importante?
A importância do KISS no desenvolvimento de software é multifacetada:
Manutenibilidade: Código simples é mais fácil de entender e modificar.
Depuração: Soluções simples apresentam menor superfície para bugs potenciais.
Integração de novos desenvolvedores: Profissionais recém-chegados ao projeto conseguem compreender e contribuir para o código mais rapidamente.
Desempenho: Soluções simples frequentemente apresentam melhor desempenho que alternativas complexas.
Adaptabilidade: Sistemas simples geralmente respondem melhor a novos requisitos.
Exemplos Práticos em Java
Sem aplicar o princípio KISS:
@Service
public class ValidadorEmailService {
@Autowired
private UsuarioRepository usuarioRepository;
@Autowired
private EmailBlacklistRepository blacklistRepository;
@Autowired
private ConfiguracoesService configuracoesService;
public ValidationResult validarEmail(String email) {
ValidationResult result = new ValidationResult();
// Verificação 1: Email não pode ser nulo ou vazio
if (email == null || email.trim().isEmpty()) {
result.addError("EMAIL_EMPTY", "Email não pode ser vazio");
return result;
}
// Verificação 2: Formato básico do email
if (!email.contains("@")) {
result.addError("EMAIL_INVALID_FORMAT", "Email deve conter @");
return result;
}
// Verificação 3: Validação estrutural detalhada
String[] partes = email.split("@");
if (partes.length != 2) {
result.addError("EMAIL_INVALID_STRUCTURE", "Email deve ter exatamente uma parte local e uma parte de domínio");
return result;
}
String localPart = partes[0];
String domainPart = partes[1];
if (localPart.isEmpty()) {
result.addError("EMAIL_LOCAL_PART_EMPTY", "Parte local do email não pode ser vazia");
return result;
}
if (domainPart.isEmpty()) {
result.addError("EMAIL_DOMAIN_EMPTY", "Domínio do email não pode ser vazio");
return result;
}
if (!domainPart.contains(".")) {
result.addError("EMAIL_INVALID_DOMAIN", "Domínio deve ter pelo menos um ponto");
return result;
}
String[] domainParts = domainPart.split("\\.");
for (String part : domainParts) {
if (part.isEmpty()) {
result.addError("EMAIL_INVALID_DOMAIN_PART", "Partes do domínio não podem ser vazias");
return result;
}
}
// Verificação 4: Verificação de caracteres permitidos
String allowedCharsLocal = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_+";
for (char c : localPart.toCharArray()) {
if (allowedCharsLocal.indexOf(c) < 0) {
result.addError("EMAIL_INVALID_CHAR_LOCAL", "Caractere não permitido na parte local: " + c);
return result;
}
}
String allowedCharsDomain = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-";
for (char c : domainPart.toCharArray()) {
if (allowedCharsDomain.indexOf(c) < 0) {
result.addError("EMAIL_INVALID_CHAR_DOMAIN", "Caractere não permitido no domínio: " + c);
return result;
}
}
// Verificação 5: Regras de negócio específicas
if (blacklistRepository.isEmailDomainBlacklisted(domainPart)) {
result.addError("EMAIL_DOMAIN_BLACKLISTED", "Domínio não permitido");
return result;
}
if (usuarioRepository.existsByEmail(email) && configuracoesService.isEmailUniqueRequired()) {
result.addError("EMAIL_ALREADY_EXISTS", "Email já cadastrado");
return result;
}
// Se chegou até aqui, o email é válido
result.setValid(true);
return result;
}
}
Aplicando o princípio KISS:
@Service
public class ValidadorEmailService {
@Autowired
private UsuarioRepository usuarioRepository;
@Autowired
private EmailBlacklistRepository blacklistRepository;
@Autowired
private ConfiguracoesService configuracoesService;
// Expressão regular para validação de email seguindo RFC 5322
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$");
public ValidationResult validarEmail(String email) {
ValidationResult result = new ValidationResult();
// Verificação 1: Email não pode ser nulo ou vazio
if (email == null || email.trim().isEmpty()) {
result.addError("EMAIL_EMPTY", "Email não pode ser vazio");
return result;
}
// Verificação 2: Formato do email usando regex
if (!EMAIL_PATTERN.matcher(email).matches()) {
result.addError("EMAIL_INVALID_FORMAT", "Formato de email inválido");
return result;
}
// Verificação 3: Regras de negócio específicas
String dominio = email.substring(email.lastIndexOf('@') + 1);
if (blacklistRepository.isEmailDomainBlacklisted(dominio)) {
result.addError("EMAIL_DOMAIN_BLACKLISTED", "Domínio não permitido");
return result;
}
if (usuarioRepository.existsByEmail(email) && configuracoesService.isEmailUniqueRequired()) {
result.addError("EMAIL_ALREADY_EXISTS", "Email já cadastrado");
return result;
}
// Se chegou até aqui, o email é válido
result.setValid(true);
return result;
}
}
Na versão KISS, utilizamos uma expressão regular padronizada para validar o formato do email, em vez de implementar manualmente todas as verificações estruturais. Isso torna o código significativamente mais curto, mais legível e menos propenso a erros, enquanto mantém todas as regras de negócio necessárias.
Aplicando o princípio KISS com expressão regular e Apache Commons Lang3:
import org.apache.commons.lang3.StringUtils;
@Service
public class ValidadorEmailService {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$");
public ValidationResult validarEmail(String email) {
ValidationResult result = new ValidationResult();
// Utilização do StringUtils.isBlank() da Apache Commons Lang3
if (StringUtils.isBlank(email)) {
result.addError("EMAIL_EMPTY", "Email não pode ser vazio");
return result;
}
// Validação de formato com regex
if (!EMAIL_PATTERN.matcher(email).matches()) {
result.addError("EMAIL_INVALID_FORMAT", "Formato de email inválido");
return result;
}
// Demais regras de negócio...
result.setValid(true);
return result;
}
}
Por que isso é KISS?
Ao usar o método StringUtils.isBlank() da biblioteca Apache Commons Lang3, encapsulamos a verificação de null, string vazia e espaços em branco em uma única chamada clara e descritiva. Isso reduz ruído, melhora a legibilidade e evita erros comuns em validações manuais.
Essa abordagem exemplifica bem o princípio KISS — evitar reinventar a roda e utilizar ferramentas confiáveis para manter o código simples, expressivo e eficiente.
YAGNI - You Aren't Gonna Need It (Você Não Vai Precisar Disso)
Origem e História
O princípio YAGNI surgiu no contexto do Extreme Programming (XP), uma metodologia ágil de desenvolvimento de software formulada por Kent Beck no final dos anos 1990. O conceito central propõe que os desenvolvedores não devem adicionar funcionalidades até que sejam realmente necessárias, não apenas porque potencialmente poderiam ser úteis no futuro.
YAGNI é um princípio contra-intuitivo para muitos desenvolvedores, pois contraria o instinto natural de planejar com antecedência e tentar antecipar necessidades futuras. No entanto, a experiência coletiva da indústria demonstrou que a maioria das funcionalidades projetadas para uso futuro nunca são efetivamente necessárias, ou são implementadas de forma diferente quando a necessidade real emerge.
Por que é importante?
A importância do YAGNI pode ser compreendida através de seus benefícios diretos:
Foco em valor imediato: Direciona os recursos para resolver problemas reais e atuais.
Redução de complexidade: Cada funcionalidade adicional aumenta a complexidade do sistema.
Economia de recursos: Tempo e energia são conservados ao evitar desenvolvimento desnecessário.
Design mais claro: Sistemas projetados para resolver problemas reais tendem a apresentar designs mais coerentes.
Adaptabilidade: É mais fácil adaptar um sistema simples a novos requisitos do que retrabalhar um sistema complexo.
Exemplos Práticos em Java
Sem aplicar o princípio YAGNI:
@Entity
@Table(name = "usuarios")
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nome;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String senha;
// Campos necessários atualmente
private boolean ativo;
private LocalDateTime dataCriacao;
private LocalDateTime ultimoLogin;
// Campos para funcionalidades futuras que "talvez" sejam necessárias
private String telefone;
private String endereco;
private String complemento;
private String cidade;
private String estado;
private String cep;
private LocalDate dataNascimento;
private String genero;
private String fotoPerfil;
private String biografia;
private String tokenResetSenha;
private LocalDateTime dataExpiracaoToken;
private boolean contaVerificada;
private String tokenVerificacao;
private Integer tentativasLogin;
private boolean bloqueado;
private LocalDateTime dataBloqueio;
private String preferenciasNotificacao;
private String configuracaoPrivacidade;
private String tipoUsuario;
private Float avaliacao;
private Integer numeroAvaliacoes;
// Relações que "podem ser úteis" no futuro
@OneToMany(mappedBy = "usuario")
private List<Pedido> pedidos;
@OneToMany(mappedBy = "usuario")
private List<Endereco> enderecos;
@ManyToMany
@JoinTable(
name = "usuarios_interesses",
joinColumns = @JoinColumn(name = "usuario_id"),
inverseJoinColumns = @JoinColumn(name = "interesse_id")
)
private Set<Interesse> interesses;
@ManyToMany
@JoinTable(
name = "usuarios_roles",
joinColumns = @JoinColumn(name = "usuario_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles;
// Getters e setters para TODOS os campos...
// (métodos omitidos para brevidade)
}
@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
@Autowired
private UsuarioService usuarioService;
@GetMapping("/{id}")
public ResponseEntity<UsuarioDTO> buscarUsuario(@PathVariable Long id) {
Usuario usuario = usuarioService.buscarPorId(id);
UsuarioDTO dto = converterParaDTO(usuario);
return ResponseEntity.ok(dto);
}
private UsuarioDTO converterParaDTO(Usuario usuario) {
// Conversão com todos os campos possíveis
UsuarioDTO dto = new UsuarioDTO();
dto.setId(usuario.getId());
dto.setNome(usuario.getNome());
dto.setEmail(usuario.getEmail());
dto.setAtivo(usuario.isAtivo());
dto.setDataCriacao(usuario.getDataCriacao());
dto.setUltimoLogin(usuario.getUltimoLogin());
// Campos para funcionalidades futuras
dto.setTelefone(usuario.getTelefone());
dto.setEndereco(usuario.getEndereco());
dto.setComplemento(usuario.getComplemento());
dto.setCidade(usuario.getCidade());
dto.setEstado(usuario.getEstado());
dto.setCep(usuario.getCep());
dto.setDataNascimento(usuario.getDataNascimento());
dto.setGenero(usuario.getGenero());
dto.setFotoPerfil(usuario.getFotoPerfil());
dto.setBiografia(usuario.getBiografia());
dto.setContaVerificada(usuario.isContaVerificada());
dto.setBloqueado(usuario.isBloqueado());
dto.setTipoUsuario(usuario.getTipoUsuario());
dto.setAvaliacao(usuario.getAvaliacao());
dto.setNumeroAvaliacoes(usuario.getNumeroAvaliacoes());
// Relações
if (usuario.getPedidos() != null) {
dto.setPedidos(usuario.getPedidos().stream()
.map(this::converterPedidoParaDTO)
.collect(Collectors.toList()));
}
if (usuario.getEnderecos() != null) {
dto.setEnderecos(usuario.getEnderecos().stream()
.map(this::converterEnderecoParaDTO)
.collect(Collectors.toList()));
}
return dto;
}
}
Aplicando o princípio YAGNI:
@Entity
@Table(name = "usuarios")
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nome;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String senha;
// Apenas campos necessários para os requisitos atuais
private boolean ativo;
private LocalDateTime dataCriacao;
private LocalDateTime ultimoLogin;
// Getters e setters apenas para os campos existentes
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getSenha() {
return senha;
}
public void setSenha(String senha) {
this.senha = senha;
}
public boolean isAtivo() {
return ativo;
}
public void setAtivo(boolean ativo) {
this.ativo = ativo;
}
public LocalDateTime getDataCriacao() {
return dataCriacao;
}
public void setDataCriacao(LocalDateTime dataCriacao) {
this.dataCriacao = dataCriacao;
}
public LocalDateTime getUltimoLogin() {
return ultimoLogin;
}
public void setUltimoLogin(LocalDateTime ultimoLogin) {
this.ultimoLogin = ultimoLogin;
}
}
@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
@Autowired
private UsuarioService usuarioService;
@GetMapping("/{id}")
public ResponseEntity<UsuarioDTO> buscarUsuario(@PathVariable Long id) {
Usuario usuario = usuarioService.buscarPorId(id);
UsuarioDTO dto = converterParaDTO(usuario);
return ResponseEntity.ok(dto);
}
private UsuarioDTO converterParaDTO(Usuario usuario) {
// Apenas os campos necessários atualmente
UsuarioDTO dto = new UsuarioDTO();
dto.setId(usuario.getId());
dto.setNome(usuario.getNome());
dto.setEmail(usuario.getEmail());
dto.setAtivo(usuario.isAtivo());
dto.setDataCriacao(usuario.getDataCriacao());
dto.setUltimoLogin(usuario.getUltimoLogin());
return dto;
}
// Apenas os métodos CRUD necessários...
}
Na versão YAGNI, a entidade Usuario
e seu respectivo DTO contêm apenas os campos necessários para os requisitos atuais do sistema. Campos adicionais, relacionamentos e métodos que poderiam ser úteis no futuro foram eliminados. Isso resulta em um código mais limpo, mais fácil de entender e manter. Se futuramente houver necessidade de adicionar novos campos ou relacionamentos, eles podem ser incluídos quando a necessidade real surgir, evitando a complexidade desnecessária e o esforço de manutenção de código que pode nunca ser utilizado.
TDA - Tell, Don't Ask (Diga, Não Pergunte)
Origem e História
O princípio "Tell, Don't Ask" foi popularizado por Andy Hunt e Dave Thomas (os mesmos autores de "The Pragmatic Programmer" que formalizaram o DRY). O conceito fundamental estabelece que objetos devem ser responsáveis por seu próprio comportamento, e o código cliente deve "dizer" a um objeto o que fazer, em vez de "perguntar" sobre seu estado interno e tomar decisões baseadas nessa informação.
Este princípio é fundamentalmente relacionado à programação orientada a objetos e enfatiza o encapsulamento - o princípio de que os detalhes internos de um objeto devem ser ocultos do mundo exterior.
Por que é importante?
A importância do TDA manifesta-se em vários aspectos:
Encapsulamento: Mantém a lógica relacionada a dados específicos junto com esses dados.
Coesão: Resulta em classes mais coesas onde comportamentos relacionados estão agrupados.
Desacoplamento: Reduz o acoplamento entre componentes do sistema.
Manutenibilidade: Mudanças em regras de negócio afetam apenas as classes responsáveis por essas regras.
Testabilidade: Classes que seguem TDA são geralmente mais fáceis de testar isoladamente.
Exemplos Práticos em Java
Sem aplicar o princípio TDA:
@Service
public class PedidoService {
@Autowired
private PedidoRepository pedidoRepository;
@Autowired
private ClienteRepository clienteRepository;
@Autowired
private ProdutoRepository produtoRepository;
@Autowired
private EmailService emailService;
public void processarPagamento(Long pedidoId, String formaPagamento) {
Pedido pedido = pedidoRepository.findById(pedidoId)
.orElseThrow(() -> new RecursoNaoEncontradoException("Pedido não encontrado"));
Cliente cliente = pedido.getCliente();
// Valida disponibilidade dos produtos
for (ItemPedido item : pedido.getItens()) {
Produto produto = item.getProduto();
// Verifica se tem estoque suficiente
if (produto.getEstoqueDisponivel() < item.getQuantidade()) {
throw new NegocioException("Produto " + produto.getNome() + " sem estoque suficiente");
}
}
// Verifica limite do cliente para pagamento por crédito
if ("CREDITO".equals(formaPagamento)) {
BigDecimal limiteDisponivel = cliente.getLimiteCredito().subtract(cliente.getSaldoUtilizado());
if (pedido.getValorTotal().compareTo(limiteDisponivel) > 0) {
throw new NegocioException("Cliente não possui limite de crédito suficiente");
}
// Atualiza saldo utilizado do cliente
cliente.setSaldoUtilizado(cliente.getSaldoUtilizado().add(pedido.getValorTotal()));
clienteRepository.save(cliente);
}
// Atualiza status do pedido e processa pagamento
pedido.setStatus(StatusPedido.PAGO);
pedido.setFormaPagamento(formaPagamento);
pedido.setDataPagamento(LocalDateTime.now());
// Reduz estoque dos produtos
for (ItemPedido item : pedido.getItens()) {
Produto produto = item.getProduto();
produto.setEstoqueDisponivel(produto.getEstoqueDisponivel() - item.getQuantidade());
produtoRepository.save(produto);
}
pedidoRepository.save(pedido);
// Envia email de confirmação
String mensagem = "Seu pedido #" + pedido.getId() + " no valor de R$ " +
pedido.getValorTotal() + " foi confirmado com sucesso.";
emailService.enviarEmail(cliente.getEmail(), "Confirmação de Pagamento", mensagem);
}
}
Aplicando o princípio TDA:
@Service
public class PedidoService {
@Autowired
private PedidoRepository pedidoRepository;
@Autowired
private EmailService emailService;
public void processarPagamento(Long pedidoId, String formaPagamento) {
Pedido pedido = pedidoRepository.findById(pedidoId)
.orElseThrow(() -> new RecursoNaoEncontradoException("Pedido não encontrado"));
// Validar disponibilidade de produtos
pedido.validarDisponibilidadeProdutos();
// Processar pagamento
boolean pagamentoRealizado = pedido.processarPagamento(formaPagamento);
if (!pagamentoRealizado) {
throw new NegocioException("Falha ao processar pagamento");
}
pedidoRepository.save(pedido);
// Enviar confirmação ao cliente
pedido.enviarConfirmacao(emailService);
}
}
@Entity
public class Pedido {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Cliente cliente;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "pedido")
private List<ItemPedido> itens;
@Enumerated(EnumType.STRING)
private StatusPedido status;
private String formaPagamento;
private LocalDateTime dataPagamento;
private BigDecimal valorTotal;
// Outros campos e getters/setters
public void validarDisponibilidadeProdutos() {
for (ItemPedido item : itens) {
if (!item.validarDisponibilidade()) {
throw new NegocioException("Produto " + item.getProduto().getNome() + " sem estoque suficiente");
}
}
}
public boolean processarPagamento(String formaPagamento) {
// Validar pagamento com base na forma escolhida
if ("CREDITO".equals(formaPagamento)) {
boolean temLimite = cliente.validarLimiteCredito(valorTotal);
if (!temLimite) {
return false;
}
cliente.utilizarCredito(valorTotal);
}
// Atualizar status e data
this.status = StatusPedido.PAGO;
this.formaPagamento = formaPagamento;
this.dataPagamento = LocalDateTime.now();
// Atualizar estoque
itens.forEach(ItemPedido::atualizarEstoque);
return true;
}
public void enviarConfirmacao(EmailService emailService) {
String mensagem = "Seu pedido #" + id + " no valor de R$ " +
valorTotal + " foi confirmado com sucesso.";
emailService.enviarEmail(cliente.getEmail(), "Confirmação de Pagamento", mensagem);
}
}
@Entity
public class Cliente {
// Campos e outros métodos
private BigDecimal limiteCredito;
private BigDecimal saldoUtilizado;
public boolean validarLimiteCredito(BigDecimal valorCompra) {
BigDecimal limiteDisponivel = limiteCredito.subtract(saldoUtilizado);
return valorCompra.compareTo(limiteDisponivel) <= 0;
}
public void utilizarCredito(BigDecimal valor) {
this.saldoUtilizado = saldoUtilizado.add(valor);
}
}
@Entity
public class ItemPedido {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Pedido pedido;
@ManyToOne
private Produto produto;
private Integer quantidade;
private BigDecimal valorUnitario;
// Getters e setters
public boolean validarDisponibilidade() {
return produto.getEstoqueDisponivel() >= quantidade;
}
public void atualizarEstoque() {
produto.reduzirEstoque(quantidade);
}
}
@Entity
public class Produto {
// Campos e getters/setters
private Integer estoqueDisponivel;
public void reduzirEstoque(int quantidade) {
if (estoqueDisponivel < quantidade) {
throw new NegocioException("Estoque insuficiente");
}
this.estoqueDisponivel -= quantidade;
}
}
Na versão que aplica o princípio TDA, cada entidade é responsável por seu próprio comportamento. O PedidoService
não pergunta mais sobre o estado interno dos objetos para tomar decisões; em vez disso, delega comportamentos aos objetos apropriados. Por exemplo:
- O
Pedido
é responsável por validar a disponibilidade de seus itens. - O
Cliente
é responsável por validar seu próprio limite de crédito. - Cada
ItemPedido
sabe como validar sua disponibilidade e atualizar o estoque. - O
Produto
é responsável por gerenciar seu próprio estoque.
Este design encapsula melhor o comportamento dentro dos objetos relevantes, tornando o código mais modular, mais fácil de manter e estender. Além disso, facilita a adição de novas regras de negócio relacionadas a cada entidade sem precisar modificar o serviço principal.
TDA e a Lei de Demeter
É relevante observar que o princípio Tell, Don't Ask (TDA) frequentemente opera em conjunto com a Lei de Demeter (LoD), também conhecida como "princípio do menor conhecimento". A Lei de Demeter estabelece que um objeto só deve invocar métodos que pertençam a:
- O próprio objeto
- Objetos recebidos como parâmetros para o método
- Objetos criados diretamente pelo método
- Propriedades/campos diretos do objeto
No exemplo do TDA previamente discutido, quando transformamos o código, estamos também aderindo à Lei de Demeter. Vamos analisar um exemplo mais específico:
Código violando a Lei de Demeter:
@Service
public class RelatorioVendasService {
@Autowired
private VendaRepository vendaRepository;
public RelatorioMensal gerarRelatorioMensal(Long lojaId, int mes, int ano) {
List<Venda> vendas = vendaRepository.findByLojaIdAndPeriodo(lojaId, mes, ano);
RelatorioMensal relatorio = new RelatorioMensal(lojaId, mes, ano);
for (Venda venda : vendas) {
// Viola Lei de Demeter: navega pela cadeia de objetos
String nomeProduto = venda.getItens().get(0).getProduto().getNome();
String categoriasProduto = venda.getItens().get(0).getProduto().getCategoria().getNome();
String nomeVendedor = venda.getVendedor().getDepartamento().getGerente().getNome();
// Mais código usando estas informações...
}
return relatorio;
}
}
Código aderente à Lei de Demeter e ao TDA:
@Service
public class RelatorioVendasService {
@Autowired
private VendaRepository vendaRepository;
public RelatorioMensal gerarRelatorioMensal(Long lojaId, int mes, int ano) {
List<Venda> vendas = vendaRepository.findByLojaIdAndPeriodo(lojaId, mes, ano);
RelatorioMensal relatorio = new RelatorioMensal(lojaId, mes, ano);
// Delegamos a responsabilidade para a entidade Venda
for (Venda venda : vendas) {
relatorio.adicionarVenda(venda);
}
return relatorio;
}
}
@Entity
public class Venda {
// atributos e outros métodos
public void adicionarAoRelatorio(RelatorioMensal relatorio) {
// A Venda conhece sua própria estrutura e sabe como
// extrair informações relevantes
relatorio.adicionarValorVenda(this.valor);
relatorio.registrarFormaPagamento(this.formaPagamento);
for (ItemVenda item : this.itens) {
item.adicionarAoRelatorio(relatorio);
}
this.vendedor.adicionarEstatisticasAoRelatorio(relatorio);
}
}
public class ItemVenda {
// atributos e outros métodos
public void adicionarAoRelatorio(RelatorioMensal relatorio) {
relatorio.adicionarItem(
this.quantidade,
this.valorUnitario,
this.produto.obterInformacaoResumida()
);
}
}
public class Vendedor {
// atributos e outros métodos
public void adicionarEstatisticasAoRelatorio(RelatorioMensal relatorio) {
relatorio.registrarVendedor(this.id, this.nome);
relatorio.adicionarComissao(this.calcularComissao());
// O Vendedor sabe como obter informações do departamento sem expor
// a estrutura interna do departamento
relatorio.registrarDepartamento(this.obterInformacaoDepartamento());
}
public String obterInformacaoDepartamento() {
// Encapsula acesso à cadeia de objetos
return this.departamento.obterInformacaoCompleta();
}
}
No exemplo aderente à Lei de Demeter, cada objeto é responsável pelo seu próprio comportamento e por conhecer sua estrutura interna. O serviço não precisa navegar por cadeias de objetos, pois cada entidade sabe como extrair e fornecer suas próprias informações, seguindo o princípio "Tell, Don't Ask".
A Lei de Demeter é frequentemente resumida como "só fale com seus amigos imediatos, não com os amigos dos seus amigos". Essa abordagem reduz o acoplamento entre componentes, facilita a manutenção e promove um melhor encapsulamento, complementando perfeitamente o princípio TDA.
Críticas e Limitações dos Princípios
Apesar de seus evidentes benefícios, é importante reconhecer que cada um destes princípios tem suas limitações e críticas relevantes. Aplicá-los sem considerar o contexto específico pode resultar em problemas.
Críticas ao DRY
Acoplamento indesejado: A busca pela eliminação de toda duplicação pode criar acoplamento entre componentes que deveriam ser independentes. Quando partes do sistema que evoluem por razões diferentes compartilham código, mudanças em uma parte podem afetar outras desnecessariamente.
Abstração prematura: Para eliminar duplicação, frequentemente criamos abstrações que são difíceis de entender e podem ser mais complexas que a duplicação original. Isso levou ao princípio alternativo WET (Write Everything Twice ou "Escreva Tudo Duas Vezes"), sugerindo que alguma duplicação controlada pode ser preferível a abstrações inadequadas.
Repetição acidental vs. essencial: Nem toda semelhança no código representa uma verdadeira duplicação conceitual. Por vezes, partes do código parecem similares mas têm propósitos distintos e podem evoluir em direções diferentes.
Críticas ao KISS
Subjetividade da simplicidade: O que é "simples" para um desenvolvedor pode ser complexo para outro. Essa subjetividade dificulta a aplicação consistente do princípio.
Simplificação excessiva: Em sistemas intrinsecamente complexos, soluções excessivamente simplificadas podem não abordar adequadamente a complexidade inerente do domínio.
Compromisso com desempenho: Soluções mais simples podem não oferecer o desempenho ideal. Em sistemas com requisitos rigorosos de performance, o KISS pode conduzir a soluções subótimas.
Críticas ao YAGNI
Dívida técnica estrutural: Seguir YAGNI estritamente pode resultar em arquiteturas difíceis de adaptar posteriormente. Algumas decisões arquiteturais fundamentais são difíceis de modificar uma vez que o sistema está em produção.
Custo de retrabalho: Se uma funcionalidade inicialmente adiada realmente se torna necessária, o custo de implementá-la posteriormente pode ser significativamente maior do que teria sido se incorporada desde o início.
Conhecimento do domínio: Em domínios bem estabelecidos, desenvolvedores experientes podem prever com razoável precisão quais funcionalidades serão necessárias, tornando o YAGNI menos relevante.
Críticas ao TDA
Classes sobrecarregadas: Uma aplicação estrita do TDA pode resultar em classes com múltiplas responsabilidades, violando o Princípio de Responsabilidade Única (SRP).
Incompatibilidade com paradigmas funcionais: Em programação funcional ou em designs orientados a dados, o TDA pode ser inadequado ou simplesmente não aplicável.
Desafios com aspectos transversais: Funcionalidades como logging, segurança ou auditoria frequentemente necessitam "perguntar" sobre o estado interno dos objetos para funcionar adequadamente.
Como observado pelo arquiteto de software Sebastian Malaca, "princípios são genéricos e livres de contexto, mas soluções reais são baseadas em contexto, então ajuste os princípios ao contexto, não o contrário". Esta é uma perspectiva valiosa para considerar durante a aplicação destes princípios.
Integrando os Princípios em seu Desenvolvimento
Estes quatro princípios (DRY, KISS, YAGNI e TDA) não existem isoladamente - eles se complementam e, quando aplicados com discernimento, criam uma abordagem holística para o desenvolvimento de software de qualidade.
Como os princípios se relacionam?
DRY + KISS: A eliminação de duplicação (DRY) frequentemente resulta em soluções mais simples (KISS).
KISS + YAGNI: Manter a simplicidade (KISS) muitas vezes envolve evitar adicionar funcionalidades desnecessárias (YAGNI).
YAGNI + TDA: Concentrar-se apenas no necessário no momento (YAGNI) conduz a um design onde objetos têm responsabilidades claras e bem definidas (TDA).
TDA + DRY: Quando objetos são responsáveis por seu próprio comportamento (TDA), torna-se mais fácil evitar duplicação de lógica (DRY).
Implementando na Prática
Comece com KISS: Ao iniciar qualquer componente, busque a solução mais simples e direta possível.
Aplique YAGNI: Questione cada funcionalidade proposta: "É necessária agora?". Se a resposta for negativa, postergue sua implementação.
Refatore para DRY: À medida que o código evolui, identifique duplicações e refatore para eliminar redundâncias.
Estruture com TDA: Organize seu código orientado a objetos para que cada classe seja responsável por seu próprio comportamento.
Use discernimento: Lembre-se que princípios são diretrizes, não regras absolutas. Saiba quando ser flexível e adaptá-los ao contexto específico.
Conclusão
Os princípios DRY, KISS, YAGNI e TDA não são apenas técnicas de codificação - são uma filosofia de desenvolvimento que valoriza clareza, manutenção e eficácia. Quando você internaliza e aplica esses princípios, eles mudam não só o código que você escreve, mas também a maneira como você pensa sobre os problemas de software.
No mundo atual, onde a complexidade técnica cresce a cada dia, a habilidade de manter a simplicidade enquanto resolve problemas complexos é um diferencial valioso. Esses princípios dão um caminho confiável para alcançar esse equilíbrio difícil, desde que você os veja como guias e não como regras absolutas.
Lembre-se: os princípios existem para nos servir, não o contrário. O desenvolvedor maduro sabe quando aplicá-los rigorosamente e quando adaptá-los (ou até mesmo quebrá-los de propósito) para servir a um objetivo maior no design do sistema.
Como Leonardo da Vinci disse há séculos, "a simplicidade é o último grau de sofisticação". No desenvolvimento de software, essa verdade continua valendo, mas o caminho para a simplicidade nem sempre é fácil - exige discernimento, experiência e sabedoria para aplicar os princípios certos, na hora certa, do jeito certo.
Referências Bibliográficas
Hunt, A., & Thomas, D. (1999). The Pragmatic Programmer: From Journeyman to Master. Addison-Wesley Professional.
Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall.
Beck, K. (1999). Extreme Programming Explained: Embrace Change. Addison-Wesley Professional.
Fowler, M. (2018). Refactoring: Improving the Design of Existing Code (2nd ed.). Addison-Wesley Professional.
Freeman, S., & Pryce, N. (2009). Growing Object-Oriented Software, Guided by Tests. Addison-Wesley Professional.
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional.
McConnell, S. (2004). Code Complete: A Practical Handbook of Software Construction (2nd ed.). Microsoft Press.
Castilho, R. (2014). Tell, don't ask. [Blog]. Disponível em: https://robsoncastilho.com.br/2014/05/11/conceitos-tell-dont-ask/
Souza, U. (2022). KISS, YAGNI, DRY – três princípios que todo desenvolvedor deveria conhecer! DEV Community. Disponível em: https://dev.to/urielsouza29/kiss-yagni-dry-tres-principios-que-todo-desenvolvedor-deveria-conhecer-47gg
Thiagomr. (2020). Desvendando o mundo mágico dos acrônimos: SOLID, KISS, YAGNI, DRY, DDD, TDD. DEV Community. Disponível em: https://dev.to/thiagomr/desvendando-o-mundo-magico-dos-acronimos-solid-kiss-yagni-dry-ddd-tdd-2onp
Lazaros. (2023). Conheça os 6 princípios essenciais para arquitetura de um software. Disponível em: https://www.lazaros.com.br/blog/6-principios-arquitetura-software/
Prado, L. P. (2017). O princípio Tell, Don't Ask. [Blog]. Disponível em: https://luizpauloprado.com.br/2017/01/04/o-principio-tell-dont-ask/
Fonseca Chaves, G. (2020). Conceito Tell, Don't Ask. LinkedIn. Disponível em: https://pt.linkedin.com/pulse/conceito-tell-dont-ask-guilherme-fonseca-chaves
Silva, R. (2019). Princípio Tell, Don't Ask. [Blog]. Disponível em: https://ramonsilva.net/post/princípio-tell-don-t-ask# A Arte da Simplicidade no Desenvolvimento de Software: DRY, KISS, YAGNI e TDA