Estruturas de Dados
Estruturas de dados não são apenas recipientes. A forma como modelamos dados influencia encapsulamento, acoplamento, extensibilidade e até a legibilidade do domínio. Em código limpo, escolher entre objeto rico, DTO, record, coleção, mapa ou composição não é detalhe técnico: é parte do desenho da solução.
Dados e representação
Seção intitulada “Dados e representação”Um mesmo conceito pode ser representado de várias maneiras. Um ponto no plano, por exemplo, pode ser cartesiano (x, y) ou polar (r, theta). O ponto central não é apenas armazenar valores, mas decidir que operações são permitidas e que invariantes precisam ser preservadas.
Quando expomos diretamente atributos sem refletir sobre contrato, amarramos clientes a uma representação específica. Quando abstraímos demais sem necessidade, criamos burocracia. O equilíbrio está em modelar o dado segundo o tipo de mudança esperado.
Abstração de dados
Seção intitulada “Abstração de dados”Considere duas abordagens:
public class Ponto { public Double x; public Double y;}e
public interface Ponto { Double getX(); Double getY(); Double getR(); Double getTheta(); void setCartesiano(Double x, Double y); void setPolar(Double r, Double theta);}Na primeira, expomos representação. Na segunda, expomos operações e escondemos detalhes internos. A diferença é importante: abstração de dados não significa esconder tudo, mas sim esconder o que não deveria ser compromisso externo.
Anti-simetria entre dados e objetos
Seção intitulada “Anti-simetria entre dados e objetos”Uma ideia central do Clean Code é a anti-simetria entre estruturas de dados e objetos:
- estruturas de dados expõem dados e têm pouco ou nenhum comportamento;
- objetos escondem dados internos e expõem comportamento.
Problema comum: criar híbridos que fazem os dois de forma ruim. Um híbrido costuma:
- expor getters e setters em excesso;
- conter regras de negócio dispersas;
- permitir acesso interno demais;
- gerar dúvida sobre onde a lógica realmente pertence.
Se uma classe é DTO, deixe-a ser simples. Se é objeto de domínio, concentre comportamento nela.
A Lei de Demeter
Seção intitulada “A Lei de Demeter”A Lei de Demeter pode ser resumida assim: um módulo não deve conhecer detalhes internos profundos de outros objetos. Em termos práticos, um método deveria chamar apenas:
- métodos da própria classe;
- métodos de objetos recebidos como parâmetro;
- métodos de atributos próprios;
- métodos de objetos criados localmente.
O objetivo não é proibir chamadas encadeadas por estética, mas reduzir acoplamento estrutural. Quanto mais um método conhece o interior de outros objetos, mais frágil ele se torna diante de mudanças.
Carrinhos de trem
Seção intitulada “Carrinhos de trem”Chamadas encadeadas profundas como esta são sinal clássico de violação de Demeter:
music.getArtist().getAlbum().getSong().play();Esse estilo acopla o chamador à estrutura interna de múltiplos objetos. Se um elo da cadeia mudar, vários clientes quebram.
Melhor alternativa: mover responsabilidade para um objeto mais próximo do comportamento desejado.
Por exemplo, em vez de pedir detalhes internos do álbum, talvez o próprio MusicFile ou Player devesse saber como tocar a música correta.
Interfaces fluentes não são automaticamente problema
Seção intitulada “Interfaces fluentes não são automaticamente problema”Nem todo encadeamento é um “carrinho de trem”. Interfaces fluentes, quando retornam o próprio objeto e não expõem estrutura interna, podem continuar sendo compatíveis com Demeter.
Exemplo:
Order order = new Order() .setCustomer("John") .setShippingAddress("123 Main St") .setTotal(100.0);Aqui o cliente conversa com o mesmo objeto de forma fluente; não está navegando por uma árvore de dependências internas.
DTOs: objetos de transferência de dados
Seção intitulada “DTOs: objetos de transferência de dados”DTOs existem para transportar dados entre camadas, processos ou fronteiras de sistema. Eles devem ser simples e previsíveis. Em muitos casos, records são representação excelente porque comunicam imutabilidade e intenção de transporte.
Ruim:
public class Pessoa { private String nome; private String cpf;
public String getNome() { return this.nome; } public String getCpf() { return this.cpf; } public void setNome(String nome) { this.nome = nome; } public void setCpf(String cpf) { this.cpf = cpf; }}Melhor:
public record Pessoa(String nome, String cpf);Se a classe serve apenas para carregar dados, não há ganho em cercá-la de boilerplate.
Active Record
Seção intitulada “Active Record”Active Record é uma forma especial de objeto de dados que conhece operações simples sobre si mesmo, como save, update ou find. Isso pode ser útil em aplicações pequenas ou em certas ORMs, mas exige cuidado.
Regra prática:
- operações de persistência simples podem fazer sentido;
- regras de negócio complexas não devem morar ali.
Quando Active Record começa a concentrar validação, cálculo, autorização, integração e persistência ao mesmo tempo, a classe perde foco.
Escolhendo estruturas de coleção
Seção intitulada “Escolhendo estruturas de coleção”Arrays e listas
Seção intitulada “Arrays e listas”Use quando ordem importa, iteração sequencial é comum ou o processamento acontece como coleção inteira.
Boas para:
- preservar sequência;
- percorrer elementos em ordem;
- map/filter/reduce;
- acesso por índice, quando apropriado.
Use quando a operação dominante é consulta por chave. Mapas comunicam intenção de associação entre identificador e valor.
Boas para:
- busca rápida por id;
- cache em memória;
- roteamento por tipo;
- substituição de cadeias longas de condicionais simples.
Use quando unicidade é requisito principal. Se repetição é erro conceitual, conjunto comunica melhor do que lista.
Boas para:
- tags sem duplicidade;
- permissões;
- membros de grupo;
- comparação de presença.
Herança profunda vs composição
Seção intitulada “Herança profunda vs composição”Hierarquias longas tendem a dificultar entendimento e aumentar dependência entre níveis. Quanto maior a árvore, maior o esforço para descobrir de onde veio determinado comportamento.
Em muitos casos, composição produz modelo mais simples:
public class Funcionario { private Pessoa pessoa; private Cargo cargo; private Setor setor;}Essa abordagem evita inflar a hierarquia apenas para representar combinações de papel, cargo ou contexto.
KISS: mantenha simples
Seção intitulada “KISS: mantenha simples”KISS (Keep It Simple, Stupid) lembra que a melhor modelagem nem sempre é a mais elaborada. Nem todo domínio precisa de camadas extras, abstrações genéricas ou um festival de interfaces.
Perguntas úteis:
- esta abstração resolve um problema real ou imaginário?
- esta estrutura facilita mudança provável ou complica o presente?
- o modelo escolhido comunica o domínio ou apenas exibe sofisticação técnica?
Polimorfismo vs condicionais
Seção intitulada “Polimorfismo vs condicionais”Quando o comportamento varia por tipo, polimorfismo costuma ser melhor do que múltiplos if/else ou switch repetidos.
Exemplo:
public abstract class Funcionario { public abstract Double calculaSalario();}Cada subtipo implementa sua própria regra. Isso reduz condicionais espalhadas e facilita extensão.
Ainda assim, nem toda condicional é ruim. O problema é repetição sistemática da mesma decisão por vários pontos do sistema.
Maps vs condicionais
Seção intitulada “Maps vs condicionais”Algumas decisões simples podem ser representadas por mapas em vez de cadeia de ifs, especialmente em cenários de lookup ou seleção de estratégia.
Exemplo conceitual:
Map<String, Funcionario> funcionarios = new HashMap<>();funcionarios.put("Gerente", new Gerente());Esse estilo funciona bem quando a variação pode ser tratada como associação entre chave e comportamento/objeto.
Identidade vs igualdade
Seção intitulada “Identidade vs igualdade”Comparar objetos exige entender o contrato da linguagem e do domínio.
- identidade: pergunta se é o mesmo objeto em memória;
- igualdade: pergunta se representam o mesmo valor lógico.
Para tipos de valor, igualdade costuma ser central. Para entidades, identidade de domínio pode importar mais. Modelar isso corretamente evita bugs sutis em coleções, cache, busca e comparação.
Mutabilidade e passagem por referência
Seção intitulada “Mutabilidade e passagem por referência”Coleções e objetos mutáveis podem ser alterados por funções que os recebem como parâmetro. Isso significa que uma operação aparentemente local pode impactar o estado observado por outros trechos do sistema.
Por isso, vale decidir conscientemente:
- este objeto deve ser mutável?
- faz sentido copiar antes de alterar?
- devo devolver nova instância em vez de modificar a existente?
Imutabilidade, quando viável, reduz efeitos colaterais e simplifica raciocínio.
Checklist prático
Seção intitulada “Checklist prático”Ao modelar estruturas de dados, pergunte:
- esta classe é objeto rico ou apenas transporte de dados?
- há híbridos confusos misturando dados e comportamento?
- o código viola Demeter com navegação profunda demais?
- a coleção escolhida comunica a operação dominante?
- composição resolveria melhor que herança?
- igualdade e identidade estão bem definidas?
- mutabilidade está sob controle?