Entendendo Encapsulação

Você irá aprender

  • O que é Encapsulação.

  • Porque usamos encapsulação.

  • Como usar Encapsulação.

Pré-requisitos

Protegendo dados de uso indevido

Durante as duas primeiras aulas de POO, você viu muito falarmos sobre escopo de classes, sendo as com maior frequência: public, e private. Nesta aula, nós entraremos em detalhe em como usar essas palavras-chave, além de explicar o padrão do encapsulamento, muito importante quando estamos falando de Programação Orientada a Objetos.

Vou mais uma vez lembrar das nossas Structs, você devê se lembrar que lá, quando temos algum dado na mesma, podemos acessá-lo sem nenhum problema, basta criar uma instância da Struct e podemos acessar seu atributo sem problemas.

Pessoa p;
p.idade = 18;

Em muitos casos, ter esse livre acesso aos membros de uma amarração não é um problema, mas agora, pensaremos que estamos criando uma classe Pessoa para identificar a conta bancária de um cliente qualquer, e agora temos o atributo saldo.

typedef struct Pessoa {
  char nome[50];
  float saldo;
  char cpf[13];
};

 int main(void){
  Pessoa p;
  p.saldo = 50;
 }

Obviamente o saldo é algo muito importante, e por questões de segurança, precisamos proteger esse dado de manipulações indevidas, caso contrário alguém poderia facilmente aumentar o seu saldo de R$1,00 para R$1.000.000,00 apenas trocando o valor de uma variável, ou até pior, pode diminuir o seu saldo. É por conta de problemas como esse que criamos design de software, para implementar soluções que resolvam problemas diversos, isso inclui soluções para segurança e integridade dos dados.

Para o nosso caso, podemos usar um dos princípios de POO ser a Encapsulação.

O que é Encapsulação?

Em resumo, encapsulação é um padrão de software, onde colocamos os dados da classe como privado e para acessar/modificar tais dados, usamos funções de escopo público.

A ideia aqui é bem simples, como o único meio de acessar os dados da classe é a partir dessa função, nós estamos protegendo a variável de ser modificada de formas imprevisíveis. Além disso, caso a lógica de recuperar ou modificar tais dados modifique durante a criação do software (Algo que acontece bem frequentemente na vida útil de qualquer aplicação), o custo para refatorar essas modificações é mais baixo, já que tudo que temos que fazer é modificar essas funções.

Especificadores de acesso

É aqui que as palavras-chave public e private entram em ação, pois diferente de C, C++ e Java (assim como outras linguagens que possuem suporte a POO), também introduzem esse conceito de especificadores de acesso. Também temos a palavra-chave protected mas entraremos em maiores detalhes quando falarmos de Herança.

Public

Quando dizemos que a variável é pública, quer dizer que tanto dentro quanto fora da classe, podemos acessar esse atributo. Esse é o mesmo comportamento das Structs em C, onde você pode acessar qualquer coisa usando o ponto "." seguido do atributo da struct. A mesma coisa acontece em classes:

#include<iostream>
#include<string>

class Pessoa {
  public:
  
    std::string nome;
    std::string cpf;
    float saldo;
  
    Pessoa(std::string nome, std::string cpf, float saldo) {
      this.nome = nome;
      this.cpf = cpf;
      this.saldo = saldo;
    }
};

int main(void) {
  Pessoa joao = Pessoa("Joao","58423593212",4584);
  std::cout << joao.saldo << "\n";
  return 0;
}

Uma observação importante, quando você não coloca nenhum especificador em Java, ele não será "public", mas sim "default", nós não iremos entrar em detalhes disso, pois isso é coisa da linguagem, mas para mais informações, acesse esse link.

Private

Já o especificador de acesso private, significa que tal atributo não pode ser acessado fora do escopo da classe, e caso a gente tente, o compilador irá nos retornar um erro:

#include<iostream>
#include<string>

class Pessoa {
  private:
  
    std::string nome;
    std::string cpf;
    float saldo;
  public:
    Pessoa(std::string nome, std::string cpf, float saldo) {
      this->nome = nome;
      this->cpf = cpf;
      this->saldo = saldo;
    }
};

int main(void) {
  Pessoa joao = Pessoa("Joao","58423593212",4584);
  std::cout << joao.saldo << "\n";
  return 0;
}

Dê uma olhada no erro que recebemos:

main.cpp:20:21: error: 'saldo' is a private member of 'Pessoa'
  std::cout << joao.saldo << "\n";
                    ^
main.cpp:9:11: note: declared private here
    float saldo;
          ^
1 error generated.

Note que em Java o erro é um pouco diferente, mas a ideia é a mesma. "Cannot find symbol" quer dizer que, ou ele não existe, ou o escopo de acesso não permite que ele seja visível, logo ele não existe.

Agora fica a questão, se o nosso atributo é privado, como que o acessamos fora da classe? Para isso, voltaremos a falar de Encapsulação, mais especificamente, das funções tipo "Getters" e "Setters".

Getters

Para acessar esses dados, nós temos os getters, uma função que tem como único objetivo retornar o atributo.

#include<iostream>
#include<string>

class Pessoa {
  private:
    std::string nome;
    std::string cpf;
    float saldo;
    
  public:
  
    Pessoa(std::string nome, std::string cpf, float saldo) {
      this->nome = nome;
      this->cpf = cpf;
      this->saldo = saldo;
    }
    // Funcoes getters
    std::string get_nome() {
      return nome;
    }
    
    std::string get_cpf() {
      return cpf;
    }
    
    float get_saldo() {
      return saldo;
    }
};

int main(void) {
  Pessoa joao = Pessoa("Joao","58423593212",4584);
  std::cout << joao.get_saldo() << "\n";
  return 0;
}

Perceba que colocamos as funções de getters no espaço de escopo público, dessa forma, podemos acessar essas funções fora da classe. Além disso, o padrão dessas funções é bem simples. O tipo de retorna da função é o tipo da variável, ou seja, se a variável é uma string, o nosso getter precisa retornar uma string. O nome é mais uma questão de convenção, mas geralmente é a palavra "get" acompanhado do nome da variável, então nesse caso: "get_nome". Por fim, temos a função em si, que apenas para princípios práticos, estamos apenas colocando para retornar o dado.

Note que neste caso não usamos a palavra "this", pois diferente do construtor, não temos outra variável chamada "nome" ou "cpf" ou "saldo", neste caso, quando colocarmos qualquer uma dessas variáveis, a linguagem vai entender que você quer acessar o atributo da classe e não uma variável de escopo local.

Setters

Por outro lado, temos os setters, funções que existem para mudarmos o valor de uma variável.

#include<iostream>
#include<string>

class Pessoa {
  private:
    std::string nome;
    std::string cpf;
    float saldo;
    
  public:
  
    Pessoa(std::string nome, std::string cpf, float saldo) {
      this->nome = nome;
      this->cpf = cpf;
      this->saldo = saldo;
    }
    // Funcoes getters
    std::string get_nome() {
      return nome;
    }
    
    std::string get_cpf() {
      return cpf;
    }
    
    float get_saldo() {
      return saldo;
    }
    
    void set_nome(std::string nome) {
      this->nome = nome;
    }
    
    void set_cpf(std::string cpf) {
      this->cpf = cpf;
    }
    
    void set_saldo(float saldo) {
      this->saldo = saldo;
};

int main(void) {
  Pessoa joao = Pessoa("Joao","58423593212",4584);
  joao.set_saldo(5833);
  std::cout << joao.get_saldo() << "\n";
  return 0;
}

Com os setters o padrão é um pouquinho diferente das funções de "getters", primeiramente toda função é void, pois não existe nenhum dado que precisamos voltar. Segundo, colocamos a palavra "set" ao lado do nome da variável. Terceiro, funções setters recebe como argumento o novo valor da nossa variável.

Por fim, como aqui estamos trabalhando com variáveis com o mesmo nome, precisamos usar a palavra "this" para distinguir as variáveis de escopo local e variáveis da classe.

Last updated