Error Driven Development: Estamos evitando erros ou sendo forçados a segui-los?
A função de um bom programador não é apenas fazer o código funcionar, mas garantir que ele seja sustentável para o futuro.
Error Driven Development acontece quando erros impulsionam o desenvolvimento, ao invés de boas práticas. Mas como erros podem orientar o desenvolvimento de software? Isso parece contraditório, não é? Calma lá, eu não enlouqueci (ainda) 😂. Esse termo é uma pequena piada sobre algo que todos já vimos no dia a dia e sentimos na pele ao ter que dar manutenção em sistemas – independentemente da linguagem de programação e framework.
Todo mundo já ouviu falar de TDD (Test Driven Development), DDD (Domain Driven Design) e tantos outros acrônimos que aparecem como “o caminho certo” para escrever código melhor. Mas e EDD (Error Driven Development)? Bom, esse ninguém ensina nos cursos de boas práticas. Na verdade, EDD não é um método, é uma crítica. Uma crítica à forma como a indústria de software e muitos programadores lidam com a programação: sem planejamento, sem design, sem princípios básicos e esperando que os erros apareçam para decidir o que precisa ser feito.
É aquele cenário clássico: o código está rodando, os usuários começam a reportar bugs, e ao invés de se perguntar “O que fizemos de errado no design?”, a equipe corre para apagar incêndios. O desenvolvimento passa a ser guiado pelos erros. As correções vêm na base do desespero, e não da prevenção. Cada bug descoberto gera um novo patch improvisado, que por sua vez cria outros bugs, e o ciclo se repete.
Seguimos o EDD: primeiro a gente erra, depois descobre como deveria ter feito.
Mas ao invés de só reclamar, vamos olhar mais de perto o que realmente está por trás desse problema. Vamos ver exemplos em código de como a falta de encapsulamento pode ser um perigo, como nomes de métodos ruins dificultam a manutenção, e como métodos gigantes são um convite para um sistema frágil e difícil de evoluir.
Se você já perdeu horas tentando entender um código confuso, bem-vindo ao Error Driven Development. Você já fez parte dele. E agora, vamos destrinchá-lo.
Como a falta de encapsulamento é tão prejudicial?
Encapsulamento é um daqueles conceitos fundamentais que todo mundo aprende cedo na programação, mas que nem sempre recebe a atenção que merece. Na prática, ele significa proteger a integridade dos dados, garantindo que eles sejam acessados e modificados apenas de maneiras seguras e controladas. Aliás, eu dediquei um artigo inteiro a esse tema: “Encapsulamento: Vendo Além da Superfície”, onde exploro esse conceito mais a fundo.
A ideia principal aqui é simples: nem todas as partes do código devem ser capazes de modificar diretamente os dados internos de uma classe. Em vez disso, a classe deve expor métodos que permitem que essas modificações aconteçam de forma segura. Dessa forma, podemos garantir que os dados sempre permaneçam em um estado válido, prevenindo bugs e inconsistências difíceis de detectar.
Mas será que a falta de encapsulamento realmente pode causar problemas? Vamos visualizar isso com um exemplo em Java.
Um Exemplo Clássico de Código Sem Encapsulamento
Aqui temos um cenário típico: uma classe BankAccount que representa uma conta bancária. Vamos cometer um erro clássico e deixar os atributos públicos, permitindo que qualquer parte do código os modifique diretamente.
public class BankAccount {
public String owner;
public double balance;
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
public void displayBalance() {
System.out.println(owner + " tem um saldo de: R$ " + balance);
}
public static void main(String[] args) {
BankAccount account = new BankAccount("João", 1000.0);
account.displayBalance();
// Qualquer um pode modificar os dados diretamente 😨
account.balance = -500.0; // Saldo negativo? Isso deveria ser permitido?
account.owner = ""; // Um nome vazio? Isso não faz sentido!
account.displayBalance();
}
}
Se você rodar esse código, verá algo assim:
João tem um saldo de: R$ 1000.0
tem um saldo de: R$ -500.0
• O saldo da conta ficou negativo, sem nenhum tipo de validação.
• O nome do dono da conta foi alterado para uma string vazia.
• Qualquer parte do código pode modificar os atributos sem restrições.
Olhando rapidamente, você pode pensar:
"Mas eu já vi classes assim um centenas de vezes, qual o problema?"
O problema é que qualquer parte do código pode modificar os atributos owner
e balance
diretamente.
Isso significa que:
📌 Não há validação alguma — Qualquer valor pode ser atribuído, sem restrições.
📌 Não há regras de consistência — Os dados podem ser alterados para estados inválidos.
📌 O código não protege suas próprias variáveis — Ele apenas assume que ninguém vai usá-lo de maneira errada.
E esse último ponto é um erro fatal. Código bem escrito não deve confiar no programador para sempre lembrar de usá-lo corretamente.
Agora, expanda esse raciocínio para qualquer outra aplicação:
Um sistema de autenticação que permite alterar a senha diretamente sem validar regras de segurança.
Um sistema de pedidos onde o status do pedido pode ser alterado arbitrariamente, sem seguir um fluxo lógico.
Um sistema de usuários onde qualquer um pode mudar seu próprio nível de acesso para "administrador" com uma simples atribuição de variável.
Isso tudo pode parecer absurdo, mas acontece o tempo todo quando não há encapsulamento.
Isso não é só um problema de organização de código — isso pode gerar inconsistências e bugs perigosos no sistema. Imagine se um sistema de banco real permitisse isso!
Corrigindo o Problema com Encapsulamento
Agora, vamos proteger os dados da conta bancária tornando os atributos privados e expondo métodos de acesso (get e set) que garantem que os valores sempre estarão em um estado válido.
public class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner, double balance) {
setOwner(owner);
deposit(balance); // Usamos deposit para garantir que o saldo inicial seja válido
}
public void setOwner(String owner) {
if (owner == null || owner.trim().isEmpty()) {
throw new IllegalArgumentException("Nome do titular não pode ser vazio");
}
this.owner = owner;
}
public String getOwner() {
return owner;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("O valor do depósito deve ser positivo");
}
this.balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("O valor do saque deve ser positivo");
}
if (amount > balance) {
throw new IllegalArgumentException("Saldo insuficiente para o saque");
}
this.balance -= amount;
}
public double getBalance() {
return balance;
}
public void displayBalance() {
System.out.println(owner + " tem um saldo de: R$ " + balance);
}
public static void main(String[] args) {
BankAccount account = new BankAccount("João", 1000.0);
account.displayBalance();
// Agora tentamos ações inválidas
try {
account.deposit(-100.0);
} catch (IllegalArgumentException e) {
System.out.println("Erro: " + e.getMessage());
}
try {
account.withdraw(2000.0);
} catch (IllegalArgumentException e) {
System.out.println("Erro: " + e.getMessage());
}
try {
account.setOwner("");
} catch (IllegalArgumentException e) {
System.out.println("Erro: " + e.getMessage());
}
account.displayBalance();
}
}
Agora, o que mudou?
Se rodarmos esse código, veremos que erros são evitados antes que causem problemas sérios:
João tem um saldo de: R$ 1000.0
Erro: O valor do depósito deve ser positivo
Erro: Saldo insuficiente para o saque
Erro: Nome do titular não pode ser vazio
João tem um saldo de: R$ 1000.0
Agora:
✔ O saldo não pode ficar negativo.
✔ O nome do dono não pode ser inválido.
✔ Ninguém pode alterar os dados sem passar pelas validações da classe.
Encapsulamento Não é Frescura
Esse é um exemplo simples, mas ele ilustra um ponto importante: quando deixamos os dados expostos, abrimos espaço para erros difíceis de rastrear e corrigir.
Encapsular não é só sobre “seguir boas práticas” — é sobre proteger a integridade dos dados e evitar que seu sistema se torne um caos absoluto. Afinal, um pequeno descuido pode gerar um problema enorme, e no fim das contas, é isso que alimenta o Error Driven Development.
A Falta de Encapsulamento Torna a Manutenção Mais Custosa?
Se tem uma coisa que todo desenvolvedor já passou, foi pegar um código e ficar se perguntando: “O que diabos está acontecendo aqui?”
O problema? Falta de encapsulamento.
Quando os dados estão completamente expostos e podem ser modificados de qualquer lugar do código, não há garantias sobre o estado real do sistema em determinado momento. Isso pode levar a bugs difíceis de rastrear e, pior ainda, custosos de corrigir.
E aqui vai um detalhe importante: nem sempre o problema aparece logo de cara. Às vezes, uma alteração acontece em um ponto do código e só gera um erro muito tempo depois, em outra parte do sistema. Quando isso acontece, a manutenção se torna uma verdadeira investigação criminal.
Um Exemplo na Prática
Imagine que temos um sistema que gerencia pedidos de um e-commerce. Aqui está um trecho de código problemático:
import java.util.ArrayList;
import java.util.List;
class Order {
public List<String> items = new ArrayList<>();
public boolean isProcessed = false;
}
public class Main {
public static void main(String[] args) {
Order order = new Order();
order.items.add("Notebook");
order.items.add("Mouse");
System.out.println("Itens do pedido: " + order.items);
// Em algum outro ponto do código, um programador acidentalmente faz isso:
processOrder(order);
// Em outro lugar do sistema, tentamos adicionar mais itens ao pedido...
order.items.add("Teclado"); // 🚨 Opa! Mas o pedido já foi processado!
System.out.println("Pedido processado? " + order.isProcessed);
System.out.println("Itens do pedido: " + order.items);
}
public static void processOrder(Order order) {
order.isProcessed = true;
System.out.println("Pedido processado com sucesso!");
}
}
O Que Pode Dar Errado Aqui?
Se rodarmos esse código, teremos essa saída:
Itens do pedido: [Notebook, Mouse]
Pedido processado com sucesso!
Pedido processado? true
Itens do pedido: [Notebook, Mouse, Teclado] <-- Isso não deveria ser possível!
Percebe o problema? O pedido foi marcado como processado, mas ainda conseguimos adicionar itens nele!
E aqui está o grande perigo:
1. Esse erro não é evidente – não há uma exceção sendo lançada, o sistema continua funcionando “normalmente”.
2. Dependendo do sistema, pode gerar um problema sério – no mundo real, isso pode fazer com que um cliente pague por algo que nunca foi adicionado à fatura, ou pior, que um item seja enviado sem estar no pedido oficial.
3. É um bug difícil de rastrear – porque a falha acontece em um ponto, mas só causa um problema mais tarde.
Agora imagine esse erro em produção, impactando clientes. Como corrigimos isso sem quebrar outras partes do código? Como garantimos que novos desenvolvedores na equipe não repitam esse erro?
Como Encapsulamento Resolve Isso?
Agora, vamos corrigir esse código protegendo os atributos da classe Order. Assim, garantimos que nenhum item pode ser adicionado após o pedido ser processado:
import java.util.ArrayList;
import java.util.List;
class Order {
private List<String> items = new ArrayList<>();
private boolean isProcessed = false;
public void addItem(String item) {
if (isProcessed) {
throw new IllegalStateException("Não é possível adicionar itens a um pedido já processado.");
}
items.add(item);
}
public List<String> getItems() {
return new ArrayList<>(items); // Retorna uma cópia para evitar modificações diretas
}
public void processOrder() {
if (items.isEmpty()) {
throw new IllegalStateException("Não é possível processar um pedido sem itens.");
}
isProcessed = true;
System.out.println("Pedido processado com sucesso!");
}
public boolean isProcessed() {
return isProcessed;
}
}
public class Main {
public static void main(String[] args) {
Order order = new Order();
order.addItem("Notebook");
order.addItem("Mouse");
System.out.println("Itens do pedido: " + order.getItems());
// Agora processamos o pedido
order.processOrder();
// Tentamos adicionar mais itens depois de processado
try {
order.addItem("Teclado");
} catch (IllegalStateException e) {
System.out.println("Erro: " + e.getMessage());
}
System.out.println("Pedido processado? " + order.isProcessed());
System.out.println("Itens do pedido: " + order.getItems());
}
}
Agora, O Que Mudou?
Se rodarmos esse código, a saída será:
Itens do pedido: [Notebook, Mouse]
Pedido processado com sucesso!
Erro: Não é possível adicionar itens a um pedido já processado.
Pedido processado? true
Itens do pedido: [Notebook, Mouse]
Agora, se tentarmos adicionar um item após o pedido ter sido processado, recebemos um erro claro.
Por Que Isso Importa na Manutenção?
Agora pense no seguinte cenário:
Imagine que um novo desenvolvedor entre na equipe e precise mexer nesse código. Qual versão será mais fácil de entender e manter?
• A primeira versão? Onde qualquer parte do código pode modificar os dados sem nenhuma restrição, tornando os bugs difíceis de rastrear?
• Ou a segunda versão? Onde as regras de negócio estão claramente definidas e protegidas dentro da classe Order
?
A segunda opção não só reduz o risco de bugs, mas também torna o código mais previsível e fácil de manter.
Quanto menos pontos no sistema puderem modificar um estado crítico, menor a chance de bugs inesperados surgirem.
Proteja Seus Dados, Proteja Seu Tempo
A falta de encapsulamento não só cria bugs difíceis de rastrear, mas também torna a manutenção mais cara, porque cada mudança requer mais tempo e esforço para garantir que nada quebre.
Encapsular os dados corretamente não é só sobre segurança, é sobre manter a sanidade da equipe de desenvolvimento. Porque no final das contas, o que sai mais barato: investir em código bem estruturado ou gastar semanas apagando incêndios?
Se você já passou horas tentando entender um bug que parecia “mágico”, pode ter certeza: a falta de encapsulamento teve um papel nisso.
A Falta de Encapsulamento e o Aumento da Carga Cognitiva
Você já teve a sensação de olhar para um código e simplesmente não conseguir entender o que ele faz sem abrir várias outras classes e métodos? Ou pior, já passou um bom tempo tentando entender por que um valor está errado, só para descobrir que ele foi modificado por algum lugar inesperado?
Isso acontece porque o código está exigindo uma carga cognitiva alta para ser compreendido.
O Que é Carga Cognitiva?
O termo carga cognitiva se refere à quantidade de esforço mental que precisamos para entender algo. Em software, quanto mais informações precisamos manter na cabeça ao mesmo tempo para entender um código, maior a carga cognitiva.
John Ousterhout, no livro A Philosophy of Software Design, define carga cognitiva como:
“A complexidade de um sistema de software é determinada pela quantidade de informação que um programador deve manter na mente ao trabalhar nele.”
Ou seja, quanto mais detalhes você precisa lembrar e rastrear mentalmente para entender um código, mais difícil e custosa se torna a manutenção.
E sabe um dos grandes vilões que aumentam essa carga cognitiva? A falta de encapsulamento.
O Problema de Modificar Estados Sem Controle
Imagine um sistema onde temos uma classe User, mas deixamos suas propriedades expostas com setters gerados automaticamente. Isso significa que qualquer parte do código pode modificar os atributos de um usuário sem nenhuma validação.
Aqui está um exemplo com Lombok, uma biblioteca popular no Java que gera automaticamente getters e setters:
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
class User {
private String name;
private int age;
}
public class Main {
public static void main(String[] args) {
User user = new User();
user.setName("Alice");
user.setAge(25);
System.out.println(user.getName() + " tem " + user.getAge() + " anos.");
// Em algum outro lugar do código...
user.setAge(-5); // 🚨 Opa! Idade negativa?! Quem fez isso?
System.out.println(user.getName() + " tem " + user.getAge() + " anos.");
}
}
Saída esperada:
Alice tem 25 anos.
Alice tem -5 anos. <-- Bug silencioso
O Que Deu Errado Aqui?
O problema está na falta de controle sobre as modificações.
1. A idade foi definida como negativa, algo que não faz sentido no mundo real, mas o código aceita porque não há restrições no setAge().
2. Qualquer parte do código pode modificar diretamente os atributos sem passar por uma regra de negócio centralizada.
3. Se um bug aparecer, encontrar onde a idade foi alterada será difícil, pois qualquer parte do sistema pode ter chamado setAge().
Agora, imagine que esse erro ocorre em um sistema de folha de pagamento, onde a idade influencia cálculos de benefícios. Esse simples descuido pode propagar um erro em várias partes do software.
E para piorar, a carga cognitiva de manter esse código aumenta. Os desenvolvedores precisam lembrar todas as maneiras possíveis de modificar age, em vez de terem um único ponto central onde essa lógica pode ser controlada.
Corrigindo com Encapsulamento
Agora, vamos proteger a integridade do objeto, eliminando setAge() e garantindo que a idade sempre esteja válida.
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
setAge(age); // Chamamos o setter validado
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Idade não pode ser negativa.");
}
this.age = age;
}
}
public class Main {
public static void main(String[] args) {
User user = new User("Alice", 25);
System.out.println(user.getName() + " tem " + user.getAge() + " anos.");
// Tentamos definir uma idade inválida
try {
user.setAge(-5);
} catch (IllegalArgumentException e) {
System.out.println("Erro: " + e.getMessage());
}
System.out.println(user.getName() + " tem " + user.getAge() + " anos.");
}
}
Agora, O Que Mudou?
Se rodarmos esse código, a saída será:
Alice tem 25 anos.
Erro: Idade não pode ser negativa.
Alice tem 25 anos.
Agora, o sistema impede modificações erradas antes que elas se tornem um problema.
Por Que Isso Importa Para a Agilidade da Equipe?
Agora pense em uma equipe grande trabalhando nesse sistema. Com a primeira versão do código (@Setter do Lombok), qualquer desenvolvedor pode modificar age diretamente, e o impacto pode ser imprevisível.
Quando há um bug relacionado a isso, encontrar onde exatamente esse erro ocorreu pode levar horas, porque qualquer parte do sistema pode ter chamado setAge()
de forma errada.
Com a versão encapsulada, existe apenas um único ponto no código que pode modificar age, e esse ponto já protege contra valores inválidos.
Isso significa que:
✔ Menos risco de erros
✔ Menos tempo gasto depurando problemas
✔ Menos carga cognitiva para os desenvolvedores entenderem o código
Lombok: Amigo ou Inimigo?
Lombok é uma ferramenta poderosa, mas gerar setters automaticamente sem pensar nas implicações pode ser um tiro no pé.
A ideia de encapsulamento é justamente evitar acesso descontrolado aos dados, então expor todos os atributos com setters públicos vai contra esse princípio.
💡 Dica: Use @Getter para leitura, mas evite @Setter indiscriminadamente. Prefira criar métodos específicos que garantam que os dados sejam modificados corretamente.
Encapsulamento Reduz Carga Cognitiva e Evita Bugs
Encapsulamento não é só um conceito bonito – ele reduz a carga cognitiva dos desenvolvedores e evita bugs difíceis de rastrear.
Se os dados podem ser modificados de qualquer lugar do código, os programadores precisam manter mais informações na cabeça para entender como o sistema funciona. Isso torna o código mais difícil de manter e aumenta os custos da equipe.
Se John Ousterhout estiver certo (e ele está), então quanto menos você precisar lembrar para entender o código, melhor será seu design de software.
A próxima vez que você ver um set()
automático, pergunte-se:
• Esse atributo realmente precisa ser alterado diretamente?
• Existe alguma validação que deveria ser feita antes?
• Isso pode gerar estados inconsistentes?
Se a resposta para qualquer uma dessas perguntas for sim, então o encapsulamento vai te salvar de muita dor de cabeça.
Encapsulamento É Controle, Não Apenas Restrição
No fim das contas, encapsulamento não significa esconder dados por esconder, mas sim garantir que o objeto seja usado corretamente.
Isso envolve:
✅ Definir regras claras para modificar um objeto.
✅ Garantir que o estado do objeto sempre permaneça válido.
✅ Evitar expor detalhes internos que criam dependências desnecessárias.
Se o encapsulamento for apenas colocar private
e expor getters
e setters
sem controle algum, então ele é uma ilusão.
Encapsular bem é proteger o código contra seu pior inimigo: o próprio desenvolvedor.1
Encapsulamento e Error Driven Development: Como Tudo Se Conecta?
Você pode estar se perguntando: “Ok, falamos bastante sobre encapsulamento… mas como isso tudo se relaciona com Error Driven Development?”
Ótima pergunta.
A resposta está no pensamento de curto prazo que domina a indústria de software.
O Fast Software e a Cultura da Pastelaria
Vivemos na era do Fast Software. A velocidade de entrega é tudo.
A agilidade e a pressa, por si só, não são o problema. O problema é a pressa sem responsabilidade.
O que vemos em muitas empresas é o modelo “pastelaria de software”:
• O foco está em entregar rápido e não em entregar bem.
• “Dá seus pulos e faz funcionar.”
• O código está frágil, mas ninguém vê isso no início.
• Quando os problemas aparecem, o time apaga incêndios em vez de resolver as causas.
• Ninguém consegue tempo para refletir se o código está fácil de manter, entender ou corrigir.
Resultado? O que parecia um atalho para acelerar a entrega vira um pesadelo de manutenção.
O projeto fica mais caro com o tempo porque pequenas mudanças se tornam difíceis, os bugs demoram mais para serem corrigidos e os desenvolvedores gastam mais tempo tentando entender o que está acontecendo.
Esse é o Error Driven Development na sua forma mais pura: o código cresce sem estrutura e só corrigimos os problemas depois que os erros aparecem.
O Preço de Ignorar Princípios Básicos
No início deste artigo, falamos que Error Driven Development não é um método, é uma crítica.
Uma crítica ao fato de que muitas empresas e programadores simplesmente ignoram princípios básicos da programação.
E quando falamos de princípios básicos, não estamos falando de micro-otimizações ou padrões sofisticados. Estamos falando de:
✅ Encapsular dados para evitar estados inválidos.
✅ Usar nomes claros para que o código seja fácil de entender.
✅ Segregar responsabilidades para evitar classes que fazem tudo.
✅ Criar testes para pegar problemas antes que eles cheguem aos clientes.
Mas, no “Fast Software”, essas preocupações são vistas como “perfumaria”.
“Depois a gente refatora.”
“Testes? A gente testa no usuário.”
“Encapsulamento? Ah, só adiciona um setter e segue o jogo.”
Até que um dia… a bomba explode.
• Bugs começam a aparecer com mais frequência.
• O tempo para resolver problemas aumenta exponencialmente.
• Ninguém entende mais como o código funciona.
• Cada pequena alteração parece quebrar outra coisa inesperada.
• A empresa começa a perder dinheiro e reputação.
E o pior? Quando alguém sugere que “talvez devêssemos programar direito desde o começo”, a resposta é: “Agora não dá tempo.”
Os Programadores Precisam Deixar Isso Claro
Como desenvolvedores, não podemos nos acomodar nesse ciclo. É nosso papel tornar isso explícito em reuniões técnicas, documentações e decisões estratégicas.
Se a empresa quer velocidade, ótimo. Mas não podemos sacrificar os princípios fundamentais da programação em nome de prazos irreais.
Se necessário, a conversa precisa ser direta:
💬 “Nosso código não é fácil de manter. Cada mudança leva tempo demais.”
💬 “Se não estruturarmos isso direito agora, cada novo recurso vai ser um parto.”
💬 “Podemos perder dinheiro e reputação porque os bugs estão saindo do controle.”
Software ruim não é apenas um problema técnico. Ele tem impacto financeiro direto.
E no final do dia, quem paga o preço de um código ruim não é só a empresa, são os programadores que têm que lidar com ele.
Segregação de Comportamentos e o Próximo Perigo
Se a falta de encapsulamento pode deixar o código inconsistente e difícil de entender, outro problema ignorado é a ausência de segregação de comportamentos.
Quando misturamos regras de negócio, persistência, lógica de apresentação e tudo mais dentro de uma mesma classe, o sistema se torna um caos absoluto.
No próximo tópico, vamos explorar como esse erro é cometido diariamente e como podemos evitá-lo.
A Falta de Segregação de Comportamentos
Se tem uma coisa que acontece o tempo todo no desenvolvimento de software, é a bagunça de responsabilidades dentro de uma mesma Service ou Caso de Uso.
Afinal, todo mundo já viu (ou até escreveu) aquele “Deus Service” – um arquivo de centenas (às vezes milhares) de linhas, onde tudo acontece. Regras de negócio, persistência, chamadas a APIs externas, manipulação de strings, geração de PDFs… tudo misturado no mesmo lugar. Algumas coisas simplesmente não deveriam estar no mesmo lugar.
O Que Acontece Quando Misturamos Comportamentos Sem Pensar?
O problema é que, na correria do dia a dia, muitas vezes não paramos para nos perguntar:
• Os comportamentos que estou agrupando realmente pertencem ao mesmo conjunto de negócio?
• Ou estou jogando tudo dentro da mesma classe só porque parece conveniente agora?
Se não refletimos sobre isso, acabamos criando código que não faz sentido do ponto de vista de negócio. E isso tem consequências sérias:
1. Classes gigantescas e difíceis de entender – Se um arquivo tem 800 linhas, boa sorte para encontrar onde realmente está a regra que precisa de manutenção.
2. Acoplamento desnecessário – Um comportamento depende de outro sem necessidade, o que torna mudanças arriscadas.
3. Dificuldade de testar – Testar um serviço monolítico é um pesadelo, porque cada teste precisa lidar com múltiplas responsabilidades.
4. Reuso zero – Como tudo está interligado, reaproveitar um único comportamento exige carregar junto um monte de código que não tem nada a ver.
O Serviço Que Faz de Tudo
Vamos imaginar um e-commerce. Uma OrderService poderia ser algo assim:
• Criar o pedido
• Validar o estoque
• Calcular impostos
• Gerar a fatura
• Processar o pagamento
• Enviar e-mails de confirmação
• Atualizar o sistema de rastreamento de entregas
💡 Parece tudo relacionado, certo? Mas vamos parar e pensar: será que todas essas funções deveriam estar na mesma classe?
Porque se tudo estiver em um único serviço, você acaba com uma classe que…
• Depende do banco de dados para buscar o pedido
• Depende da API de pagamentos para processar a compra
• Depende de um gerador de faturas
• Depende de um serviço de e-mails
Isso amarra todo o código junto e dificulta qualquer mudança.
Como Pensar na Segregação de Comportamentos?
A segregação de comportamentos exige pensar no código com a mentalidade do negócio, e não apenas como uma sequência de instruções a serem executadas.
Aqui estão algumas diretrizes para isso:
1️⃣ Identifique o Domínio Central
Antes de escrever código, entenda qual é a unidade central de negócio.
Exemplo: no caso de um e-commerce, um pedido (Order) é uma entidade central, e várias ações podem ser feitas em cima dele: criar, pagar, cancelar, enviar etc.
Cada uma dessas ações não precisa estar dentro do mesmo serviço.
2️⃣ Evite Aglutinar Responsabilidades Só Porque Elas Ocorrem em Sequência
Uma das falhas mais comuns é agrupar comportamentos porque eles ocorrem um após o outro. Mas isso não significa que pertencem ao mesmo lugar.
📌 Erro comum:
“Esse método sempre vem depois desse, então vou deixar os dois juntos na mesma classe.”
Essa lógica pode criar dependências desnecessárias. A criação do pedido, por exemplo, não deveria saber nada sobre pagamento. O pagamento acontece depois, mas pertence a um outro contexto.
3️⃣ Crie Serviços Mais Enxutos e Especializados
Se a sua service está ficando gigante, provavelmente ela está assumindo mais responsabilidades do que deveria.
Dicas práticas:
✅ Divida regras de negócio em casos de uso menores e específicos.
✅ Evite services que fazem de tudo – prefira serviços especializados, como OrderCreationService
, PaymentService
, InvoiceService
.
✅ Não misture camadas – uma regra de negócio não deveria estar fazendo chamada direta ao banco de dados ou enviando e-mails.
4️⃣ Mantenha Cada Comportamento No Seu Lugar
Vamos voltar para a analogia da cozinha e do banheiro.
Por que o banheiro não tem uma porta para a cozinha? Porque são ambientes diferentes, com propósitos diferentes.
O mesmo vale para o código:
• Processar um pagamento é um comportamento específico.
• Gerar um e-mail é um comportamento diferente.
• Criar um pedido é outra coisa.
Cada um desses comportamentos deve ter seu próprio espaço, sem depender diretamente do outro.
Isso evita que alterações em um lugar quebrem funcionalidades em outro.
E Se Alguém Reclamar Que “Ah, Mas Isso é Só Separação de Código”?
Segregar responsabilidades não é só uma questão de organização visual.
É uma estratégia para reduzir acoplamento, facilitar testes, melhorar manutenção e evitar surpresas na produção.
Se a empresa quer velocidade e qualidade ao mesmo tempo, precisamos construir sistemas que sejam fáceis de evoluir, e não apenas fáceis de entregar na primeira versão.
Separe as Coisas Certas
A segregação de comportamentos não significa criar centenas de classes pequenas sem sentido. O objetivo é manter juntos apenas os comportamentos que fazem sentido juntos.
Antes de juntar duas funções dentro do mesmo serviço, pergunte-se:
• Esse comportamento realmente faz parte da mesma regra de negócio?
• Se eu precisar alterar um, o outro será impactado?
• O acoplamento entre essas funções está dificultando mudanças futuras?
No próximo tópico, vamos explorar como uma abstração mal feita pode levar a confusão do uso dessa abstração e ao alto acoplamento.
O Perigo das Abstrações Mal Feitas e o Alto Acoplamento
Nem toda abstração é uma boa abstração. Às vezes, ao invés de simplificar, ela adiciona camadas desnecessárias e confunde os programadores.
No artigo,
falo sobre como o objetivo de uma boa abstração é esconder os detalhes certos, reduzindo a complexidade sem comprometer a flexibilidade.
E essa frase do livro The Pragmatic Programmer, de Andrew Hunt e David Thomas, resume isso perfeitamente:
“As melhores abstrações são aquelas que você mal percebe; elas simplesmente funcionam.”
E ainda mais importante:
“A chave para uma boa abstração é esconder os detalhes certos – aqueles que podem mudar com frequência ou complicar a interface – enquanto mantém o sistema simples e robusto.”
Mas quando a abstração não esconde os detalhes certos, ela se torna frágil e enganosa, expondo o programador a mais problemas do que soluções.
Uma Abstração Que Deveria Ajudar… Mas Só Atrapalha
Imagine que você precisa trabalhar com arquivos em disco.
Para facilitar, uma biblioteca oferece uma classe FileManager, que promete uma interface simples para ler e escrever arquivos. Parece útil, certo?
Agora, veja essa implementação de como essa abstração pode ser mal feita:
class FileManager {
private String filePath;
public FileManager(String filePath) {
this.filePath = filePath;
}
public void writeToFile(String data) throws Exception {
java.io.FileWriter writer = new java.io.FileWriter(filePath);
writer.write(data);
writer.close();
}
public String readFromFile() throws Exception {
java.io.FileReader reader = new java.io.FileReader(filePath);
char[] buffer = new char[1024];
int length = reader.read(buffer);
reader.close();
return new String(buffer, 0, length);
}
}
O Que Está Errado Aqui?
Essa abstração deveria facilitar a manipulação de arquivos, mas na prática, ela cria mais problemas do que resolve:
1. Falta de Controle de Erros – Se o arquivo não existir ou houver um erro de leitura/escrita, o programador que usa essa classe precisa lidar com exceções manualmente.
2. Alto Acoplamento ao Caminho do Arquivo – A instância de FileManager é sempre criada para um arquivo específico. Se precisar manipular vários arquivos ao mesmo tempo, será necessário instanciar múltiplos objetos.
3. Recriação do Arquivo ao Escrever – A cada chamada de writeToFile(), o arquivo é sobrescrito. Se o programador quiser adicionar conteúdo em vez de apagar o anterior, ele precisará descobrir sozinho que essa abstração não suporta append e terá que modificar a implementação.
4. Baixa Flexibilidade – E se quisermos armazenar os dados não em um arquivo físico, mas em um banco de dados ou na memória? Essa classe não nos dá escolha.
Agora, imagine um desenvolvedor novo no projeto. Ele vê FileManager e assume que é uma boa abstração, mas depois descobre que precisa lidar com detalhes inesperados, como exceções que não foram tratadas e a impossibilidade de manter múltiplos arquivos sem recriar instâncias.
A abstração, que deveria esconder os detalhes técnicos, acabou expondo problemas internos e tornando o código mais difícil de usar.
Como Corrigir Essa Abstração?
Agora, vamos melhorar a implementação para que realmente esconda detalhes complexos e forneça uma interface intuitiva e fácil de trabalhar para o programador.
import java.util.List;
public interface IFileManager {
void writeToFile(String data, boolean append);
void writeLines(List<String> lines, boolean append);
String readFromFile();
List<String> readLines();
void writeBytes(byte[] data);
byte[] readBytes();
boolean deleteFile();
}
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
class FileManager implements IFileManager {
private final Path filePath;
public FileManager(String filePath) {
this.filePath = Paths.get(filePath);
ensureFileExists();
}
private void ensureFileExists() {
try {
if (filePath.getParent() != null) {
Files.createDirectories(filePath.getParent());
}
if (!Files.exists(filePath)) {
Files.createFile(filePath);
}
} catch (IOException e) {
throw new FileManagerException("Erro ao criar o arquivo ou diretório: " + filePath, e);
}
}
@Override
public void writeToFile(String data, boolean append) {
try {
StandardOpenOption option = append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING;
Files.writeString(filePath, data + System.lineSeparator(), option, StandardOpenOption.CREATE);
} catch (IOException e) {
throw new FileManagerException("Erro ao escrever no arquivo: " + filePath, e);
}
}
@Override
public void writeLines(List<String> lines, boolean append) {
try {
StandardOpenOption option = append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING;
Files.write(filePath, lines, option, StandardOpenOption.CREATE);
} catch (IOException e) {
throw new FileManagerException("Erro ao escrever no arquivo: " + filePath, e);
}
}
@Override
public String readFromFile() {
try {
return Files.exists(filePath) ? Files.readString(filePath).trim() : "";
} catch (IOException e) {
throw new FileManagerException("Erro ao ler o arquivo: " + filePath, e);
}
}
@Override
public List<String> readLines() {
try {
return Files.exists(filePath) ? Files.readAllLines(filePath) : List.of();
} catch (IOException e) {
throw new FileManagerException("Erro ao ler linhas do arquivo: " + filePath, e);
}
}
@Override
public void writeBytes(byte[] data) {
try {
Files.write(filePath, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
throw new FileManagerException("Erro ao escrever bytes no arquivo: " + filePath, e);
}
}
@Override
public byte[] readBytes() {
try {
return Files.exists(filePath) ? Files.readAllBytes(filePath) : new byte[0];
} catch (IOException e) {
throw new FileManagerException("Erro ao ler bytes do arquivo: " + filePath, e);
}
}
@Override
public boolean deleteFile() {
try {
return Files.deleteIfExists(filePath);
} catch (IOException e) {
throw new FileManagerException("Erro ao deletar o arquivo: " + filePath, e);
}
}
}
Agora temos:
1️⃣ Uso de Interface (IFileManager) → Contrato Claro
✅ A interface IFileManager define um contrato explícito para qualquer implementação futura, garantindo que qualquer classe que precise manipular arquivos possa seguir a mesma estrutura.
✅ Facilita a substituição ou extensão da implementação (FileManager) sem impactar outras partes do código.
✅ Permite criar implementações alternativas (ex.: CloudFileManager para armazenamento na nuvem, DatabaseFileManager para salvar em banco, etc.).
2️⃣ Princípio da Responsabilidade Única (SRP - Single Responsibility Principle)
✅ A classe FileManager está focada exclusivamente em manipulação de arquivos, sem misturar outras responsabilidades.
✅ Isso facilita a manutenção e entendimento do código.
3️⃣ Desacoplamento e Flexibilidade
✅ Como FileManager implementa IFileManager, podemos facilmente trocar sua implementação sem modificar o código que a utiliza.
✅ O código cliente não precisa saber se os dados estão sendo armazenados em um arquivo local, banco de dados, ou serviço externo.
Esse é um exemplo de uma abstração que realmente ajuda o desenvolvedor, ao invés de expô-lo a problemas que deveria esconder.
5️⃣ Métodos Abrangentes e Funcionais
✅ Manipulação de arquivos de texto e binários
• writeToFile(), writeLines(), readFromFile(), readLines()
→ Arquivos de texto.
• writeBytes(), readBytes()
→ Suporte a arquivos binários.
✅ Exclusão segura do arquivo
• deleteFile()
remove o arquivo se ele existir e retorna true ou false, evitando exceções desnecessárias.
✅ Evita valores nulos retornando defaults apropriados
• readFromFile()
retorna "" (string vazia) se o arquivo não existir.
• readLines()
retorna List.of()
(lista vazia).
• readBytes()
retorna new byte[0].
Quando Criar (ou Não Criar) uma Abstração?
Nem toda abstração é necessária. Às vezes, é melhor usar diretamente uma API bem projetada do que criar uma camada adicional que só complica as coisas.
Antes de criar uma abstração, pergunte-se:
1. Estou realmente escondendo a complexidade ou apenas adicionando uma camada extra?
2. A abstração reduz a carga cognitiva do desenvolvedor ou apenas desloca o problema?
3. Essa abstração é flexível o suficiente para mudanças futuras sem precisar ser completamente reescrita?
4. Ela expõe apenas o necessário ou força os desenvolvedores a lidar com detalhes internos?
Se a resposta para qualquer uma dessas perguntas for “não”, então talvez a abstração não seja a melhor escolha.
Precisamos entender que quando bem feitas, as abstrações simplificam o desenvolvimento e tornam o código mais fácil de entender e manter.
Quando mal feitas, elas confundem os programadores e adicionam complexidade desnecessária – exatamente o oposto do que deveriam fazer.
Como disse Robert C. Martin:
“A abstração é a eliminação do irrelevante e a amplificação do essencial.”
Então, da próxima vez que pensar em criar (ou usar) uma abstração, reflita: essa camada está realmente ajudando ou apenas complicando o código?
Tá estamos entendendo como Error Driven Development está interligado com um monte de coisas… mas como isso tudo gera alto acoplamento?
Como Uma Abstração Mal Feita Pode Acoplar Classes e Dificultar a Manutenção?
Imagine que você está organizando um grande evento e precisa contratar fornecedores para diferentes partes da festa: um para o buffet, outro para a decoração, um terceiro para a música e assim por diante.
Agora, pense nesse cenário desastroso: o fornecedor do buffet decide incluir no contrato a obrigação de fornecer o sistema de som, porque "faz sentido para ele". O fornecedor da decoração exige que todas as cadeiras venham do mesmo lugar que os pratos, porque "é mais conveniente".
Isso significa que:
1️⃣ Se quisermos trocar o DJ, talvez precisemos trocar o fornecedor de comida junto.
2️⃣ Se quisermos mudar o tipo de cadeira, pode ser necessário renegociar toda a decoração.
3️⃣ Qualquer pequena mudança gera um efeito cascata, exigindo ajustes em várias partes que deveriam ser independentes.
Agora, traga essa ideia para o software.
Quando uma abstração é mal feita, ela amarra funcionalidades que deveriam ser independentes. Isso significa que qualquer mudança mínima em uma parte do código pode impactar várias outras classes, mesmo quando elas não deveriam ter relação direta.
O resultado? Um sistema onde toda modificação se torna um risco, porque as dependências estão interligadas de forma confusa e desnecessária. Se um simples ajuste exige revisar múltiplas classes que, em teoria, nem deveriam estar relacionadas, isso é um claro sinal de alto acoplamento gerado por uma abstração ruim.
Aonde Isso Nos Leva no Código?
Quando uma abstração mal planejada cria dependências rígidas entre classes, o sistema se torna difícil de manter por vários motivos:
🔗 Acoplamento Desnecessário: Se uma classe depende diretamente dos detalhes internos de outra, qualquer modificação nela exige que todas as classes que a utilizam também sejam ajustadas.
⚠️ Menos Flexibilidade: O código se torna difícil de evoluir porque não podemos simplesmente modificar ou substituir uma parte do sistema sem afetar o resto.
🔄 Dívida Técnica Crescente: Com o tempo, desenvolvedores começam a criar gambiarras para evitar mexer na abstração mal feita, resultando em código cada vez mais difícil de entender e modificar.
O Resultado? Manutenção Caótica e Custo Alto
Agora, imagine que uma empresa precisa mudar um fornecedor porque encontrou um motor mais eficiente para os carros. Se os motores estivessem independentes, bastaria substituir um pelo outro. Mas como o motor veio acoplado ao chassi e aos pneus, a mudança se torna um projeto enorme e caro.
No software, acontece a mesma coisa. Se uma funcionalidade deveria ser independente, mas foi mal abstraída e está fortemente acoplada a outras partes do sistema, qualquer alteração mínima vira um pesadelo.
O resultado?
📌 Desenvolvedores gastam horas tentando entender todas as interdependências.
📌 Pequenas mudanças exigem modificações em várias partes do sistema.
📌 O time evita refatorar porque qualquer mudança pode quebrar tudo.
📌 A dívida técnica cresce e compromete a evolução do software.
Se você já sentiu esse desespero ao mexer em um código que parece uma teia de aranha onde tocar em um lugar quebra outro, você já sofreu com uma abstração mal feita que gerou alto acoplamento.
E Como Isso Se Conecta ao Error Driven Development?
Talvez agora você esteja pensando:
“Beleza, entendi que abstrações ruins e alto acoplamento são problemas… mas será que isso realmente tem alguma conexão com Error Driven Development?”
Talvez eu esteja exagerando. Talvez eu esteja enxergando um problema maior do que ele realmente é. Mas pensa comigo:
• Quanto tempo você já perdeu por causa de uma abstração que mais atrapalhava do que ajudava?
• Já precisou entender uma classe gigante com responsabilidades misturadas só para fazer uma alteração simples?
• Quantas vezes uma dependência inesperada entre classes transformou uma mudança pequena em um efeito dominó, quebrando outras partes do sistema?
• Já ficou travado numa sprint porque não conseguia desacoplar um código que deveria ser simples?
• Já teve que fazer horas extras tentando entender como modificar algo sem quebrar o resto?
• Ou pior, já introduziu mais complexidade do que deveria só para conseguir entregar dentro do prazo?
Se você já passou por qualquer um desses cenários, então você já viveu o Error Driven Development.
Error Driven Development é Sobre Tempo, Recursos e Dinheiro
O que acontece quando as decisões de design de código são ruins?
O que era para ser uma simples alteração em um módulo isolado se torna um caos porque as classes estão amarradas umas às outras.
O que deveria ser uma abstração útil vira um pesadelo porque exige que você saiba mais detalhes internos do que deveria.
O que deveria ser um sistema flexível se torna uma dor de cabeça porque qualquer mudança impacta diversas partes inesperadas do código.
E então, de um problema técnico, a coisa escala para um problema financeiro:
📌 Mudanças levam mais tempo do que o planejado.
📌 O time perde produtividade tentando entender um código confuso.
📌 O sistema se torna mais frágil e propenso a bugs.
📌 A empresa começa a gastar mais dinheiro com retrabalho e suporte.
Quando a engenharia não consegue enxergar os erros que está cometendo, o time se vê preso num ciclo onde os erros se tornam o guia do desenvolvimento – não porque eles estão sendo corrigidos de forma estruturada, mas porque eles ditam as prioridades. Esse é o Error Driven Development.
E Como Quebrar Esse Ciclo?
A única forma de evitar cair no Error Driven Development é tomar boas decisões antes que os problemas apareçam.
O grande problema é que muitas vezes não percebemos os erros de design no momento em que estamos escrevendo o código. Tudo parece estar funcionando bem… até que chega a hora de testar. E aí vem a surpresa: testar pequenas partes do sistema se torna um pesadelo.
Se um código está bem estruturado, ele deveria ser fácil de testar. Mas quando um simples teste exige configurar várias dependências, preparar cenários complicados ou até interagir com recursos externos, isso é um sinal claro de que o design do código está errado.
Agora pense na bagunça que isso pode causar:
📌 Testes falhando por dependerem de variáveis externas que não controlamos.
📌 Testes demorando mais do que o necessário porque exigem acesso a sistemas ou operações caras.
📌 Testes quebrando de forma imprevisível, dependendo do ambiente onde rodam.
Tudo isso porque o código está acoplado demais a detalhes específicos e não permite isolar o que realmente deve ser testado.
Agora, e se em vez de depender diretamente dessas implementações, o código fosse projetado de forma a trabalhar apenas com contratos bem definidos, garantindo que a lógica de negócio possa ser testada de forma independente? O que aconteceria?
O código se tornaria mais flexível.
Poderíamos testar sem precisar lidar com dependências desnecessárias.
A manutenção ficaria mais fácil, pois cada parte do sistema poderia ser validada sem precisar replicar um ambiente completo.
Com essa mudança, conseguimos escrever testes rápidos e confiáveis, sem precisar criar um ambiente artificialmente complicado só para validar uma simples funcionalidade.
E essa é a grande lição aqui: testabilidade é um reflexo direto da qualidade do design do código.
Se testar algo exige malabarismos, configurações infinitas e um ritual completo de preparação, é porque o código não está bem desenhado.
Testes de Unidade São o Raio-X do Design do Código
Testes de unidade fazem muito mais do que garantir que um sistema funciona. Eles são uma ferramenta poderosa para expor problemas estruturais no código. Se algo é difícil de testar, isso não é um problema do teste – é um problema do design do código.
Aqui estão alguns sinais de alerta que os testes podem revelar:
1️⃣ Encapsulamento Frágil: Quando Testar um Objeto Exige Mexer Onde Não Deveríamos
Se, para testar um objeto, precisamos acessar diretamente seus atributos internos ou modificar estados de forma não controlada, isso indica um problema de encapsulamento.
Um bom encapsulamento protege a integridade dos dados, garantindo que qualquer modificação passe por regras bem definidas. Mas se os testes exigem que manipulemos diretamente propriedades que deveriam ser internas, isso revela que o código não está se protegendo contra mudanças indevidas.
Em outras palavras: se precisamos “forçar” um teste mexendo diretamente nos detalhes internos de um objeto, o encapsulamento falhou.
2️⃣ Abstrações Vazadas ou Mal Projetadas: Quando Testar se Torna Incompreensível
Uma abstração mal feita não esconde a complexidade – ela apenas a espalha pelo sistema.
Se um teste exige conhecer detalhes excessivos de como uma funcionalidade foi implementada, então a abstração não está cumprindo seu papel. O ideal é que os testes verifiquem o comportamento esperado e não o funcionamento interno do código.
Mas se, para escrever um teste, precisamos configurar um monte de detalhes que não deveriam importar para a funcionalidade testada, isso significa que a abstração vazou.
Uma abstração bem feita simplifica os testes. Uma abstração ruim torna os testes mais complexos do que deveriam ser.
3️⃣ Alto Acoplamento: Quando Mudar um Código Exige Refazer Meio Sistema
Um código com baixo acoplamento permite que mudanças em uma parte do sistema não quebrem testes de partes que não deveriam ser afetadas.
Se ao modificar uma classe, uma quantidade absurda de testes começa a falhar, isso indica que o sistema está mais interligado do que deveria.
Isso é um grande alerta de que:
📌 As dependências estão sendo arrastadas entre componentes sem necessidade.
📌 Pequenas mudanças exigem reescrita de testes que deveriam ser independentes.
📌 O código não foi projetado para ser flexível e sustentável.
O ideal é que um teste que verifica uma funcionalidade não precise ser reescrito só porque mudamos um detalhe interno de outra classe. Se isso acontece, o código está excessivamente acoplado.
Se Testar É Difícil, O Código Está Gritando Por Ajuda
Testes de unidade não servem apenas para capturar bugs – eles revelam falhas de design antes que elas causem problemas ainda maiores.
Se um teste parece exigir malabarismos, configurações excessivas ou acesso a detalhes internos, isso é um sinal de que o código precisa ser refatorado.
Ignorar esses sinais é seguir o caminho do Error Driven Development, onde o sistema só melhora depois que algo dá muito errado.
Um código bem testado não é um luxo, é uma necessidade. Sem testes, você apenas torce para que tudo continue funcionando.
Os testes nos mostram onde o design do código pode ser aprimorado. Basta estarmos dispostos a ouvir. 🚀
Conclusão
O Error Driven Development não acontece de uma hora para outra. Ele se instala silenciosamente quando não prestamos atenção nos fundamentos:
• Encapsulamento
• Segregação de comportamentos
• Abstrações bem feitas
• Baixo acoplamento
• Testes que garantem que o código é sustentável
A questão não é se vamos cometer erros. Isso é inevitável. A questão é como lidamos com esses erros.
Se esperamos que os problemas apareçam antes de tomar atitudes, então já estamos no Error Driven Development.
Mas se usamos testes, revisões e boas práticas para enxergar os problemas antes que eles afetem o sistema, estamos no caminho certo.
A escolha é sua: você quer construir software que cresce de forma sustentável ou quer continuar apagando incêndios?
Fico por aqui neste post! Se gostar do conteúdo compartilhe! Até o próximo!
Artigos relacionados:
Deixe os Testes Falarem: O Poder do Feedback no Ciclo de Desenvolvimento
Os testes são frequentemente vistos como a última linha de defesa entre o código recém-escrito e sua liberação. Mas o verdadeiro ‘ganho’ que conseguimos é quando começamos a "ouvir" o que os testes estão tentando nos dizer, indo além da simples verificação de passa/falha e mergulhando nos insights profundos que eles oferecem sobre a qualidade do nosso d…
Encapsulamento: Vendo Além da Superfície 🔍
Um dos pilares fundamentais da OOP se chama encapsulamento. Mas alguns desenvolvedores ainda não entendem o objetivo e o verdadeiro poder do encapsulamento. Espero que esse post possa ajudar a esclarecer melhor esse pilar tão importante! Se gostar do conteúdo, por favor, compartilhe e deixe seu like no post!
O Que É Abstração? Escondendo o Complexo, Mostrando o Essencial
Recentemente, me dei conta de algo curioso: explicar abstração de forma simples é muito mais difícil do que parece. Eu estava tentando explicar para um colega desenvolvedor o que, de fato, é uma abstração, e naquele momento percebi que, se eu não conseguia torná-lo compreensível, talvez eu mesmo não tivesse entendido completamente. Foi aí que decidi mer…
Encapsulamento Não É Só Manter as Propriedades Privadas
Muitos desenvolvedores acreditam que encapsulamento significa apenas declarar atributos como private
e expô-los apenas através de getters
e setters
. Mas essa visão é simplista e incompleta.
Encapsulamento não se trata apenas de esconder dados, mas de proteger a integridade do estado do objeto e controlar como ele pode ser modificado.
Porque, no final das contas, de nada adianta declarar campos privados se…
1️⃣ Os Getters e Setters Expostos Quebram a Proteção
Se todo atributo privado tem um getter e um setter público sem nenhuma validação, então o encapsulamento é praticamente inexistente.
Se um objeto expõe total controle sobre seus atributos, permitindo que qualquer parte do código altere seus valores sem restrições, então a única diferença entre usar private
e public
é sintática. O comportamento continua desprotegido.
Encapsulamento não significa "usar setters para tudo", mas sim definir regras claras para modificar um objeto.
2️⃣ O Objeto Pode Ser Colocado em Estados Inválidos
Um bom encapsulamento garante que o objeto sempre esteja em um estado válido.
Se não houver restrições no momento de modificar os atributos, então qualquer parte do código pode colocar o objeto em um estado inconsistente.
Por exemplo, imagine um sistema de pedidos onde um pedido pode ser marcado como "Entregue" antes de ter sido enviado. Se o código permite esse tipo de alteração arbitrária, o encapsulamento falhou, porque o próprio modelo de domínio está permitindo um estado inválido.
Encapsular bem significa definir quais estados são válidos e impedir transições que não fazem sentido.
3️⃣ O Objeto Expõe Detalhes Internos Que Não Deveria
Outro erro comum é permitir que outras partes do sistema dependam diretamente de detalhes internos do objeto, quebrando o encapsulamento de forma sutil.
Isso acontece quando:
📌 O objeto retorna referências diretas para estruturas internas mutáveis.
📌 O código externo precisa conhecer detalhes internos para interagir com o objeto corretamente.
📌 O comportamento esperado do objeto depende de convenções externas, em vez de estar bem definido dentro dele.
Se um objeto precisa expor detalhes demais para ser utilizado corretamente, ele não está encapsulando bem sua lógica.