7. ENCAPSULAMENTO & VISIBILIDADE

“Encapsulamento - a integridade pessoal dos objetos não deve ser violada. Isto é verdadeiro seja o objeto um construto de software ou uma pessoa. O limite do público/privado é tido como impermeável.”

David West, Object Thinking.

Todos nós lidamos com diversos objetos, objetos reais, no nosso dia a dia. Dos simples aos complexos, como tesouras, cafeteiras, impressoras, veículos, televisores, etc. Os objetos têm uma utilidade, nós os manuseamos ou operamos com um objetivo em mente. A maioria de nós não sabe criar um objeto assim (eu sou incapaz de criar uma tesoura), mas sabemos usá-los através do seu design e comandos, através de sua interface (<-- guarde esta palavra). Por exemplo, sabemos que o pedal ou manete de freio faz o veículo diminuir a velocidade até parar, mas não conhecemos todo o processo que leva até a roda. O mecanismo interno dos objetos é escondido (<-- guarde esta palavra também) de nós. Deve ser mesmo, pois os humanos (com raras exceções) não se importam como a coisa funciona desde que funcione. O encapsulamento na Programação Orientada a Objetos é exatamente sobre isto. Sob a metáfora de adicionar um invólucro opaco, encapsular o objeto é esconder seu mecanismo interno e fornecer uma interface com o mínimo de detalhes (abstração), apenas o suficiente para operá-lo.

7.1 Conceito de encapsulamento

Durante o projeto e implementação do objeto são definidos atributos, construtores, e métodos, enfim, todos os recursos necessários para definir o algoritmo e cumprir a funcionalidade. Pense nestes atributos e construtos lógicos como se fossem fios e engrenagens de uma máquina e que não devem estar expostos. Por isso, eles são ocultos e invisíveis do lado de fora do objeto.

O encapsulamento é a ocultação da representação interna dos objetos. Em termos práticos, ele está além do estado constante imutável, pois não é o caso onde os atributos não podem ser modificados mas que esses atributos não sejam nem mesmo visíveis ou acessáveis de fora da classe/objeto.

Toda a representação e operações escondidas, o que está encapsulado, é conhecido como a implementação do objeto. A representação e operações visíveis é a abstração do objeto. Portanto, abstração e encapsulamento são conceitos complementares. A abstração trata do comportamento observável de um objeto, enquanto o encapsulamento trata da implementação que origina este comportamento.

7.2 Ocultação de informações

O encapsulamento e a ocultação de informações são tratados como o mesmo conceito. No entanto, encapsulamento é visto mais como uma decisão pontual para proteger módulos, uma implementação da ocultação de informações, que seria um conceito mais amplo e abstrato, não relacionado apenas à POO.

A ocultação de informações é anterior a POO, ela já era discutida na década de 70, para tratar questões de modularidade, isto é, aproveitar um código existente empacotado como um módulo ou biblioteca de procedimentos (não necessariamente na forma de classes e objetos) sem expor as configurações destes mesmos módulos

Portanto, mesmo que se encontre na literatura e manuais a expressão ocultação de informações (ou information hinding, em Inglês), este livro seguirá com a expressão e técnica adotada na POO para cumprir esse mecanismo de proteção, o encapsulamento, que é definido, tipicamente, pelo controle do alcance da visibilidade das informações.

7.3 Visibilidade

Nas linguagens de programação modernas é possível “ajustar” o encapsulamento. Em outras palavras, é possível definir quais informações serão visíveis (e acessíveis) apenas internamente de um objeto, para uma “família” de objetos ou objetos em um “contexto” (um módulo, componente, biblioteca).

Portanto, não é apenas uma questão booleana do que se não é visível então é invisível, mas de graus de visibilidade, que estendem ou restringem o acesso, desde apenas a classe onde está declarada a informação, até o pacote ou namespace, módulo, etc.

A visibilidade define o nível de acesso, como já foi dito, de um círculo mais interno ao mais externo. Este nível pode ser controlado por palavras-chave nas linguagens conhecidas como modificadores de acesso.

7.4 Modificadores de acesso

Os modificadores de acesso são palavras-chave disponíveis nas linguagens para restringir ou ampliar o acesso à atributos, construtores e métodos e até as próprias classes elas mesmas.

No nível mais básico está a separação do que é privado e público, declarados como private ou public. Por exemplo, dois atributos, um restrito e outro não, seriam declarados assim nas linguagens Java e C#: private int valorInterno; e public int valor;. O mesmo pode ser aplicado aos métodos, por exemplo, enquanto private int calculaTaxas() { // ... } é um método privado e acessível apenas pela própria classe, o método public int calculaImposto() { // .. } é acessível em qualquer parte do código. A aplicação prática destas configurações será trabalhada nos tópicos seguintes.

Por fim, além do par private/public, existem outros modificadores de acesso tais como protected, package e internal ou combinações destes. No entanto, não há como fazer uma cobertura extensiva de todos estes já que este livro trata dos conceitos e princípios da OO em vez de focar-se nos recursos das linguagens.

7.5 Atributos vs Propriedades

Nem sempre há uma clara distinção entre atributos e propriedades dos objetos. Para complicar, a diferença pode depender da implementação e das linguagens de programação. Se não é o bastante, autores e profissionais da área discordam se há realmente diferença. No fim, é uma questão de terminologia, mas neste livro estes termos serão tomados como distintos.

A questão atributo/propriedade foi levantada apenas neste capítulo porque a diferença depende da visibilidade. Tanto os atributos como as propriedades podem ser públicos ou privados. No entanto, os atributos são mais comumente destinados ao armazenamento da representação do estado interno dos objetos, portanto, geralmente privados ou pelo menos constantes (ou ambos) com o objetivo de proteger as “peças” internas. As propriedades, por outro lado, são mais comumente destinadas à representação externa, lendo atributos para formar esta representação e validando informações antes que estas cheguem aos atributos internos. As propriedades tipicamente fazem parte da interface do objeto, embora possam existir propriedades privadas também.

Algumas linguagens (como JavaScript, C# e Ruby) possuem suporte a uniformidade de atributos e propriedades. Isto é, não é possível distinguir se uma informação é obtida de um atributo ou propriedade porque eles têm a mesma aparência. Considere o seguinte exemplo em JavaScript:

const ANO_ATUAL = 2021;
const MES_ATUAL = 3;

class Pet {
    constructor(nome, anoNascimento, mesNascimento) {
        this.nome = nome;
        this.anoNascimento = anoNascimento;
        this.mesNascimento = mesNascimento;
    }
    get idade() {
        if (this.anoNascimento == ANO_ATUAL && this.mesNascimento == MES_ATUAL) return "recém nascido";
        const totalMeses = MES_ATUAL - this.mesNascimento + (ANO_ATUAL - this.anoNascimento) * 12;
        const anos = parseInt(totalMeses / 12);
        const meses = totalMeses % 12;
        let idade = anos > 0 ? (anos == 1 ? "um ano" : anos + " anos") : "";
        if (anos > 0 && meses > 0) idade += " e ";
        idade += meses > 0 ? (meses == 1 ? "um mês" : (meses == 6 ? "meio" : meses + " meses")) : "";
        return idade;
    }
}

const fito = new Pet('Fito', 2018, 9)
console.log(fito.nome, fito.idade) // Fito 2 anos e meio
const mimi = new Pet('Mimi', 2015, 2)
console.log(mimi.nome, mimi.idade) // Mimi 6 anos e um mês

No exemplo anterior, nome, anoNascimento e mesNascimento são atributos, enquanto get idade é uma propriedade. Do ponto de visa da execução não há diferença entre acessar pet.nome e pet.idade. No entanto, enquanto nome representa um estado armazenado, a idade é uma representação calculada a partir da representação interna (ano e mês de nascimento). Esta uniformidade do acesso é discutida no tópico Princípio do Acesso Uniforme ainda neste capítulo.

A questão central é que ambos, atributos e propriedades, representam o estado. A diferença é que atributos oferecem acesso direto ao estado, enquanto propriedades oferecem acesso indireto.

A linguagem Java não disponibiliza uma interface uniforme para atributos e propriedades. Em vez, Java utiliza um padrão chamado de Java Beans, que usa métodos com o prefixo get para ler as propriedades e set para atribuí-las (também conhecidos como getters e setters). Por exemplo, a instrução String nome; declara um atributo nome enquanto String getNome() { // ... } declara uma propriedade Nome. Considere o seguinte código em Java:

// Dimensoes.java
class Dimensoes {

  int largura;
  int altura;
  int profundidade;
  String unidade;

  Dimensoes(int largura, int altura, int profundidade, String unidade) {
    this.largura = largura;
    this.altura = altura;
    this.profundidade = profundidade;
    this.unidade = unidade;
  }

  String getVolume() {
    return (largura * altura * profundidade) + unidade + "³";
  }
}
// App.java
class App {
  public static void main(String[] args) {
    Dimensoes pacote = new Dimensoes(30, 40, 50, "cm");
    // lendo atributos
    System.out.println(pacote.largura); // 30
    System.out.println(pacote.altura); // 40
    System.out.println(pacote.profundidade); // 50
    System.out.println(pacote.unidade); // cm
    // lendo propriedade
    System.out.println(pacote.getVolume()); // 60000cm³
  }
}

No exemplo anterior, foram lidos os atributos largura, altura, profundidade e unidade. Nenhum deles foi declarado como privado ou público, ou seja, todos são default, que significa visível para as outras classes no mesmo pacote. O volume, no entanto, é um estado calculado a partir das dimensões, portanto ele foi declarado como uma propriedade usando o padrão JavaBean String getVolume().

7.6 Acessores (getters) & Mutadores (setters)

Como visto em Validade do Estado, é necessário proteger os objetos de estados considerados inválidos. Por este motivo é comum adicionar regras de validação na inicialização (nos construtores). Assim como é comum tornar o estado imutável, para garantir que ele se manterá válido depois de validado. No entanto, como fica a situação para objetos mutáveis? Retomemos o exemplo do tópico anterior:

// Dimensoes.java
class Dimensoes {

  int largura;
  int altura;
  int profundidade;
  String unidade;

  Dimensoes(int largura, int altura, int profundidade, String unidade) {
    if (this.largura < 1) {
      throw new IllegalArgumentException("largura precisa ser positiva");
    }
    if (this.altura < 1) {
      throw new IllegalArgumentException("altura precisa ser positiva");
    }
    if (this.profundidade < 1) {
      throw new IllegalArgumentException("profundidade precisa ser positiva");
    }
    if (this.unidade == null || this.unidade.isEmpty()) {
      throw new IllegalArgumentException("unidade precisa ser informada");
    }
    this.largura = largura;
    this.altura = altura;
    this.profundidade = profundidade;
    this.unidade = unidade;
  }

  String getVolume() {
    return (largura * altura * profundidade) + unidade + "³";
  }
}
// App.java
class App {
  public static void main(String[] args) {
    // dimensões inválidas
    try {
      new Dimensoes(30, 0, 50, "cm");
    } catch (Exception e) {
      System.err.println(e); // altura precisa ser positiva
    }
    try {
      new Dimensoes(-30, 40, 50, "cm");
    } catch (Exception e) {
      System.err.println(e); // largura precisa ser positiva
    }
  // dimensões válidas
    Dimensoes pacote = new Dimensoes(30, 40, 50, "cm");
    // lendo atributos
    System.out.println(pacote.largura); // 30
    System.out.println(pacote.altura); // 40
    System.out.println(pacote.profundidade); // 50
    System.out.println(pacote.unidade); // cm
    // lendo propriedade
    System.out.println(pacote.getVolume()); // 60000cm³

    // aqui tudo dá errado:
    pacote.largura = -30;
    pacote.unidade = "";
    System.out.println(pacote.getVolume()); // -60000³
  }
}

Considerando o código do exemplo anterior, parece que o objeto está protegido de um estado inválido com as salvaguardas (os if’s) no início do construtor. No entanto, os atributos podem ser redefinidos após a instanciação, como pode-se notas nas últimas linhas pacote.largura = -30. Solução para isto?

Tornar o objeto imutável com estado constante marcando os atributos como final:

// Dimensoes.java
class Dimensoes {
  // tornando o objeto imutável
  final int largura;
  final int altura;
  final int profundidade;
  final String unidade;
  // ...

A outra opção é encapsular estes atributos e fornecer métodos acessores e mutadores que os expõem como propriedades. A maioria das linguagens modernas possuem a ideia de acessores e mutadores, geralmente na forma de getters e setters, respectivamente. Eles fornecem uma interface de acesso ao estado interno do objeto. Os setters podem ser usados para garantir pré-condições, como validar o novo estado sugerido. Os getters podem realizar um processamento dos valores antes de retorná-los. Obviamente, esta segurança só funciona se os atributos forem protegidos do acesso direto, se eles forem encapsulados, na prática, marcados como private, enquanto os acessores e mutadores representam o estado público (public).

A seguir o mesmo exemplo usando acessores e mutadores e com os modificadores de acesso declarados:

// Dimensoes.java
public class Dimensoes { // a classe é pública/aberta
  // os atributos são invisíveis fora da classe Dimensoes
  private int largura;
  private int altura;
  private int profundidade;
  private String unidade;
  // o construro é público
  public Dimensoes(int largura, int altura, int profundidade, String unidade) {
    this.setLargura(largura); // delega para o mutador
    this.setAltura(altura); // delega para o mutador
    this.setProfundidade(profundidade); // delega para o mutador
    this.setUnidade(unidade); // delega para o mutador
  }
  // métodos mutadores // setters // gravam propriedades
  public void setLargura(int largura) {
    if (this.largura < 1) {
      throw new IllegalArgumentException("largura precisa ser positiva");
    }
    this.largura = largura;
  }
  public void setAltura(int altura) {
    if (this.altura < 1) {
      throw new IllegalArgumentException("altura precisa ser positiva");
    }
    this.altura = altura;
  }
  public void setProfundidade(int profundidade) {
    if (this.profundidade < 1) {
      throw new IllegalArgumentException("profundidade precisa ser positiva");
    }
    this.profundidade = profundidade;
  }
  public void setUnidade(String unidade) {
    if (this.unidade == null || this.unidade.isEmpty()) {
      throw new IllegalArgumentException("unidade precisa ser informada");
    }
    this.unidade = unidade;
  }
  // =================================================
  // métodos acessores // getters // leem propriedades
  public int getLargura() {
    return this.largura;
  }
  public int getAltura() {
    return this.altura;
  }
  public int getProfundidade() {
    return this.profundidade;
  }
  public String getUnidade() {
    return this.unidade;
  }
  public String getVolume() {
    return (largura * altura * profundidade) + unidade + "³";
  }
}
// App.java
class App {
  public static void main(String[] args) {

    Dimensoes pacote = new Dimensoes(30, 40, 50, "cm");

    // Os atributos não podem ser acessados, as seguintes linhas comentadas são /// inválidas e não co\
mpilam:
    // System.out.println(pacote.largura);
    // System.out.println(pacote.altura);
    // System.out.println(pacote.profundidade);
    // System.out.println(pacote.unidade);

    // lendo apenas as propriedades
    System.out.println(pacote.getLargura()); // 30
    System.out.println(pacote.getAltura()); // 40
    System.out.println(pacote.getProfundidade()); // 50
    System.out.println(pacote.getUnidade()); // cm
    System.out.println(pacote.getVolume()); // 60000cm³

    // Isto não é possível:
    // pacote.largura = -30; // o atributo é PRIVADO
    // Isto não é aprovado pela validação:
    // pacote.setLargura(-30);

    // No entanto, é possível alterar as informações se elas forem válidas:
    pacote.setLargura(60);
    pacote.setAltura(20);
    pacote.setProfundidade(80);
    pacote.setUnidade("\""); // polegadas
    System.out.println(pacote.getVolume()); // -96000"³
  }
}

No exemplo anterior, os atributos foram encapsulados, tornando-se invisíveis fora da classe Dimensoes. No entanto, não são inacessíveis, desde que sejam usadas as propriedades expostas pelos métodos getters equivalentes, como getAltura().

7.7 Princípio do mínimo privilégio

Independente do paradigma de programação, programar defensivamente é sempre uma boa prática. Os melhores sistemas, os mais seguros, foram projetados e desenvolvidos por analistas e desenvolvedores prudentes e disciplinados que cultivam o hábito da programação defensiva.

A programação defensiva é apoiada por algumas técnicas e práticas, como o fail-fast discutido no Capítulo 04. O encapsulamento é um conceito, que por si só já apoia a programação defensiva. O modificador de acesso é a funcionalidade disponibilizada pelas linguagens para efetivar o encapsulamento. Resta, então, uma prática, ou regra geral, ou melhor, um princípio para saber quando encapsular. É neste ponto que entra o Princípio do Mínimo Privilégio.

A ideia geral é de que em qualquer ambiente computacional qualquer agente tenha acesso a estritamente o mínimo número de recursos que são indispensáveis para cumprir o trabalho. Este princípio se aplica à várias áreas onde segurança é primordial. Especificamente no projeto de classes ele guia a ideia de encapsulamento, partindo do pressuposto de que tudo deve ser encapsulado, ou invisível, até que se tenha um motivo plausível para liberar.

Considere um sistema para vender ingressos para um sessão no cinema. O projeto inicial poderia ser como o a seguir:

// Sessao.java
class Sessao {

  String filme;
  boolean[] ocupados;
  int vendidos;

  Sessao(String filme, int assentos) {
    this.filme = filme;
    this.ocupados = new boolean[assentos];
  }

  String getFilme() {
    return this.filme;
  }

  int getVagas() {
    this.calculaVendidos();
    return ocupados.length - vendidos;
  }

  void calculaVendidos() {
    this.vendidos = 0;
    for (boolean vendido : this.ocupados) {
      if (vendido) this.vendidos++;
    }
  }

  boolean isAssentoVago(int numero) {
    return ! this.ocupados[numero - 1];
  }

  boolean reservar(int numero) {
    if (this.isAssentoVago(numero)) {
      this.ocupados[numero - 1] = true;
      return true;
    }
    return false;
  }
}
// App.java
class App {
  public static void main(String[] args) {
    Sessao jobs = new Sessao("Jobs", 25);
    System.out.println(jobs.getFilme().equals("Jobs"));
    System.out.println(jobs.getVagas() == 25);
    System.out.println(jobs.isAssentoVago(9) == true);
    jobs.reservar(9);
    System.out.println(jobs.isAssentoVago(9) == false);
    System.out.println(jobs.getVagas() == 24);
    // há razões para acessar o array de ocupados?
    // System.out.println(jobs.ocupados);
    // ou o nro de vendidos:
    // System.out.println(jobs.vendidos);
  }
}

No exemplo anterior, a classe Sessao foi codificada como vinha sendo feito neste livro. Foram adicionados acessores, mas não foi planejado, de fato, o encapsulamento. Do ponto de vista do Princípio do Mínimo Privilégio, a mesma classe deveria ser projetada com todos os membros privados e constantes, e serem publicizados ou tornados variáveis apenas se for estritamente necessário. Portanto, seguindo princípio a risca, a classe seria projetada assim:

// Sessao.java
private class Sessao {

  private final String filme;
  private final boolean[] ocupados;
  private final int vendidos;

  private Sessao(String filme, int assentos) {
    this.filme = filme;
    this.ocupados = new boolean[assentos];
  }

  private String getFilme() {
    return this.filme;
  }

  private int getVagas() {
    this.calculaVendidos();
    return ocupados.length - vendidos;
  }

  private void calculaVendidos() {
    this.vendidos = 0;
    for (boolean vendido : this.ocupados) {
      if (vendido) this.vendidos++;
    }
  }

  private boolean isAssentoVago(int numero) {
    return ! this.ocupados[numero - 1];
  }

  private boolean reservar(int numero) {
    if (this.isAssentoVago(numero)) {
      this.ocupados[numero - 1] = true;
      return true;
    }
    return false;
  }
}

Evidentemente, uma boa parte do que foi “privatizado” deve ser liberado. Sendo a própria classe e seu construtor privados ela mesma não será acessível ou instanciável em qualquer parte do programa. Portanto, haveriam as alterações public class Sessao e public Sessao(String filme, int assentos). Quanto aos atributos filme, ocupados e vendidos, os dois primeiros podem ser constantes, já que o nome do filme não será alterado, tampouco a quantidade de assentos. A exceção é o atributo vendidos, que precisa ser calculado pelo método calculaVendidos, portanto este perde o final. O modificador private segue para todos os atributos. A última parte é a revisão dos métodos. É necessário saber o título do filme, logo private String getFilme() torna-se public String getFilme(). O mesmo acontece para o número de vagas, logo ele é publicizado também public int getVagas(). A consulta de assento vago e a operação reservar também devem ser publicizados. A exceção é o método private void calculaVendidos() que existe apenas como utilitário para o cálculo de vagas e não há nenhum motivo para ele ser invocado de fora da classe Sessao, isto é, ele é um método interno e continua private. Ao fim desta análise a classe ficaria, então, como a seguir:

// Sessao.java
public class Sessao {
  // os atributos sempre private e criteriosamente os que não serão final
  private final String filme;
  private final boolean[] ocupados;
  private int vendidos;
  // é preciso pelo menos um construtor público
  public Sessao(String filme, int assentos) {
    this.filme = filme;
    this.ocupados = new boolean[assentos];
  }
  // método consulta, ok!
  public String getFilme() {
    return this.filme;
  }
  // método consulta com estado calculado, ok!
  public int getVagas() {
    this.calculaVendidos();
    return this.ocupados.length - vendidos;
  }
  // operação interna e auxiliar, deve ser invisível/inacessível fora do objeto
  private void calculaVendidos() {
    this.vendidos = 0;
    for (boolean vendido : this.ocupados) {
      if (vendido) this.vendidos++;
    }
  }
  // necessário para o preenchimento de uma "tela" de assentos vagos
  public boolean isAssentoVago(int numero) {
    return ! this.ocupados[numero - 1];
  }
  // operação de compra ou reserva do assento precisa ser acessível
  public boolean reservar(int numero) {
    if (this.isAssentoVago(numero)) {
      this.ocupados[numero - 1] = true;
      return true;
    }
    return false;
  }
}

Resumindo, o princípio sugere que tudo deve ser privado (e constante) e que se vá liberando o que for realmente indispensável para operar o objeto. Ademais, existe uma importante lição que pode ser retirada deste exemplo: tudo o que é privado é considerado IMPLEMENTAÇÃO e tudo o que público é considerado INTERFACE do objeto.

7.8 Abstração e a interface dos objetos

A abstração de um objeto é composta por todos seus membros públicos e seu contrato de funcionamento, isto é, o comportamento esperado. A abstração é uma simplificação do objeto. Junto com o encapsulamento, que impõe uma limitação do que é público, chega-se a um conjunto mínimo de exposição que, ao final, forma a interface do objeto.

Portanto, abstração, encapsulamento se complementam para chegar na interface. O conjunto de construtores, propriedades e operações públicas foram a interface do objeto. É através dela que o objeto é usado, que é operado. Os objetos comunicam-se uns com os outros através de suas interfaces, apenas através do que é visível, o que está oculto é implementação.

Retomando o último exemplo de código no tópico anterior, para todos os efeitos, a classe e objetos desta são vistos assim do lado de fora:

// INTERFACE DE SESSAO == tudo que é público
public class Sessao {
  public Sessao(String filme, int assentos)
  public String getFilme()
  public int getVagas()
  public boolean isAssentoVago(int numero)
  public boolean reservar(int numero)
}

Tudo que não é interface, que não é visível, que é encapsulado, é implementação. Por que é importante separar a implementação da interface? Primeiro, a interface (o que é público) é abstração do objeto, já que representa o mínimo de funcionalidades para operá-lo. Segundo, toda a implementação pode ser alterada sem precisar alterar as classes que usam o objeto. Por exemplo, não é preciso alterar nada de App.java para fazer as seguintes alterações na implementação de Sessao:

// Sessao.java
import java.util.HashSet;
public class Sessao {

  private final String filme;
  private final HashSet ocupados = new HashSet(); // em vez de: boolean[] ocupados;
  private final int assentos; // em vez de: int vendidos

  public Sessao(String filme, int assentos) {
    this.filme = filme;
    this.assentos = assentos;
  }

  public String getFilme() {
    return this.filme;
  }

  public int getVagas() {
    // não é necessário calcular os vendidos
    return this.assentos - this.ocupados.size();
  }

  // método calculaVendidos é desnecessário sem o array

  public boolean isAssentoVago(int numero) {
    return ! this.ocupados.contains(numero); // consulta o conjunto (Set)
  }

  public boolean reservar(int numero) {
    if (this.isAssentoVago(numero)) {
      this.ocupados.add(numero);
      return true;
    }
    return false;
  }
}
// App.java
class App {
  public static void main(String[] args) {
    // NENHUMA ALTERAÇÃO NA CLASSE APP JÁ QUE ELA TEM ACESSO APENAS AOS
    // MÉTODOS PÚBLICOS OU INTERFACE DO OBJETO E NÃO À IMPLEMENTAÇÃO
    Sessao jobs = new Sessao("Jobs", 25);
    System.out.println(jobs.getFilme().equals("Jobs"));
    System.out.println(jobs.getVagas() == 25);
    System.out.println(jobs.isAssentoVago(9) == true);
    jobs.reservar(9);
    System.out.println(jobs.isAssentoVago(9) == false);
    System.out.println(jobs.getVagas() == 24);
  }
}

No exemplo anterior foi apresentada uma implementação bem diferente da inicial, usando um HashSet (conjunto) em vez de um array de booleans para identificar os assentos vendidos. O mais importante: a interface não foi alterada, os métodos públicos seguem iguais e respeitando o contrato (que é outro modo de dizer que a abstração é mantida).

Existem outros modos, diversos, de implementar esta classe sem alterar sua interface, e esta é uma característica fundamental provida pelo encapsulamento: a possibilidade de variar a implementação, de mudar o que está oculto sem quebrar os clientes da classe que a usam através de sua parte visível (a interface).

7.9 Considerações

Não é por acaso que este capítulo seja tão longo, o encapsulamento é um dos conceitos mais importantes para a POO. O próprio conceito mais abrangente, de ocultação de informações, é essencial para o desenvolvimento de software em qualquer linguagem e paradigma. Portanto, em linhas gerais, bom software é aquele em que a visibilidade das informações foi cuidadosamente planejada.

Para fazer este controle do que é visível/acessível ou não, especificamente, é necessário observar os recursos das linguagens e plataformas onde se está desenvolvendo. O mais comum são os modificadores de acesso, que permitem definir o que, no mínimo, é público ou privado. Se não houverem recursos como este, pelo menos deve-se seguir as convenções de código que servem para indicar informações sensíveis (como o _ antes dos nomes).

Finalmente, lembrando que o encapsulamento é trabalhado junto com a abstração para, no fim, definir o que é implementação e o que é interface do objeto. Todos estes conceitos serão usados frequentemente no decorrer do livro, então assegure-se de conhecê-los bem. Releia este capítulo se achar necessário.

7.10 Exercícios

A seguir alguns exercícios para trabalhar com o encapsulamento e seus princípios.

Encapsular Fracao

A seguir estão os casos de teste para a classe Fracao. No entanto, os testes esperam que os atributos numerador e denominador sejam visíveis. O objetivo é implementar Fracao, mas substituir nos testes o acesso ao atributo pelo acesso à propriedade com getter. Por exemplo, System.out.println(f1.numerador); deve tornar-se System.out.println(f1.getNumerador()); e o acesso direto f1.numerador deve ser impossibilitado (o atributo numerador deve ser marcado como private).

// App.java
public class App {
  public static void main(String[] args) {
    Fracao f1 = new Fracao(1, 5);
    System.out.println(f1.numerador); // 1
    System.out.println(f1.numerador == 1);
    System.out.println(f1.denominador); // 5
    System.out.println(f1.denominador == 5);

    Fracao f2 = new Fracao(1, 5);
    System.out.println(f2.numerador == 1);
    System.out.println(f2.denominador == 5);

    f2.mais(2, 5); // +2/5
    System.out.println(f2.numerador == 3);
    System.out.println(f2.denominador == 5);

    f2.mais(f1.numerador, f2.denominador);
    System.out.println(f2.numerador == 4);
    System.out.println(f2.denominador == 5);

    Fracao f3 = new Fracao(3, 7);
    System.out.println(f3.numerador == 3);
    System.out.println(f3.denominador == 7);

    f3.mais(f2.numerador, f2.denominador);
    System.out.println(f3.numerador == 43);
    System.out.println(f3.denominador == 35);

    // Começando com as alternativas
    Fracao f4 = new Fracao(6); // denominador padrão 1
    System.out.println(f4.numerador == 6);
    System.out.println(f4.denominador == 1);

    Fracao f5 = new Fracao(); // numerador padrão 0
    System.out.println(f5.numerador == 0);
    System.out.println(f5.denominador == 1);

    // Fração inválida
    // denominador 0 deve lançar IllegalArgumentException
    Fracao f6 = new Fracao(2, 0); // deve quebrar aqui
    System.out.println(f6.denominador == 0); // não deve chegar aqui
    // comente as linhas após fazer quebrar

    // Operações não suportadas
    // não lidaremos com frações negativas
    // deve lançar UnsupportedOperationException
    Fracao f7 = new Fracao(2, -5);
    Fracao f8 = new Fracao(-2, 5);
    Fracao f9 = new Fracao(-2, -5);
    // comente as linhas após fazer quebrar

    // desafio: especificar e implementar as outras operações (opcional)
  }
}

Implementar Time

Considere um instante no tempo em horas, minutos e segundos, entre 00:00:00 e 23:59:59. Implementar construtores e métodos para lidar com esse tempo de maneira fail-safe (sem rejeitar as entradas, mas adaptando-as). A interface do objeto deve ser implementada na língua inglesa com construtores para h:m:s, h:m e somente h.

O projeto deve seguir o princípio do mínimo privilégio, isto é, a classe Time deve ter seus atributos encapsulados. Considere como exercício, também, adicionar um método auxiliar interno privado.

// App.java
public class App {
  public static void main(String[] args) {
    Time t1 = new Time();
    System.out.println(t1.getHours() == 0);
    System.out.println(t1.getMinutes() == 0);
    System.out.println(t1.getSeconds() == 0);

    Time t2 = new Time(1, 40, 5);
    System.out.println(t2.getHours() == 1);
    System.out.println(t2.getMinutes() == 40);
    System.out.println(t2.getSeconds() == 5);

    // somar objetos time
    t1.plus(t2);
    System.out.println(t1.getHours() == 1);
    System.out.println(t1.getMinutes() == 40);
    System.out.println(t1.getSeconds() == 5);

    t1.plus(t2);
    System.out.println(t1.getHours() == 3);
    System.out.println(t1.getMinutes() == 20);
    System.out.println(t1.getSeconds() == 10);

    t2.plusHours(1);
    System.out.println(t2.getHours() == 2);
    t2.plusHours(23);
    System.out.println(t2.getHours() == 1);
    System.out.println(t2.getMinutes() == 40);
    System.out.println(t2.getSeconds() == 5);
    t2.plusMinutes(10);
    System.out.println(t2.getMinutes() == 50);
    t2.plusSeconds(50);
    System.out.println(t2.getSeconds() == 55);
    t2.plusMinutes(15);
    System.out.println(t2.getHours() == 2);
    System.out.println(t2.getMinutes() == 5);
    System.out.println(t2.getSeconds() == 55);
    t2.plusSeconds(10);
    System.out.println(t2.getMinutes() == 6);
    System.out.println(t2.getSeconds() == 5);
  }
}

Time com apenas UM ATRIBUTO

Tu provavelmente implementaste Time com três atributos: horas, minutos e segundos (ou equivalente em inglês). Tua missão, se decidires aceitá-la, é deixar apenas o atributo dos segundos para acumular todo o tempo nele. Os testes seguem os mesmos do exercício anterior, afinal, não é a interface que está sendo mudada, é a implementação!