Encapsulamento
O conceito de encapsulamento é uma das principais características do paradigma Orientado a Objetos. Seu princípio pode ser descrito, de forma sucinta, como "esconder detalhes de implementação".
Abordagem tradicional
Vamos imaginar o seguinte cenário, em que criamos uma classe chamada Pessoa
:
export class Pessoa{
public nome: string;
public sobrenome: string;
public dataNascimento: Date;
constructor(nome: string, sobrenome: string, dataNascimento: Date){
this.nome = nome;
this.sobrenome = sobrenome;
this.dataNascimento = dataNascimento;
}
obterNomeCompleto(): string{
return `${this.nome} ${this.sobrenome}`;
}
}
Note que, na implementação acima, declaramos explicitamente que todos os atributos da classe são públicos (public
antecede o nome da variável). No entanto, essa informação não é obrigatória. Se declararmos os atributos da classe diretamente, eles serão naturalmente considerados como públicos.
Testando o funcionamento
Para validar o funcionamento da abordagem que estamos propondo, faremos o seguinte: criaremos um outro arquivo, chamado de usaPessoa
, para criar instâncias da classe Pessoa
.
Vejamos o código:
import { Pessoa } from "./pessoa";
/* Instanciando novos objetos (Pedro e João) da classe Pessoa */
let pedro = new Pessoa("Pedro","Silva", new Date("2001-01-01"));
let joao = new Pessoa("João", "Santos", new Date("2010-01-10"));
/* Acessando os métodos */
console.log(pedro.obterNomeCompleto());
console.log(joao.obterNomeCompleto());
/* Consultando os atributos diretamente */
console.log(pedro.nome);
Tudo funciona normalmente, como esperávamos. Note que os resultados são muito parecidos com o que tínhamos em Algoritmos e Estruturas de Dados II, quando fazíamos uso de structs
para a representação de dados.
Da mesma forma, podemos alterar o estado dos objetos de Pessoa
, de forma similar à alteração dos valores de uma variável de struct
:
import { Pessoa } from "./pessoa";
let pedro = new Pessoa("Pedro","Silva", new Date("2001-01-01"));
pedro.sobrenome = "Soares";
console.log(pedro.obterNomeCompleto());
Até aqui, tudo bem. Mas vamos imaginar o seguinte cenário: imagine que existe uma regra de negócio da empresa que diz que o sobrenome de uma pessoa deve ter, no mínimo, 5 caracteres. Na abordagem atual, precisaríamos fazer essa validação todas as vezes, implementando-a em todos os lugares que usam a classe Pessoa
.
O conceito de encapsulamento contribui para uma melhoria nesse cenário, já que vai trazer a seguinte premissa: se a regra está associada à Pessoa
, o tratamento do sobrenome deve ser feito por essa classe, não por outras.
Veremos que, hoje em dia, há uma prática ainda melhor para se organizar a validação dos dados de uma classe. Por ora, vamos nos concentrar na abordagem inicial.
Aplicando o encapsulamento
Aplicando o princípio do encapsulamento nesta implementação, teremos um cenário diferente: os atributos da classe não serão manipuláveis diretamente por qualquer implementação que faça uso dela. Acompanhe o passo a passo:
- Primeiro, vamos mudar o modificador de visibilidade dos atributos da classe
Pessoa
de públicos (public
) para privados (private
):
export class Pessoa{
private nome: string;
private sobrenome: string;
private dataNascimento: Date;
constructor(nome: string, sobrenome: string, dataNascimento: Date){
this.nome = nome;
this.sobrenome = sobrenome;
this.dataNascimento = dataNascimento;
}
obterNomeCompleto(): string{
return `${this.nome} ${this.sobrenome}`;
}
}
Agora, vamos observar o que acontece ao tentarmos interagir com objetos da classe Pessoa
:
import { Pessoa } from "./pessoa";
/* Instanciando novos objetos (Pedro e João) da classe Pessoa */
let pedro = new Pessoa("Pedro","Silva", new Date("2001-01-01"));
let joao = new Pessoa("João", "Santos", new Date("2010-01-10"));
/* Acessando os métodos */
console.log(pedro.obterNomeCompleto());
console.log(joao.obterNomeCompleto());
/* Consultando os atributos diretamente */
console.log(pedro.nome); // aqui dá erro, pois nome é um atributo privado
Note que, embora o nome completo seja exibido, o atributo nome
do objeto pedro
não é exibido, pois ele é um atributo privado. Da mesma forma, a tentativa de atualização do valor de um atributo, como no código a seguir, será frustrada:
pedro.sobrenome = "Soares"; //aqui também dá erro, pois sobrenome é um atributo privado
Getters e Setters
Para a manipulação dos atributos private
de uma classe, precisamos usar os métodos getters
e setters
. Enquanto o get
retorna o valor de um atributo, o método set
define o valor para um atributo.
Aqui, há uma diferença substancial quanto à implementação desses métodos, que pode variar de uma linguagem para outra.
Em Java, seria:
public class Pessoa{
private String nome;
///...
String getNome(){
return this.nome;
}
void setNome(String nome){
this.nome = nome;
}
}
Assim, quando estávamos usando uma classe com encapsulamento, fazemos a chamada do método:
Pessoa p = new Pessoa();
p.setNome("Joãozinho");
System.out.println(p.getNome());
Note que, nessa abordagem, sempre teremos o prefixo get
antes do nome do atributo que desejamos manipular.
Em TypeScript, a implementação de get
e set
é um pouco diferente:
class Pessoa{
private nome: string;
get nome(){
return this.nome;
}
set nome(nome: string){
return this.nome = nome;
}
}
O código acima resultará em erro, que será explicado a seguir.
Perceba que, no código acima, há um espaço entre get
e nome
: isso porque há o entendimento de que get
é um getter
. A mesma coisa se aplica ao setter
. Essa diferença tem um motivo bem simples: facilitar o uso.
let p = new Pessoa();
p.nome = "Joãozinho";
console.log(p.nome);
Note que, em TypeScript, usamos get
e set
de forma transparente. Parece que estamos acessando atributos públicos, mas não estamos.
Lembra que eu mencionei anteriormente que o código acima resultaria em erro? Isso porque o atributo nome
tem nome
igual aos métodos get
e set
. Para corrigir isso, precisamos apenas adaptar a implementação, colocando um underline à frente do nome dos atributos privados, como no exemplo a seguir:
export class Pessoa{
private _nome: string;
// ...
}