Primeiras aulas do curso Orientação a Objetos: Melhores técnicas com Java

Orientação a Objetos: Melhores técnicas com Java

Revisitando a Orientação a Objetos - Revisitando a Orientação a Objetos

O que você entende por Orientação a Objetos?

Quantas vezes nos deparamos no dia a dia com aquele pedaço de código, aquele arquivo gigante, difícil de entender e, portanto, quase impossível de modificar?

    public class Cadastro {

      private String nome;
      private String cpf;
      private String cnpj;
      private boolean pessoaFisica;

      public boolean processa(String acao, String tipo, String nome,
                              String cpf, String cnpj) {
        if(cpf != null) {
          String erro = "";
          if (cpf.length() < 11)
            erro += "Sao necessarios 11 digitos para verificacao do CPF! \n\n";
          if(cpf.matches(".*\\D.*"))
            erro += "A verificacao de CPF suporta apenas numeros! \n\n";
          if(cpf == "00000000000" || cpf == "11111111111" ||
             cpf == "22222222222" || cpf == "33333333333" ||
             cpf == "44444444444" || cpf == "55555555555" ||
             cpf == "66666666666" || cpf == "77777777777" ||
             cpf == "88888888888" || cpf == "99999999999") {
            erro += "Numero de CPF invalido!";
          }
          int[] a = new int[11];
          int b = 0;
          int c = 11;
          for(int i = 0; i < 11; i++) {
            a[i] = cpf.charAt(i) - '0';
            if(i < 9) b += (a[i] * --c);
          }
          int x = b % 11;
          if(x < 2) {
            a[9] = 0;
          }
          else {
            a[9] = 11-x;
          }
          b = 0;
          c = 11;
          for(int y = 0; y < 10; y++) b += (a[y] * c--);
          x = b % 11;
          if(x < 2) {
            a[10] = 0;
          }
          else {
            a[10] = 11-x;
          }
          if((cpf.charAt(9) - '0' != a[9]) || (cpf.charAt(10) - '0' != a[10])) {
            erro +="Digito verificador com problema!";
          }
          if(erro.length() > 0) {
            return false;
          }
        }
        if("cadastra".equals(acao)) {
          if("pessoa_fisica".equals(tipo)) {
            if(cnpj != null) {
              return false;
            }
            else {
              this.nome = nome;
              this.cpf = cpf;
              this.pessoaFisica = true;
            }
          }
          else if ("pessoa_juridica".equals(tipo)) {
            if(cpf != null) {
              return false;
            }
            else {
              this.nome = nome;
              this.cnpj = cnpj;
              this.pessoaFisica = false;
            }
          }
        }
        else if("atualiza".equals(acao)) {
          if("pessoa_fisica".equals(tipo)) {
            if(cnpj != null) {
              return false;
            }
            else {
              this.nome = nome;
              this.cpf = cpf;
              this.pessoaFisica = true;
            }
          }
          else if ("pessoa_juridica".equals(tipo)) {
            if(cpf != null) {
              return false;
            }
            else {
              this.nome = nome;
              this.cnpj = cnpj;
              this.pessoaFisica = false;
            }
          }
        }

        return false;
      }

      public String getNome() {
        return nome;
      }

      public void setNome(String nome) {
        this.nome = nome;
      }

      public String getCpf() {
        return cpf;
      }

      public void setCpf(String cpf) {
        this.cpf = cpf;
      }

      public String getCnpj() {
        return cnpj;
      }

      public void setCnpj(String cnpj) {
        this.cnpj = cnpj;
      }

      public boolean isPessoaFisica() {
        return pessoaFisica;
      }

      public void setPessoaFisica(boolean pessoaFisica) {
        this.pessoaFisica = pessoaFisica;
      }
    }

Não precisamos ir muito longe para encontrarmos mais de um desses arquivos em um mesmo projeto. Ele é o motivo de piada nas nossas conversas durante os almoços e eventos, mas por que eles continuam aparecendo no nosso dia a dia?

Boas práticas de OO servem para diminuir os efeitos negativos de um código mal estruturado. Facilitam sua manutenção e adaptação à medida que precisamos adicionar funcionalidades ou alterar o comportamento do sistema.

Tomemos como exemplo um sistema de controle financeiro de uma empresa. Nesse sistema, teremos uma classe para representar uma dívida. Nossa classe <code>Divida</code> deve armazenar o credor (para quem a empresa está devendo), o CNPJ desse credor, o valor total da dívida e o seu valor já pago.

Como todo tutorial de Orientação a Objetos, a prática comum é criar a classe com seus getters e setters:

    public class Divida {
      private double total;
      private double valorPago;
      private String credor;
      private String cnpjCredor;

      public double getTotal() {
        return this.total;
      }
      public void setTotal(double total) {
        this.total = total;
      }
      public double getValorPago() {
        return this.valorPago;
      }
      public void setValorPago(double valorPago) {
        this.valorPago = valorPago;
      }
      public String getCredor() {
        return this.credor;
      }
      public void setCredor(String credor) {
        this.credor = credor;
      }
      public String getCnpjCredor() {
        return this.cnpjCredor;
      }
      public void setCnpjCredor(String cnpjCredor) {
        this.cnpjCredor = cnpjCredor;
      }
    }

Essa classe é utilizada pelo BalancoEmpresa para registrar e atualizar dívidas. O método que registra dívidas cria uma instância de Divida, preenche o valor, os dados do credor e guarda essa dívida num mapa em que a chave é o CNPJ do credor.

    public class BalancoEmpresa {
      private HashMap<String, Divida> dividas = new HashMap<String, Divida>();

      public void registraDivida(String credor, String cnpjCredor, double valor) {
        Divida divida = new Divida();
        divida.setTotal(valor);
        divida.setCredor(credor);
        divida.setCnpjCredor(cnpjCredor);
        dividas.put(cnpjCredor, divida);
      }
    }

Temos também um método que paga parte de uma dívida. Ele recebe o CNPJ de um credor e o valor que foi pago, busca a Divida correspondente no banco usando o CNPJ do credor e a atualiza.

    public void pagaDivida(String cnpjCredor, double valor) {
      Divida divida = dividas.get(cnpjCredor);
      if (divida != null) {
        divida.setValorPago(divida.getValorPago() + valor);
      }
    }

Imagine que a empresa tem um outro sistema só para gerenciar as dívidas. Ele possui a classe GerenciadorDeDividas, que tem um método que também permite pagar uma dívida:

    public class GerenciadorDeDividas {
      public void efetuaPagamento(Divida divida, double valor) {
        divida.setValorPago(divida.getValorPago() + valor);
      }

      // outros métodos
    }

Agora alteramos o valor pago de uma dívida em dois lugares. Será que isso é ruim? Suponha que precisamos descontar uma taxa de R$ 8 dos pagamentos de dívida maiores que R$ 100. Ou seja, se o pagamento é de R$ 200 o pagamento real é de R$ 192. Se o pagamento é de R$ 40 o pagamento real também é de R$ 40. Em quantos lugares precisaremos mexer? Dois:

    public class BalancoEmpresa {
      public void pagaDivida(String cnpjCredor, double valor) {
        Divida divida = dividas.get(cnpjCredor);
        if (divida != null) {
          if (valor > 100) {
            valor = valor - 8;
          }
          divida.setValorPago(divida.getValorPago() + valor);
        }
      }
    }
    public class GerenciadorDeDividas {
      public void efetuaPagamento(Divida divida, double valor) {
        if (valor > 100) {
          valor = valor - 8;
        }
        divida.setValorPago(divida.getValorPago() + valor);
      }
    }

No nosso caso já são dois pontos de utilização, e poderiam ser muito mais! Como ter certeza que não há outros pontos do código que também usavam a variável diretamente? Ou em projetos de terceiros que usam o nosso como biblioteca?

O problema é que temos dados (valor pago da dívida) separados de comportamento (pagamento da dívida). Vemos que a classe Divida é burra: ela não faz nada, somente armazena valores; ela é uma estrutura de dados.

Para juntarmos os dados e o comportamento, vamos criar o método paga(double valor) na classe Divida.

    public class Divida {
      public void paga(double valor) {
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
      }
      // getters e setters
    }

Podemos agora simplesmente chamar o novo método quando quisermos pagar uma parte de uma dívida. O método pagaDivida da nossa classe BalancoEmpresa fica assim:

      public void pagaDivida(String cnpjCredor, double valor) {
        Divida divida = dividas.get(cnpjCredor);
        if (divida != null) {
          divida.paga(valor);
        }
      }

Enquanto isso, na classe GerenciadorDeDividas, mudamos o código do método efetuaPagamento para:

      public void efetuaPagamento(Divida divida, double valor) {
        divida.paga(valor);
      }

Agora, se precisarmos acrescentar alguma verificação no valor a ser pago, só precisamos alterar o método paga da classe Divida. Por exemplo, podemos querer verificar que o valor a ser pago é positivo. Afinal, não faz sentido pagar um valor negativo!

    public class Divida {
      public void paga(double valor) {
        if (valor < 0) {
          throw new IllegalArgumentException("Valor invalido para pagamento");
        }
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
      }
      // getters e setters
    }

Nosso problema foi resolvido, mas ainda é possível adicionar um valor da maneira antiga (usando o getter e setter). Nada impede que alguém utilize esse código dessa forma. Pior ainda, uma vez que essas variáveis estão expostas, é mais fácil para um desenvolvedor quebrar o padrão de utilização do paga e utilizar o setter, cometendo um erro, uma vez que o getter e setter são nomes padronizados: sem pensar o desenvolvedor acaba introduzindo um novo bug no sistema.

Queremos forçar o programador a utilizar o método paga quando for pagar uma parte da dívida.

Consideremos então o método setValorPago. Ele permite que mudemos o valor já pago da dívida a qualquer momento. Não queremos isso!

Qual a única forma de mudar o valor pago de uma dívida? Acrescentando um pagamento. E só! O método setValorPago não faz sentido nesse caso! Podemos então removê-lo.

    public class Divida {
      private double total;
      private double valorPago;
      private String credor;
      private String cnpjCredor;

      public double getTotal() {
        return this.total;
      }
      public void setTotal(double total) {
        this.total = total;
      }
      public double getValorPago() {
        return this.valorPago;
      }
      public String getCredor() {
        return this.credor;
      }
      public void setCredor(String credor) {
        this.credor = credor;
      }
      public String getCnpjCredor() {
        return this.cnpjCredor;
      }
      public void setCnpjCredor(String cnpjCredor) {
        this.cnpjCredor = cnpjCredor;
      }
      public void paga(double valor) {
        if (valor < 0) {
          throw new IllegalArgumentException("Valor invalido para pagamento");
        }
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
      }
    }

Quando estamos desenvolvendo um sistema orientado a objetos é comum criarmos classes para isolar os dados em conjuntos que façam mais sentido estarem juntos, o que em muitos casos é uma boa ideia.

Um problema clássico acontece quando a lógica associada a esses dados continua espalhada pelo código, quebrando o controle que aquele objeto tinha sobre seus dados. A manutenção era difícil, custosa e frágil, uma vez que diversos pontos tinham que ser alterados a cada nova modificação. Cuidando com atenção de seus membros, o controle não é perdido e a mudança é em um ponto único.

Observe que, no exemplo utilizado, o simples fato de unir dados e comportamento facilitou a adição de novas funcionalidades e facilitou a mudança das funcionalidades existentes.

No nosso exemplo, o BalancoEmpresa sabe o que a classe Divida faz, ou seja, controla seu próprio valor pago. Mas a classe BalancoEmpresa não sabe como a Divida faz esse controle. Ou seja, o modo como a classe Divida altera seu próprio valor pago está escondido das outras classes, está encapsulado. É graças a esse encapsulamento que conseguimos alterar esse comportamento facilmente.

Em um bom design orientado a objetos, dado uma mudança, você sabe onde alterar, já que esses pontos são razoavelmente explícitos. Encapsulamento é um dos caminhos.

Vamos incrementar um pouco nosso modelo. Agora, além de guardar o valor já pago, a classe Divida vai guardar informações de cada pagamento realizado. Para isso, vamos criar uma nova classe para representar um pagamento. Essa classe vai guardar o nome e o CNPJ de quem o fez, bem como o valor pago.

    public class Pagamento {
      private String pagador;
      private String cnpjPagador;
      private double valor;

      public String getPagador() {
        return this.pagador;
      }
      public void setPagador(String pagador) {
        this.pagador = pagador;
      }
      public String getCnpjPagador() {
        return this.cnpjPagador;
      }
      public void setCnpjPagador(String cnpjPagador) {
        this.cnpjPagador = cnpjPagador;
      }
      public double getValor() {
        return this.valor;
      }
      public void setValor(double valor) {
        this.valor = valor;
      }
    }

A Divida foi incrementada e agora guarda uma lista de itens do tipo Pagamento e, como esperado, criamos também seu getter e setter:

    public class Divida {
      private ArrayList<Pagamento> pagamentos = new ArrayList<Pagamento>();
      // outros atributos

      public ArrayList<Pagamento> getPagamentos() {
        return this.pagamentos;
      }
      public void setPagamentos(ArrayList<Pagamento> pagamentos) {
        this.pagamentos = pagamentos;
      }

      // outros métodos
    }

Mas será que queremos o setter? Assim como o valor pago não pode ser mudado para um número arbitrário, não queremos que qualquer um possa simplesmente trocar a nossa lista de pagamentos a qualquer momento. É possível, inclusive, fazer o seguinte uso indesejado:

      Divida divida = new Divida();
      divida.setPagamentos(null);

Estamos novamente abrindo a implementação da nossa classe. A Divida perde o controle desse atributo. Mais uma vez permitimos que outras classes alterem nossos atributos da forma que quiserem, quebramos o encapsulamento. Não queremos o método setPagamentos! Podemos então removê-lo.

Vejamos como está a classe BalancoEmpresa.

    public class BalancoEmpresa {
      private HashMap<String, Divida> dividas = new HashMap<String, Divida>();

      public void registraDivida(String credor, String cnpjCredor, double valor) {
        Divida divida = new Divida();
        divida.setTotal(valor);
        divida.setCredor(credor);
        divida.setCnpjCredor(cnpjCredor);
        dividas.put(cnpjCredor, divida);
      }

      public void pagaDivida(String cnpjCredor, double valor, String nomePagador, String cnpjPagador) {
        Divida divida = dividas.get(cnpjCredor);
        if (divida != null) {
          Pagamento pagamento = new Pagamento();
          pagamento.setCnpjPagador(cnpjPagador);
          pagamento.setPagador(nomePagador);
          pagamento.setValor(valor);
          divida.paga(valor);
          divida.getPagamentos().add(pagamento);
        }
      }
    }

Note que o método pagaDivida é responsável por adicionar um novo pagamento e atualizar o valor pago da dívida. Para manter a consistência, a classe GerenciadorDeDividas também deve adicionar o novo pagamento na dívida, além de continuar atualizando o valor pago.

O código é exatamente o mesmo da classe BalancoEmpresa, logo podemos resolver o problema facilmente copiando e colando o código de uma classe para a outra:

    public class GerenciadorDeDividas {
      public void efetuaPagamento(Divida divida, String nomePagador, String cnpjPagador, double valor) {
        Pagamento pagamento = new Pagamento();
        pagamento.setCnpjPagador(cnpjPagador);
        pagamento.setPagador(nomePagador);
        pagamento.setValor(valor);
        divida.paga(valor);
        divida.getPagamentos().add(pagamento);
      }
    }

O que aconteceria se precisássemos adicionar um pagamento na dívida em outro ponto no nosso código? Não seria improvável termos um código assim:

      public void adicionaPagamentoNaDivida(String pagador, String cnpj, double valor) {
        Pagamento pagamento = new Pagamento();
        pagamento.setCnpjPagador(cnpj);
        pagamento.setPagador(pagador);
        pagamento.setValor(valor);
        divida.getPagamentos().add(pagamento);
      }

Qual é o problema nesse código? O código em si está correto, mas esquecemos de adicionar o valor do pagamento na dívida (método paga).

A ação de registrar um novo pagamento em uma dívida consiste em inserir o pagamento na lista e adicionar o valor do pagamento no total pago. Note que essa ação está relacionada à dívida. Por que não criar um método registra na Divida que recebe um Pagamento e realiza as atividades necessárias?

    public class Divida {
      private ArrayList<Pagamento> pagamentos = new ArrayList<Pagamento>();
      private double valorPago;
      // outros atributos

      public void paga(double valor) {
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
      }
      public void registra(Pagamento pagamento) {
        this.pagamentos.add(pagamento);
        this.paga(pagamento.getValor());
      }
      // outros métodos
    }

Agora, basta chamarmos esse novo método onde é necessário registrar um pagamento de uma dívida:

    public class BalancoEmpresa {
      public void pagaDivida(String cnpjCredor, double valor, String nomePagador, String cnpjPagador) {
        Divida divida = dividas.get(cnpjCredor);
        if (divida != null) {
          Pagamento pagamento = new Pagamento();
          pagamento.setCnpjPagador(cnpjPagador);
          pagamento.setPagador(nomePagador);
          pagamento.setValor(valor);
          divida.registra(pagamento);
        }
      }
    }

Dessa forma, a responsabilidade de registrar um pagamento de uma dívida, que antes estava espalhada em várias classes, agora está centralizada em um único ponto. Se alguma regra relacionada a essa ação mudar, sabemos exatamente onde devemos mexer.

Note que não faz mais sentido deixarmos qualquer um chamar o método paga na classe Divida. Podemos deixá-lo privado.

As mudanças que fizemos vão de acordo com um princípio muito importante de orientação a objetos. Encapsulando o comportamento da classe Divida, paramos de pedir os seus dados para fazer o que queríamos e passamos a dizer a ela o que queríamos que fosse feito. O princípio, conhecido como Tell, Don't Ask, diz exatamente isso: quando você está interagindo com outro objeto (BalancoEmpresa registrando pagamentos na Divida por exemplo), você deve dizer o que quer que esse outro objeto faça (chamada do método registra()) e não perguntar sobre o seu estado para tomar decisões (implementação anterior que usava o getPagamentos()).

Veja como partimos de uma classe Divida que não fazia nada, apenas guardava valores. Ela era o que é conhecido como modelo anêmico: uma classe de modelo sem comportamento algum. Seguindo o princípio de unir comportamento e dados, escondemos os dados e a forma como interagimos com eles, encapsulamos esse comportamento. A classe Divida deixou de ser anêmica.

Melhorando a coesão de nossas classes - Melhorando a coesão de nossas classes

A ideia de passar responsabilidades para a classe Divida deixa nosso código mais encapsulado. Agora, tudo que é relacionado aos atributos dessa classe está nela mesma. Além disso, se precisarmos mudar alguma lógica relacionada a dívidas, sabemos onde mexer.

Vamos incrementar um pouco mais nosso sistema. Colocamos uma validação e alguns métodos auxiliares no CNPJ do credor e permitimos algumas filtragens nos pagamentos.

    public class Divida {
      private double total;
      private double valorPago;
      private String credor;
      private String cnpjCredor;
      private ArrayList<Pagamento> pagamentos = new ArrayList<Pagamento>();

      public boolean cnpjValido() {
        return primeiroDigitoVerificadorDoCnpj() == primeiroDigitoCorretoParaCnpj()
            && segundoDigitoVerificadorDoCnpj() == segundoDigitoCorretoParaCnpj();
      }
      public String getCnpjCredor() {
        return this.cnpjCredor;
      }
      public String getCredor() {
        return this.credor;
      }
      public double getTotal() {
        return this.total;
      }
      public double getValorPago() {
        return this.valorPago;
      }
      private void paga(double valor) {
        if (valor < 0) {
          throw new IllegalArgumentException("Valor invalido para pagamento");
        }
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
      }
      public ArrayList<Pagamento> pagamentosAntesDe(Calendar data) {
        ArrayList<Pagamento> pagamentosFiltrados = new ArrayList<Pagamento>();
        for (Pagamento pagamento : this.pagamentos) {
          if (pagamento.getData().before(data)) {
            pagamentosFiltrados.add(pagamento);
          }
        }
        return pagamentosFiltrados;
      }
      public ArrayList<Pagamento> pagamentosComValorMaiorQue(double valorMinimo) {
        ArrayList<Pagamento> pagamentosFiltrados = new ArrayList<Pagamento>();
        for (Pagamento pagamento : this.pagamentos) {
          if (pagamento.getValor() > valorMinimo) {
            pagamentosFiltrados.add(pagamento);
          }
        }
        return pagamentosFiltrados;
      }
      public ArrayList<Pagamento> pagamentosDo(String cnpjPagador) {
        ArrayList<Pagamento> pagamentosFiltrados = new ArrayList<Pagamento>();
        for (Pagamento pagamento : this.pagamentos) {
          if (pagamento.getCnpjPagador().equals(cnpjPagador)) {
            pagamentosFiltrados.add(pagamento);
          }
        }
        return pagamentosFiltrados;
      }
      private int primeiroDigitoCorretoParaCnpj() {
        // Extrai o primeiro dígito verificador do CNPJ armazenado
        // no atributo cnpj
      }
      private int primeiroDigitoVerificadorDoCnpj() {
        // Extrai o segundo dígito verificador do CNPJ armazenado
        // no atributo cnpj
      }
      public void registra(Pagamento pagamento) {
        this.pagamentos.add(pagamento);
        paga(pagamento.getValor());
      }
      private int segundoDigitoCorretoParaCnpj() {
        // Calcula o primeiro dígito verificador correto para
        // o CNPJ armazenado no atributo cnpj
      }
      private int segundoDigitoVerificadorDoCnpj() {
        // Calcula o primeiro dígito verificador correto para
        // o CNPJ armazenado no atributo cnpj
      }
      public void setCnpjCredor(String cnpjCredor) {
        this.cnpjCredor = cnpjCredor;
      }
      public void setCredor(String credor) {
        this.credor = credor;
      }
      public void setTotal(double total) {
        this.total = total;
      }
      public double valorAPagar() {
        return this.total - this.valorPago;
      }
    }

Mas será que agora nossa classe Divida não tem responsabilidades demais? Veja quantos métodos ela possui! Vemos, inclusive, muitos métodos privados com lógicas possivelmente difíceis de usar e, consequentemente, de testar e dizer se estão corretas.

Repare também na quantidade de responsabilidades distintas da classe Divida: registrar pagamentos, aplicar descontos em pagamentos quando aplicáveis, filtrar os pagamentos já realizados, validar CNPJ, dentre outras. Quando uma classe tem muitas responsabilidades com pouca ou nenhuma relação entre si, dizemos que ela não é coesa.

Veja, por exemplo, que se quisermos utilizar somente a validação de CNPJ, precisaremos ainda assim criar uma instância de Divida para poder utilizar o código já existente. E se quisermos implementar a validação de CNPJ na classe Pagamento, também? Faz sentido criar uma instância de Divida só para fazer essa validação, nesse caso? Note como a baixa coesão da classe dificulta o reaproveitamento do código dentro dela.

No entanto, se olharmos com atenção, veremos que podemos agrupar os métodos de acordo com os atributos com os quais eles trabalham.

    public class Divida {
      // atributos

      // métodos que trabalham com CNPJ
      public boolean cnpjValido() {...}
      private int primeiroDigitoCorretoParaCnpj() {...}
      private int primeiroDigitoVerificadorDoCnpj() {...}
      private int segundoDigitoCorretoParaCnpj() {...}
      private int segundoDigitoVerificadorDoCnpj() {...}
      public String getCnpjCredor() {...}
      public void setCnpjCredor(String cnpjCredor) {...}

      // métodos que trabalham com a lista de pagamentos
      public ArrayList<Pagamento> pagamentosAntesDe(Calendar data) {...}
      public ArrayList<Pagamento> pagamentosComValorMaiorQue(double valorMinimo) {...}
      public ArrayList<Pagamento> pagamentosDo(String cnpjPagador) {...}
      public void registra(Pagamento pagamento) {...}

      // outros métodos
    }

Note como os grupos de métodos são independentes. Conseguimos separá-los rapidamente em classes diferentes. Podemos, por exemplo, criar uma classe Cnpj para lidar somente com o tratamento do CNPJ da nota fiscal:

      public class Cnpj {
        private String cnpj;

        public boolean cnpjValido() {...}
        private int primeiroDigitoCorretoParaCnpj() {...}
        private int primeiroDigitoVerificadorDoCnpj() {...}
        private int segundoDigitoCorretoParaCnpj() {...}
        private int segundoDigitoVerificadorDoCnpj() {...}
        public String getCnpjCredor() {...}
        public void setCnpjCredor(String cnpjCredor) {...}
      }

Os nomes do atributo e dos métodos ficaram um pouco estranhos, não? Podemos renomeá-los, inclusive refletindo que a nossa classe representa qualquer CNPJ agora, e não apenas o CNPJ de um credor:

      public class Cnpj {
        private String valor;

        public boolean ehValido() {...}
        private int primeiroDigitoCorreto() {...}
        private int primeiroDigitoVerificador() {...}
        private int segundoDigitoCorreto() {...}
        private int segundoDigitoVerificador() {...}
        public String getValor() {...}
        public void setValor(String valor) {...}
      }

E agora, na classe Divida, basta termos uma instância de Cnpj e gerar um getter para ela:

    public class Divida {
      private double total;
      private double valorPago;
      private String credor;
      private Cnpj cnpjCredor = new Cnpj();
      private ArrayList<Pagamento> pagamentos = new ArrayList<Pagamento>();

      public Cnpj getCnpjCredor() {
        return this.cnpjCredor;
      }

      // outros métodos
    }

Precisamos mudar o código que cria uma Divida na classe BalancoEmpresa:

    public class BalancoEmpresa {
      public void registraDivida(String credor, String cnpjCredor, double valor) {
        Divida divida = new Divida();
        divida.setTotal(valor);
        divida.setCredor(credor);
        divida.getCnpjCredor().setValor(cnpjCredor); // agora usamos o getter e depois o setter
        dividas.put(cnpjCredor, divida);
      }
      public void pagaDivida(String cnpjCredor, double valor, String nomePagador, String cnpjPagador) {...}
    }

Fazendo isso, tiramos a responsabilidade de representar o CNPJ da classe Divida, deixando-a mais fácil de manter. Se precisarmos modificar algo associado ao CNPJ não precisamos procurar o código em várias classes, somente na classe Cnpj. Além disso, essa classe possui uma responsabilidade bem específica. Note como ela é pequena e reutilizável.

Utilizar tipos pequenos, como a classe Cnpj, também favorece a legibilidade do código, principalmente para quem vai usar nosso código posteriormente.

Podemos fazer o mesmo com os pagamentos da dívida. Criamos uma nova classe que represente uma lista de pagamentos, especificamente. Mas, melhor ainda, podemos aproveitar código já existente no Java para deixar nosso código mais expressivo: fazemos nossa nova classe estender ArrayList.

    public class Pagamentos extends ArrayList<Pagamento> {
      public ArrayList<Pagamento> pagamentosAntesDe(Calendar data) {
        ArrayList<Pagamento> pagamentosFiltrados = new ArrayList<Pagamento>();
        for (Pagamento pagamento : this) {
          if (pagamento.getData().before(data)) {
            pagamentosFiltrados.add(pagamento);
          }
        }
        return pagamentosFiltrados;
      }
      public ArrayList<Pagamento> pagamentosDo(String cnpjPagador) {
        ArrayList<Pagamento> pagamentosFiltrados = new ArrayList<Pagamento>();
        for (Pagamento pagamento : this) {
          if (pagamento.getCnpjPagador().equals(cnpjPagador)) {
            pagamentosFiltrados.add(pagamento);
          }
        }
        return pagamentosFiltrados;
      }
      public ArrayList<Pagamento> pagamentosComValorMaiorQue(double valorMinimo) {
        ArrayList<Pagamento> pagamentosFiltrados = new ArrayList<Pagamento>();
        for (Pagamento pagamento : this) {
          if (pagamento.getValor() > valorMinimo) {
            pagamentosFiltrados.add(pagamento);
          }
        }
        return pagamentosFiltrados;
      }
    }

Repare que é necessário alterarmos os métodos para percorrer os elementos de this.

Estendendo de ArrayList, ganhamos métodos para adicionar, remover e acessar diretamente itens da nota fiscal. Além disso, conseguimos usar uma instância da nossa classe no foreach do Java:

      Pagamentos pagamentos = new Pagamentos();
      // adiciona alguns pagamentos na colecao
      for (Pagamento pagamento : pagamento) {
        System.out.println(pagamento.getValor());
      }

Note que, na classe Divida, o método registra mexe com o atributo valorPago além da lista de itens:

      public void registra(Pagamento pagamento) {
        double valor = pagamento.getValor();
        if (valor < 0) {
          throw new IllegalArgumentException("Valor invalido para pagamento");
        }
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
        this.pagamentos.add(pagamento);
      }

Mas veja também que esse atributo está diretamente relacionado com os pagamentos da dívida. O valor pago da dívida está diretamente relacionado com os pagamentos já realizados. Faz sentido, então, passarmos esse atributo para a nossa nova classe. Colocamos também um getter para podermos utilizá-lo no resto do sistema.

      public class Pagamentos extends ArrayList<Pagamento> {
        private double valorPago;

        // outros métodos

        public double getValorPago() {
          return this.valorPago;
        }
      }

Fazendo isso, podemos passar agora o método registra para a classe Pagamentos, deixando nela tudo o que é relacionado a uma lista de pagamentos:

      public class Pagamentos extends ArrayList<Pagamento> {
        private double valorPago;

        // outros métodos

        public void registra(Pagamento pagamento) {
          double valor = pagamento.getValor();
          if (valor < 0) {
            throw new IllegalArgumentException("Valor invalido para pagamento");
          }
          if (valor > 100) {
            valor = valor - 8;
          }
          this.valorPago += valor;
          this.add(pagamento);
        }
      }

Por fim, adaptamos a classe Divida para utilizar a nossa nova classe. Apagamos a lista e o atributo valorPago que passamos para a classe Pagamentos e, no lugar, colocamos um atributo do tipo dessa classe. Apagamos, também, os métodos copiados e, no lugar, colocamos um getter para o novo atributo.

      public class Divida {
        private double total;
        private String credor;
        private Cnpj cnpjCredor = new Cnpj();
        private Pagamentos pagamentos = new Pagamentos();

        public Pagamentos getPagamentos() {...}
        public Cnpj getCnpj() {...}

        // outros métodos
      }

Além disso, precisamos mudar o método pagaDivida na classe BalancoEmpresa para usar nossa nova classe:

    public class BalancoEmpresa {
      public void registraDivida(String credor, String cnpjCredor, double valor) {...}
      public void pagaDivida(String cnpjCredor, double valor, String nomePagador, String cnpjPagador) {
        Divida divida = dividas.get(cnpjCredor);
        if (divida != null) {
          Pagamento pagamento = new Pagamento();
          pagamento.setCnpjPagador(cnpjPagador);
          pagamento.setPagador(nomePagador);
          pagamento.setValor(valor);
          divida.getPagamentos().registra(pagamento);
        }
      }
    }

Compare, agora, a classe Divida antes e depois das nossas refatorações. Veja como ela ficou mais simples. Repare também como as responsabilidades, que antes estavam todas na classe Divida, agora estão separadas em classes com papéis bem definidos, ou seja, mais coesas. Além disso, a classe Divida só precisa saber agora o que as classes Cnpj e Pagamentos fazem, não como fazem: ganhamos em encapsulamento!

A separação de responsabilidades em classes é um conceito tão importante em orientação a objetos que é considerado um princípio do paradigma, conhecido como Single Responsibility Principle. Esse princípio diz que uma classe deve ter somente uma responsabilidade e que uma responsabilidade deve estar encapsulada inteiramente em uma classe. Isso tende a deixar nosso código mais simples, pois, com as responsabilidades bem isoladas em classes distintas, fica mais fácil saber qual parte do código executa cada lógica e onde precisamos mexer para alterar um determinado comportamento.

Note como antes tínhamos uma classe sem responsabilidade nenhuma, anêmica. Depois passamos para o oposto: uma classe com muitas responsabilidades distintas. Por fim, chegamos numa distribuição de responsabilidades mais aceitável, com classes pequenas e coesas. É importante observar que o que é aceitável depende do domínio que estamos representando e do quanto queremos quebrar as responsabilidades. Sempre é possível deixar as responsabilidades mais distribuídas; cabe ao desenvolvedor decidir até onde ele quer dividí-las.

Herança: quando não usar - Herança: quando não usar

No capítulo anterior, criamos a classe Pagamentos para representar uma lista de pagamentos. Fizemos ela herdar de ArrayList, afinal ela é uma lista de Pagamento. Mas veja só os métodos que nossa classe ganhou com a herança:

Autocomplete do Eclipse cheio de métodos que não queríamos

Fizemos o método registra, mas temos um método add. Um deles calcula os impostos incidentes no pagamento e atualiza o valor total pago, outro não. Qual deles devemos chamar? Podemos resolver isso sobrescrevendo o método add, para que ele contenha a lógica que está no método registra.

    public class Pagamentos extends ArrayList<Pagamento> {
      private double valorPago;

      @Override
      public boolean add(Pagamento pagamento) {
        double valor = pagamento.getValor();
        if (valor < 0) {
          throw new IllegalArgumentException("Valor invalido para pagamento");
        }
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
        return super.add(pagamento);
      }

      // outros métodos
    }

Daí o método registra passa a chamar diretamente o método add:

    public class Pagamentos extends ArrayList<Pagamento> {
      private double valorPago;

      @Override
      public boolean add(Pagamento pagamento) {...}

      public void registra(Pagamento pagamento) {
        this.add(pagamento);
      }

      // outros métodos
    }

Podemos até apagar o método registra, para evitar confusões. Mas, veja só, temos ainda os métodos add(int index, Pagamento element) e os métodos addAll. Será que precisamos reimplementá-los também? O método addAll deve usar o add, certo? Veja o que acontece se só implementarmos o add:

    Pagamento p1 = new Pagamento();
    Pagamento p2 = new Pagamento();

    p1.setValor(105);
    p2.setValor(25);

    Pagamentos pagamentos1 = new Pagamentos();
    pagamentos1.add(p1);
    pagamentos1.add(p2);

    System.out.println("Valor já pago: " + pagamentos1.getValorPago()); // 122.0

    Pagamentos pagamentos2 = new Pagamentos();
    ArrayList<Pagamento> adicionar = new ArrayList<Pagamento>();
    adicionar.add(p1);
    adicionar.add(p2);
    pagamentos2.addAll(adicionar);

    System.out.println("Valor já pago: " + pagamentos2.getValorPago()); // 0.0

Então podemos sobrescrevê-lo também. Fazemos ele calcular o valor pago, já com descontos, e, em seguida, delegamos a adição dos elementos para a implementação da classe mãe.

    @Override
    public boolean addAll(Collection<? extends Pagamento> adicionar) {
      for (Pagamento pagamento : adicionar) {
        double valor = pagamento.getValor();
        if (valor < 0) {
          throw new IllegalArgumentException("Valor invalido para pagamento");
        }
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
      }
      return super.addAll(adicionar);
    }

Podemos fazer isso sem medo porque agora sabemos que a implementação do addAll da classe mãe não chama o add. Não corremos o risco de calcular duas vezes os impostos e o valor da nota. Mas será que isso vale para todas as coleções do Java?

Imagine que queremos evitar pagamentos duplicados na dívida. Fazemos nossa classe Pagamentos estender HashSet, mantendo nossa implementação do addAll. Veja só o que acontece agora com os valores dos impostos e da nota:

    Pagamento p1 = new Pagamento();
    Pagamento p2 = new Pagamento();

    p1.setValor(105);
    p2.setValor(25);

    Pagamentos pagamentos1 = new Pagamentos();
    pagamentos1.add(p1);
    pagamentos1.add(p2);

    System.out.println("Valor já pago: " + pagamentos1.getValorPago()); // 122.0

    Pagamentos pagamentos2 = new Pagamentos();
    ArrayList<Pagamento> adicionar = new ArrayList<Pagamento>();
    adicionar.add(p1);
    adicionar.add(p2);
    pagamentos2.addAll(adicionar);

    System.out.println("Valor já pago: " + pagamentos2.getValorPago()); // 244.0

A implementação de addAll da classe HashSet, ao contrário da classe ArrayList, chama o método add. Acabamos somando o valor dos pagamentos duas vezes! Note como precisamos conhecer a classe que estendemos para implementar nossa lógica. E note que, se a implementação da classe mãe mudar por algum motivo, nossa classe pode parar de funcionar. Isso significa que o acoplamento com a classe mãe é muito alto!

Ainda temos um outro problema. Faz sentido termos um método para remover pagamentos já realizados de uma dívida? Mesmo se a resposta for não, temos os métodos remove, removeAll e clear.

E trocar um pagamento registrado por outro? Talvez não faça sentido, mas temos o método set, que troca o elemento em uma posição. Veja quanto comportamento herdamos sem querer! E o quanto podemos fazer, sendo que não queremos liberar!

Perceba que estamos estendendo a classe ArrayList mas precisamos mudar muito o comportamento dela para adequá-la às nossas necessidades. Ainda por cima, temos métodos indesejados na nossa classe. Isso é um sinal de que não deveríamos estar usando herança nesse caso. Mas como reaproveitar o código que já existe na classe ArrayList sem usar herança?

Em vez de fazer nossa classe Pagamentos estender ArrayList, podemos fazê-la ter uma instância de ArrayList como atributo e aproveitar o código da classe ArrayList invocando os métodos que desejarmos. Veja, por exemplo, o método registra, como fica.

    public class Pagamentos {
      private double valorPago;
      private ArrayList<Pagamento> pagamentos = new ArrayList<Pagamento>();

      public void registra(Pagamento pagamento) {
        double valor = pagamento.getValor();
        if (valor < 0) {
          throw new IllegalArgumentException("Valor invalido para pagamento");
        }
        if (valor > 100) {
          valor = valor - 8;
        }
        this.valorPago += valor;
        this.pagamentos.add(pagamento);
      }

      // outros métodos
    }

Note que agora, para reaproveitar um comportamento da classe ArrayList, precisamos invocar explicitamente um método dessa classe. Não ganhamos automaticamente todos os métodos que a classe ArrayList tem. Para ganharmos um comportamento, precisamos deixar explícito que queremos reaproveitá-lo. Perceba como continuamos reaproveitando o comportamento mas, desse modo, conseguimos controlar o que queremos reaproveitar ou não.

Uma outra coisa muito importante a perceber é que agora, se a implementação da classe ArrayList mudar, nossa implementação não quebra. Nossa implementação só quebrará se os métodos de ArrayList mudarem, ou seja, se a interface de ArrayList mudar. Não precisamos mais conhecer intimamente a classe que queremos reaproveitar. Diminuímos o acoplamento entre a classe ArrayList e a classe Pagamentos.

Isso que acabamos de fazer foi trocar uma herança por uma composição. Agora, em vez de nossa classe ser um ArrayList, ela tem um ArrayList. Sempre que temos uma classe herdando de outra, é possível trocar essa herança por uma composição, ou seja, substituir o extends por uma instância da classe que queremos herdar e métodos na nossa classe que chamam métodos dessa instância.

Veja que, ao substituirmos herança por composição, perdemos, por exemplo, o método contains. Mas se precisarmos saber se um pagamento foi efetuado, podemos recuperar esse comportamento. Mais ainda: podemos dar um nome mais significativo para ele.

    public class Pagamentos {
      private double valorPago;
      private ArrayList<Pagamento> pagamentos = new ArrayList<Pagamento>();

      public boolean foiRealizado(Pagamento pagamento) {
        return pagamentos.contains(pagamento);
      }

      // outros métodos
    }

Herança é um recurso que deve ser usado com muito cuidado! Existem casos mesmo dentro da linguagem Java em que a herança foi mal utilizada, como na implementação da classe Stack, que estende Vector, ou da classe Properties, que estende Hashtable. Essas classes acabaram herdando métodos que não fazem sentido para elas e, para o desenvolvedor saber quais métodos ele pode ou não usar, ele precisa olhar na documentação da classe.

Dando preferência a composição sobre herança, evitamos quebra de encapsulamento de nossas classes. Diminuímos o acoplamento entre nossas classes, evitando, assim, que uma mudança em uma única classe quebre várias partes de nosso sistema.

Sobre o curso Orientação a Objetos: Melhores técnicas com Java

O curso Orientação a Objetos: Melhores técnicas com Java possui 82 minutos de vídeos, em um total de 45 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

  • 1241 cursos

    Cursos de programação, UX, agilidade, data science, transformação digital, mobile, front-end, marketing e infra.

  • Certificado de participação

    Certificado de que assistiu o curso e finalizou as atividades

  • App para Android e iPhone/iPad

    Estude até mesmo offline através das nossas apps Android e iOS em smartphones e tablets

  • Projeto avaliado pelos instrutores

    Projeto práticos para entrega e avaliação dos professores da Alura com certificado de aprovação diferenciado

  • Acesso à Alura Start

    Cursos de introdução a tecnologia através de games, apps e ciência

  • Acesso à Alura Língua

    Reforço online de inglês e espanhol para aprimorar seu conhecimento

Premium

  • 1241 cursos

    Cursos de programação, UX, agilidade, data science, transformação digital, mobile, front-end, marketing e infra.

  • Certificado de participação

    Certificado de que assistiu o curso e finalizou as atividades

  • App para Android e iPhone/iPad

    Estude até mesmo offline através das nossas apps Android e iOS em smartphones e tablets

  • Projeto avaliado pelos instrutores

    Projeto práticos para entrega e avaliação dos professores da Alura com certificado de aprovação diferenciado

  • Acesso à Alura Start

    Cursos de introdução a tecnologia através de games, apps e ciência

  • Acesso à Alura Língua

    Reforço online de inglês e espanhol para aprimorar seu conhecimento

12X
R$75
à vista R$900
Matricule-se

Premium Plus

  • 1241 cursos

    Cursos de programação, UX, agilidade, data science, transformação digital, mobile, front-end, marketing e infra.

  • Certificado de participação

    Certificado de que assistiu o curso e finalizou as atividades

  • App para Android e iPhone/iPad

    Estude até mesmo offline através das nossas apps Android e iOS em smartphones e tablets

  • Projeto avaliado pelos instrutores

    Projeto práticos para entrega e avaliação dos professores da Alura com certificado de aprovação diferenciado

  • Acesso à Alura Start

    Cursos de introdução a tecnologia através de games, apps e ciência

  • Acesso à Alura Língua

    Reforço online de inglês e espanhol para aprimorar seu conhecimento

12X
R$100
à vista R$1.200
Matricule-se

Max

  • 1241 cursos

    Cursos de programação, UX, agilidade, data science, transformação digital, mobile, front-end, marketing e infra.

  • Certificado de participação

    Certificado de que assistiu o curso e finalizou as atividades

  • App para Android e iPhone/iPad

    Estude até mesmo offline através das nossas apps Android e iOS em smartphones e tablets

  • Projeto avaliado pelos instrutores

    Projeto práticos para entrega e avaliação dos professores da Alura com certificado de aprovação diferenciado

  • Acesso à Alura Start

    Cursos de introdução a tecnologia através de games, apps e ciência

  • Acesso à Alura Língua

    Reforço online de inglês e espanhol para aprimorar seu conhecimento

12X
R$120
à vista R$1.440
Matricule-se
Procurando planos para empresas?

Acesso por 1 ano

Estude 24h/dia onde e quando quiser

Novos cursos todas as semanas