Capa do artigo: Encapsulamento em POO: Aprenda a Aplicar do Jeito Certo

Encapsulamento em POO: Aprenda a Aplicar do Jeito Certo

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! 🚀

👉 linkedin
👉 youtube