Encapsulamento: O primeiro pilar da Programação Orientada a Objetos
Fala Dev, tudo em riba?
No artigo anterior nós entendemos o que é a Programação Orientada a Objetos (POO), como modelamos um objeto, compreendemos o que é um construtor, atributos, métodos e como instanciamos um objeto, resumindo, aprendemos o básico do básico. Caso não tenha lido o artigo anterior, recomendo fortemente a leitura.
Agora iremos abordar os pilares da POO. Esses pilares são o que fazem a POO brilhar e ser um paradigma da programação tão dinâmico. O primeiro pilar que iremos abordar é o Encapsulamento. Então bora para o conteúdo do artigo!
Modificadores
Antes de aprofundarmos no assunto do artigo, precisamos entender, primeiramente o que são os modificadores na POO.
Os modificadores determinam o nível de visibilidade de atributos e métodos de uma classe. São eles:
- Public: Como o nome mesmo sugere, atributos ou métodos definidos com esse modificador tem acesso público, ou seja, ao criar o objeto já teremos acesso direto a eles;
- Protected: Esse modificador impede que o atributo ou método seja acessado externamente, ou seja, quando criamos um objeto não teremos acesso direto ao atributo ou método que tenha esse modificador. Apenas a própria classe ou classes que estendam a essa classe terão acesso a atributos e métodos com esse modificador (ficou um pouco complicado né? a medida que avançarmos nesse e nos próximos artigos da série, entenderemos melhor esse carinha aqui);
- Private: Atributos ou métodos com esse modificador são de uso restrito da classe e não são acessíveis externamente (quando criamos um objeto) e também não são acessíveis por classes que estendam a classe que possui atributos e métodos com esse modificador;
Antes de vermos exemplos de como os modificadores atuam, vamos entender o que é encapsulamento e conectar os conceitos.
O que é Encapsulamento?
Encapsulamento é um princípio que tem dois aspectos complementares: agrupar dados e comportamentos relacionados em uma única unidade (a classe), e esconder o estado interno dessa unidade, controlando como ele é acessado e modificado. Isso permite controlar e garantir que um objeto nunca entre em um estado inválido.
Pensa comigo: em um sistema de vendas, um pedido nunca deveria ter um status abacaxi (viajei no exemplo aqui). O status deve transitar entre pendente, confirmado, faturado, finalizado, cancelado….e por ai vai. Essa transição de status faz parte da regra de negócio do objeto e o encapsulamento é o mecanismo que garante que essas regras sejam respeitadas sempre, independentemente de quem estiver usando a classe.
Vamos a um exemplo:
class User {
public name: string;
private passwordHash: string; // ninguém de fora deve tocar nisso
constructor(name: string, passwordHash: string) {
this.name = name;
this.passwordHash = passwordHash;
}
// O objeto expõe um método seguro, sem vazar o hash
public checkPassword(input: string): boolean {
// Em produção: compare hashes (ex: bcrypt.compare(input, this.passwordHash))
// Aqui está simplificado para focar no conceito de encapsulamento
return this.passwordHash === input;
}
}
const user = new User("Carlos", "abc123hashed");
console.log(user.name); // ✅ funciona
console.log(user.passwordHash); // ❌ erro de compilação — é privado
user.checkPassword("abc123hashed"); // ✅ forma correta de interagir
Entendendo o código acima:
public name: string;: Define o atributonamecom tipostringe de acesso público;private passwordHash: string;: Define o atributopasswordHashcom tipostringe de acesso privado, ou seja, somente a própria classe tem acesso a ele;public checkPassword: Método exposto de forma pública para verificar se a senha informada está correta;const user = new User("Carlos", "abc123hashed");: Cria um objeto do tipoUseronde são informados como atributos o nome do usuário e a senha;
Após a criação do objeto, na linha console.log(user.name); acessamos diretamente o valor do atributo name. Não seria a forma ideal (e veremos isso mais adiante) mas funciona pois esse atributo está público (olha o modificador public na criação dele).
Já ao executar a linha console.log(user.passwordHash); vamos receber um erro, pois esse atributo é privado (passwordHash foi criado com modificador private) então não pode ser acessado diretamente, somente a própria classe tem acesso.
Por fim, na linha user.checkPassword("abc123hashed"); estamos chamando o método, o comportamento checkPassword e informando como parâmetro a senha abc123hashed. Note que a ideia desse método é verificar se a senha fornecida é a mesma senha presente no atributo privado passwordHash. O método está exposto publicamente, porém ele faz a verificação da senha recebida com a senha que foi definida na criação do objeto e que só pode ser acessada por métodos da própria classe. Isso é visível na linha return this.passwordHash === input; onde o termo this refere-se a acessar algo da própria classe.
Entendeu como o encapsulamento funcionou no código acima? Basicamente expôs o que poderia ser exposto e controlou o acesso a senha (passwordHash) por ser uma dado sensível.
Beleza, até aqui vimos como funciona os modificadores public e private, mas e o protected? Bom esse carinha eu vou deixar para demonstrar o funcionamento dele no próximo artigo, vai fazer mais sentido e você entenderá o motivo.
Getters e Setters
Conforme comentado acima, o ideal não seria acessar o valor de um atributo diretamente e também deve ser possível em algum momento alterar o valor de um atributo definido com o modificador private. No caso do exemplo anterior, deveria ser possível alterar a senha para uma nova, concorda?
Da mesma forma o objeto User pode ter um status que defina se ele está ativo ou não no sistema. Esse status não deve ter a possibilidade de modificação externa, pois faz parte de uma regra de negócio e deve ser garantido que ele deve transitar entre valores corretos. No entanto deve ser possível, de alguma forma, consultar qual é o status atual do usuário. Como alguém de fora lê o status do objeto User?
Para conseguirmos ler ou alterar o valor de um atributo privado de fora da classe, utilizamos os Getters e os Setters.
Acessando o valor de um atributo privado utilizando Getter
Vamos ver um código exemplo de como acessar o valor de um atributo que está privado para acesso externo à classe:
enum EUserStatus {
Active = 'active',
Inactive = 'inactive'
}
class User {
public name: string;
private passwordHash: string;
private _status: EUserStatus; // convenção: _ para atributos privados com getter/setter
constructor(name: string, passwordHash: string) {
this.name = name;
this.passwordHash = passwordHash;
this._status = EUserStatus.Active;
}
get status(): EUserStatus {
return this._status
}
// O objeto expõe um método seguro, sem vazar o hash
public checkPassword(input: string): boolean {
// Em produção: compare hashes (ex: bcrypt.compare(input, this.passwordHash))
// Aqui está simplificado para focar no conceito de encapsulamento
return this.passwordHash === input;
}
}
const user = new User("Carlos", "abc123hashed");
console.log(user.status);
Note que temos um atributo status que é privado, só é acessível pela própria classe. Quando um objeto do tipo User é criado, ele recebe no construtor o status active. Foi criado o getter get status(): e é esse carinha que expõe o valor do status, como pode ser visto na linha console.log(user.status);. Massa não acha?
Alterando o valor de um atributo privado utilizando Setter
Aproveitando o exemplo do status, vamos alterar o valor dele para inactive utilizando o setter:
enum EUserStatus {
Active = 'active',
Inactive = 'inactive'
}
class User {
public name: string;
private passwordHash: string;
private _status: EUserStatus; // convenção: _ para atributos privados com getter/setter
constructor(name: string, passwordHash: string) {
this.name = name;
this.passwordHash = passwordHash;
this._status = EUserStatus.Active;
}
get status(): EUserStatus {
return this._status
}
set status(value: EUserStatus) {
this._status = value;
}
// O objeto expõe um método seguro, sem vazar o hash
public checkPassword(input: string): boolean {
// Em produção: compare hashes (ex: bcrypt.compare(input, this.passwordHash))
// Aqui está simplificado para focar no conceito de encapsulamento
return this.passwordHash === input;
}
}
const user = new User("Carlos", "abc123hashed");
console.log(user.status);
user.status = EUserStatus.Inactive;
console.log(user.status);
user.status = 'pendente' as any; // ❌ erro de compilação — não é um EUserStatus válido
Note que agora adicionamos um setter para status. Isso pode ser visto na linha set status(value: EUserStatus). Perceba que o tipo do parâmetro é o próprio enum EUserStatus — isso é importante: ao invés de validar os valores permitidos manualmente com condicionais, deixamos o TypeScript fazer esse trabalho em tempo de compilação. Qualquer tentativa de atribuir um valor fora do enum, como 'pendente' ou 'abacaxi', gera um erro antes mesmo do código rodar.
Nas quatro últimas linhas do código, depois do objeto criado, é feito:
- Verificação do status atual: retorna active;
- Alteração do status para inactive usando o enum
EUserStatus.Inactive; - Verificação do status atual: retorna inactive;
- Tentativa de atribuir um valor inválido (
'pendente'): erro de compilação, pois não pertence ao enum;
Conseguiu perceber a importância de proteger o valor do atributo status e a utilização do getter e do setter? Garantimos que sempre o valor do status será um valor correto.
Encapsulamento no contexto do backend
Em projetos backend, o encapsulamento aparece principalmente em dois lugares:
- Entidades de domínio: Classes que representam as regras de negócio (como Order, User, Product). O estado interno é protegido e só muda através de métodos que respeitam as regras;
- Serviços e repositórios: Onde a lógica de acesso a dados fica encapsulada, sem vazar detalhes de implementação para quem consome;
Conclusão
Entender o conceito de Encapsulamento é muito importante, pois sua correta aplicação trará benefícios como:
- Proteção dos dados: Sem encapsulamento, qualquer parte do código pode modificar o estado interno de um objeto de forma inesperada;
- Manutenção e evolução do código: Quando você esconde a implementação interna, pode mudar como algo funciona por dentro sem interferir na utilização da classe, em códigos que criam objetos a partir desta classe onde a manutenção foi executada;
- Controle de regras de negócio centralizadas: A lógica de validação e demais regras ficam dentro da classe, não espalhada pelo sistema;
- Redução do acoplamento: Classes que expõem tudo criam dependências frágeis. Com encapsulamento, o código externo depende apenas da interface pública, não dos detalhes internos.
Espero que esse artigo tenha contribuído para seu aprendizado ou entendimento sobre o conceito de Encapsulamento, um dos 4 pilares da Programação Orientada a Objetos. No próximo artigo veremos o segundo pilar, a Herança.
Forte abraço e até mais!