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
tokenVendaedataHoraVendaexistem em função daVendado ingresso e não doIngressoem si. Logo pode ser extraídos (o nome dos atributos já denunciava um não-pertencimento àquele lugar); - O atributo
randome a contanteALPHAsó existem para a geração doTokenno métodogerarToken, 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;
}
// ...