Cada paradigma de programação possui seus pilares fundamentais. No caso da programação orientada a objetos (POO), os principais são: herança, polimorfismo, abstração e encapsulamento e aqui vou trazer alguns elementos relacionado a encapsulamento.
Variáveis (Atributos)
Na maioria das linguagens de programação baseadas no paradigma da orientação a objetos, é comum modelar classes com atributos privados (private) ou protegidos (protected) para restringir o acesso direto. No entanto, um erro frequente é tornar os atributos privados e, ao mesmo tempo, fornecer getters e setters para todos eles. Essa prática enfraquece o encapsulamento, pois, na prática, não há grande diferença entre acessar um atributo diretamente ou por meio de métodos simples.
É importante destacar que a visibilidade dos modificadores de acesso varia entre linguagens. No Java, por exemplo, um membro com protected pode ser acessado por classes do mesmo pacote (package-private) e por subclasses, independentemente do pacote. Já no Kotlin, protected limita o acesso apenas às subclasses, impedindo que outras classes do mesmo pacote o acessem diretamente.
Portanto, ao modelar classes, é essencial compreender as particularidades da linguagem escolhida para garantir um encapsulamento eficiente e coerente com a arquitetura do sistema.
public class Food {
private String id;
private String description;
private Product product;
private LocalDate received;
private LocalDate sampled;
public String getId() {
return id;
}
public String getDescription() {
return description;
}
public String getProduct() {
return product;
}
public String getReceived() {
return received;
}
public String getSampled() {
return sampled;
}
public void setId(String id) {
this.id = id;
}
public void setDescription(String description) {
this.description = description;
}
public void setProduct(Product product) {
this.product = product;
}
public void setReceived(LocalDate received) {
this.received = received;
}
public void setSampled(LocalDate sampled) {
this.sampled = sampled;
}
}Métodos (Comportamentos)
Adicionar métodos a um objeto vai além de simplesmente obter ou modificar valores de atributos. Mas aí você pode estar pensando: Entendi, mas como vou inicializar meu objeto?
Existem algumas formas de fazer isso, e a mais comum é o uso do construtor. Mas talvez você diga: Manoel, nem sempre tenho todas as informações no momento da criação do objeto. Vou precisar passar null para os atributos que ainda não tiverem valores?
A resposta rápida é: não necessariamente. Você pode, por exemplo, criar múltiplos construtores (vamos aprofundar melhor isso ao falar de polimorfismo). Outra abordagem bastante utilizada é o padrão de projeto Builder, um dos padrões do GoF (Gang of Four), que, particularmente, gosto bastante de aplicar.
O verdadeiro propósito do encapsulamento vai além de simplesmente criar métodos para alterar valores e esconder variáveis. Seu objetivo é prover comportamentos que modifiquem o estado do objeto de forma controlada. Isso é especialmente crítico em domínios complexos, onde a modelagem correta do comportamento do objeto impacta diretamente a integridade e a consistência do sistema.
Encapsulamento e Modelagem de Comportamentos
Como a POO busca simular o mundo real por meio de objetos, vejamos um exemplo:
Imagine uma classe Pessoa, que possui um estado representado pelos atributos energia e percentual de gordura. Se a energia da pessoa está baixa, como podemos alterá-la? Bastaria chamar setEnergia(100)?
No mundo real, isso não faz sentido. Em vez disso, estimulamos uma ação: comer. O método comer(Alimento alimento) encapsula essa ação. O que acontece dentro dele não precisa ser conhecido por quem o chama, mas sabemos que, ao final, a energia do objeto Pessoa será ajustada.
public class Pessoa {
private static final int MAX_ENERGY = 100;
private static final double KCAL_PER_KG_FAT = 7700.0;
private int energy;
private double fat;
public Pessoa(int energy, double fat) {
this.energy = Math.min(energy, MAX_ENERGY);
this.fat = Math.max(0, fat);
}
public void eat(Food food) {
if (isOverload(food)) {
fat += energyOverloadedToFat(food);
energy = MAX_ENERGY;
} else {
energy = Math.min(MAX_ENERGY, energy + food.getEnergy());
}
}
public void exercise(Modality modality) {
int energySpent = (int) modality.getExpendedCalories();
energy = Math.max(0, energy - energySpent);
double fatCalories = modality.getExpendedCalories() * 0.5;
double fatBurned = fatCalories / KCAL_PER_KG_FAT;
fat = Math.max(0, fat - fatBurned);
}
private double energyOverloadedToFat(Food food) {
double excessEnergy = Math.max(0, food.getEnergy() - (MAX_ENERGY - energy));
return excessEnergy / KCAL_PER_KG_FAT;
}
private boolean isOverload(Food food) {
return energy + food.getEnergy() > MAX_ENERGY;
}
}Encapsulamento na Prática
A energia de uma pessoa é um estado do corpo, enquanto comer é um comportamento. O encapsulamento garante que a energia não possa ser diretamente alterada. Em vez disso, apenas os métodos apropriados podem modificar esse estado de maneira controlada.
Além disso, essa ação pode desencadear outros efeitos. Se a pessoa consumir mais energia do que gasta, o excesso será armazenado como gordura. No entanto, o atributo percentual de gordura não pode ser modificado diretamente. Ao comer, percebemos que a energia excedente aumenta o percentual de gordura. E para reduzi-lo? É necessário adotar outro comportamento, como correr, por exemplo.
Ou seja, o corpo possui métodos que representam ações e atributos que representam estados. A modificação dos estados ocorre internamente, como consequência das ações, e não por manipulação direta dos atributos.
A exposição deliberada dos atributos para modificação direta compromete o encapsulamento, tornando o sistema mais frágil e propenso a inconsistências.
A Importância do Encapsulamento no Design de Código
Quando ignoramos o poder do encapsulamento, dois problemas comuns surgem:
1️⃣ Primitive Obsession (Obsessão por Tipos Primitivos): Ocorre quando negligenciamos a essência de um atributo e, em vez de encapsulá-lo em um objeto mais representativo, utilizamos diretamente tipos primitivos. Isso pode resultar em um código menos expressivo e mais difícil de manter. Esse é um tema relevante que explorarei em um artigo futuro.
2️⃣ Entidades Anêmicas: Ocorre quando classes atuam apenas como recipientes de dados, contendo apenas getters e setters, sem comportamento real. No Domain-Driven Design (DDD), esse tipo de modelagem enfraquece o domínio, pois desloca a lógica de negócio para serviços externos, tornando o código menos expressivo e mais difícil de evoluir.
Um bom design de código aproveita ao máximo os recursos da linguagem. Na programação orientada a objetos, isso significa aplicar o encapsulamento corretamente, limitando o acesso direto aos atributos e criando entidades ricas em comportamento.
O DDD vai além, pois incentiva a criação de modelos que refletem fielmente o domínio do mundo real. Ao utilizar uma linguagem ubíqua, alinhada com especialistas do negócio, garantimos que o código represente com precisão os conceitos e regras essenciais do sistema. Assim, as entidades deixam de ser apenas estruturas de dados e passam a capturar o significado e a intenção do domínio.
Modelar classes de forma adequada não só assegura a integridade das regras de negócio, mas também torna o sistema mais expressivo, coeso e sustentável a longo prazo.
Conclusão
O encapsulamento vai muito além de simplesmente tornar atributos privados. Seu verdadeiro poder está na criação de comportamentos que garantem a consistência do domínio.
Ao aplicar encapsulamento corretamente e modelar entidades ricas, garantimos que nosso código não apenas funcione, mas represente fielmente as regras e conceitos do mundo real. Isso fortalece o design do sistema, melhora sua manutenção e reduz erros decorrentes de modificações inconsistentes no estado dos objetos.
Se gostou desse conteúdo, fique atento ao próximo artigo, onde explorarei mais sobre
🔗 Quer mais conteúdos sobre liderança, design de código, POO e DDD?
Me acompanhe também nas redes sociais para mais discussões sobre programação! 🚀