11. COESÃO

“Ninguém na breve história da computação já escreveu uma peça perfeita de software. É improvável que sejas o primeiro.”

Andy Hunt

Bastante do esforço na Programação Orientada a Objeto está em decidir onde (em que objeto) colocar as responsabilidades (os dados e métodos). Frequentemente, acabamos com um objeto que tem muitas responsabilidades e, no pior caso, não relacionadas. A solução é, geralmente, dividir este comportamento em unidades menores e bem definidas ou, melhor dizendo, coesas. Neste capítulo, então, é tratado um traço qualitativo dos módulos chamado coesão, seu conceito, identificação e melhoria.

11.1 Conceito de Coesão

A coesão é uma expressão usada com muita frequência no campo da programação ou engenharia de softwares. Seu significado, na área, é similar ao entendimento comum ou, especialmente, na escrita de textos. A coesão textual em uma redação diz respeito a harmonia entre os elementos textuais e uma conexão lógica entre as partes de um texto.

Na programação, as classes, que descrevem os objetos, também precisam de harmonização e conexão lógica entre seus elementos. A coesão, portanto, é o grau em que os elementos de uma classe (ou módulo) fazem sentido juntos.

Coesão pode ser objetivamente observada e serve, logo, como um índice da qualidade, que mede o grau de independência dos módulos (classes, pacotes). Módulos de software que são coesos permitem que um programa seja mais seguro, confiável e, assim como os textos, inteligível.

11.2 Alta Coesão > Baixa Coesão

Uma classe, pode ter uma baixa ou alta coesão. O mesmo vale para um pacote, ou módulo, ou até o (sub)sistema inteiro. A baixa coesão é o resultado da pouca ou nenhuma relação dos atributos e métodos de uma classe ou das próprias classes em um pacote. A alta coesão, por outro lado, é a precisa separação de um modo que estes elementos façam sentido juntos.

Por exemplo, considere a necessidade de registrar a compra de um ticket para um show, como é feito nos websites e apps para eventos e shows. Vamos partir do pressuposto que existisse uma classe Ingresso, o qual seria vendido para um Cliente, conforme exemplo a seguir:

// Ingresso.java
class Ingresso {

  private static final String ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*0123456789";
  private Random random = new Random();
  private int id;
  private String tokenVenda;
  private Evento evento;
  private LocalDateTime dataHoraVenda;
  private Cliente cliente;

  Ingresso(int id, Evento evento) {
    this.id = id;
    this.evento = evento;
  }

  public void vender(Cliente cli) {
    this.dataHoraVenda = LocalDateTime.now();
    this.tokenVenda = gerarToken(10);
    this.cliente = cli;
  }

  private String gerarToken(int length) {
    String token = "";
    for (int i = 0; i < length; i++) {
      token += ALPHA.charAt(random.nextInt(ALPHA.length()));
    }
    return token;
  }
}

O exemplo não é muito longo, para caber num livro, mas a ideia básica está ali. Quais são os estados e comportamentos, ou atributos e métodos, que realmente pertencem ao Ingresso? Um meio de medir é contar as menções dos atributos no métodos e identificar clusters, isto é, blocos ou agrupamentos de código, pedaços de lógica que anda junta. Neste exemplo, em particular, há a dataHoraVenda e o tokenVenda, onde ambos pertencem a uma entidade Venda. Outro agrupamento observado é o método gerarToken que se baseia na constante ALPHA. A Venda é aparte do Ingresso, assim como a geração do token, que é um problema por si só. A classe Ingresso tem baixa coesão, pois contém lógica que não precisa ser sua responsabilidade.

11.3 Aumentando a coesão

Sabendo que a causa da baixa coesão é a lógica não-relacionada, a solução típica para o aumento da coesão é relacionar essa lógica pela separação em blocos coesos. Há um fator que ajuda bastante nesta tarefa: classes e pacotes menores.

Classes muito longas tendem a ser pouco coesas. É pouco provável que todos os métodos usem todos os atributos ou tenham relação entre si. No caso de pacotes ou conjuntos muito grande de classes, é pouco provável que todas estas classes tenham relação direta ou próxima ou, pelo menos, podem haver subconjuntos de classes com mais afinidade.

Portanto, a coesão aumenta conforme os módulos se tornam menores, onde módulos se entende por uma classe, um arquivo, um pacote ou agrupamento de classes e arquivos. A alteração de códigos, buscando a organização e melhoria da qualidade interna sem mudar o comportamento geral, é chamada refatoração.

11.4 Refatoração: extração/introdução

As refatorações são catalogadas, dependendo do problema que resolvem. Na categoria de quebrar módulos muito grandes em partes menores está a extração ou introdução de novas classes ou novos métodos.

No caso de classes pouco coesas, uma solução típica é extrair a lógica (atributos, métodos, etc) e introduzir uma nova classe ou superclasse.

Voltando ao exemplo do Ingresso que tem baixa coesão, é necessário analisar o seguinte:

  • Os atributos tokenVenda e dataHoraVenda existem em função da Venda do ingresso e não do Ingresso em si. Logo pode ser extraídos (o nome dos atributos já denunciava um não-pertencimento àquele lugar);
  • O atributo random e a contante ALPHA só existem para a geração do Token no método gerarToken, portanto pode, também, ser extraído.

Uma solução com coesão mais alta pode ser vista no código a seguir:

// Ingresso.java
class Ingresso {

  private int id;
  private Evento evento;
  private Venda venda;

  Ingresso(int id, Evento evento) {
    this.id = id;
    this.evento = evento;
  }

  Venda vender(Cliente cli) {
    if (this.isVendido()) throw new IllegalStateException("Ingresso já foi vendido");
    return this.venda = new Venda(cli, this);
  }
  // ...
}
// Venda.java
class Venda {

  private Cliente cliente;
  private Ingresso ingresso;
  private Token token;
  private LocalDateTime dataHora;

  Venda(Cliente cliente, Ingresso ingresso) {
    this.cliente = cliente;
    this.ingresso = ingresso;
    this.token = new Token(10);
    this.dataHora = LocalDateTime.now();
  }
  // ...
}
// Token.java
class Token {
  private static final String ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*0123456789";
  private Random random = new Random();
  private String token;
  private int length;

  Token(int length) {
    this.length = length;
    this.gerar();
  }

  public String gerar() {
    this.token = "";
    for (int i = 0; i < this.length; i++) {
      this.token += ALPHA.charAt(random.nextInt(ALPHA.length()));
    }
    return this.token;
  }
  // ...
}

No código anterior, foram extraídos os atributos tokenVenda e dataHoraVenda de Ingresso e reorganizados com a introdução da classe Venda, mais coesa. Ainda, o random e ALPHA foram extraídos e usados na introdução da classe Token, que utiliza todos os seus atributos no método gerar.

Com esta refatoração, Ingresso, Venda e Token têm uma afinidade melhor com seus atributos, métodos e responsabilidades, portanto, alta coesão.

11.5 Considerações

Como foi visto neste capítulo, o problema da baixa coesão foi solucionado através da adição de novas classes e delegando as responsabilidades para objetos especializados nestas, como no caso do Token. Enquanto aumenta a coesão, a introdução de novas classes e objetos implica na associação entre objetos para cumprir uma funcionalidade. Isto é, para manter um sistema coeso é preciso dividir as responsabilidades entre os objetos e associá-los. Por exemplo, a venda do Ingresso só é possível com a colaboração dos objetos Venda e Token. A associação é o relacionamento entre objetos, e esta característica colaborativa na Programação Orientada a Objetos será examinada em mais detalhes no capítulo seguinte: 12 // Associação.

11.6 Exercícios

Considere um imóvel e sua locação, conforme código a seguir. Refatore-o para aumentar sua coesão, tentando identificar os agrupamentos de dados e lógica que poderiam ser extraídos.

// Imovel.java
class Imovel {
  private boolean locado;
  private String locatario;
  private String cpfLocatario;
  private String locador;
  private String cpfLocador;
  private String endereco;
  private double valor;
  private LocalDate dataLocacao;
  private int pagamentosRestantes;

  Imovel(String locador, String cpfLocador, String endereco) {
    if (cpfLocador.length() != 11) {
      throw new IllegalArgumentException("CPF deve ter 11 dígitos");
    }
    for (char digito : cpfLocador.toCharArray()) {
      if (!Character.isDigit(digito)) {
        throw new IllegalArgumentException("CPF deve ter 11 dígitos");
      }
    }
    this.cpfLocador = cpfLocador;
    this.locador = locador;
    this.endereco = endereco;
  }

  void locar(String locatario, String cpfLocatario, double valor) {
    if (this.locado)
      throw new IllegalStateException("Imóvel já locado");
    if (cpfLocatario.length() != 11) {
      throw new IllegalArgumentException("CPF deve ter 11 dígitos");
    }
    for (char digito : cpfLocatario.toCharArray()) {
      if (!Character.isDigit(digito)) {
        throw new IllegalArgumentException("CPF deve ter 11 dígitos");
      }
    }
    this.locatario = locatario;
    this.cpfLocatario = cpfLocatario;
    this.valor = valor;
    this.pagamentosRestantes = 12; // meses
    this.dataLocacao = LocalDate.now();
    this.locado = true;
  }

  void renovar(double valor) {
    if (!this.locado)
      throw new IllegalStateException("Imóvel não está locado");
    this.valor = valor;
    this.pagamentosRestantes = 12;
    this.dataLocacao = LocalDate.now();
  }

  void pagar() {
    this.pagamentosRestantes--;
    if (this.pagamentosRestantes == 0) {
      this.terminar();
    }
  }

  void terminar() {
    this.locado = false;
    this.locatario = null;
    this.cpfLocatario = null;
    this.dataLocacao = null;
  }
  // ...