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:

  1. 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;
   // ...
}