Utilitário de Comparação

Tempo de leitura: menos de 1 minuto

Comparando os objetos

Um certa vez no trabalho, me deparei com um problema que já algum tempo era comum, principalmente nos testes, a necessidade de saber se um objeto foi modificado. Se sim, quais a diferenças ou igualdades entre o objeto original e o modificado. Também era necessário em alguns casos, remover um ou mais atributos da comparação, por exemplo o atributo objVersion (atributo usado pelo Hibernate para controle de  concorrência).

Então decidir perder um pouco mais de tempo para criar um utilitário de comparação de fácil uso.

O que seria mais fácil de usar que uma API usando Fluent Interface?

Como pede uma boa interface fluente, comecei elaborando sua forma de uso, ou seja, como o desenvolvedor gostaria de usar esse utilitário.

Abrindo um parenteses, uma das coisas que mais gosto de fazer é inventar utilitários, principalmente utilizando a linguagem Groovy, fecha parenteses.

Veja como ficou a forma de uso:

[code language=”groovy”]
//Obtendo as diferenças
ComparadorUtil.comparaObjetoOriginal(tal)
.comModificado(tal2).obtendoMapaComDiferencas();
//Obtendo as igualdades
ComparadorUtil.comparaObjetoOriginal(tal)
.comModificado(tal2).obtendoMapaComIgualdades();
[/code]

O quê acharam? Ficou simples o uso? acho que sim né!

Eis que o problema do tempo aparece, lembra que falei que ia perder um pouco mais de tempo, então… como fica a implementação desse utilitário? Como falei no post Interface Fluente e StepBuilders, a Fluent Interface tem a mesma característica do Builders, uso simplificado, mas muito mais código a escrever.

Mas chega de papo furado, vamos ao que interessa.

Implementação do Utilitário

A classe ComparadorUtil, possui duas inner-classes, necessárias para dar a fluides no uso do utilitário:

[code language=”groovy”]
class ComparadorUtil {
class ComparadorDeObjeto {…}
class OperacoesComparacao {…}

public ComparadorDeObjeto comparaObjetoOriginal(pObjeto){
return new ComparadorDeObjeto(pObjeto)
}
}
[/code]

Na classe utilitária, apenas à um método (comparaObjetoOriginal). Ele recebe o objeto original e repassa para a primeira inner-class (ComparadorDeObjeto).

Esta tem por objetivo, guardar o objeto original, para comparar com o objeto modificado. Poderá guardar também os campos que o usuário precisa comparar.

Nota: Em alguns casos, não existe a necessidade de comparar o objeto inteiro, basta alguns campos.

[code language=”groovy”]
class ComparadorDeObjeto {

private def objetoOriginal
private def camposAComparar = []

public ComparadorDeObjeto(pObjeto) {
objetoOriginal = pObjeto
}

public OperacoesComparacao comModificado(pObjetoModificado) {
validaObjeto(pObjetoModificado)
final Map mapDadosModificados = obterMapaDadosObjetoModificado(pObjetoModificado)
final Map mapComDiferencas = [:]
final Map mapComIgualdades = [:]
mapDadosModificados.each { chave, valorModificado ->
if (objetoOriginal.respondsTo("get${chave.capitalize()}")) {
if (objetoOriginal."${chave}" != valorModificado) {
mapComDiferencas.put(chave, valorModificado)
} else {
mapComIgualdades.put(chave, valorModificado)
}
}
}
return new OperacoesComparacao(mapComDiferencas, mapComIgualdades)
}

public ComparadorDeObjeto peloCampos(… pListaCampos) {
camposAComparar.addAll(pListaCampos)
return this
}

private Map obterMapaDadosObjetoModificado(pObjetoModificado) {
Map retorno = pObjetoModificado instanceof Map ? pObjetoModificado : PropertyUtils.describe(pObjetoModificado)
if (camposAComparar) {
Map subRetorno = retorno.findAll{ campo, valor -> camposAComparar.contains(campo)}
retorno = subRetorno
}
return retorno;
}

private void validaObjeto(pObjetoModificado) {
if (!pObjetoModificado.getClass().equals(objetoOriginal.getClass()) &&
!(pObjetoModificado instanceof Map)) {
throw new IllegalArgumentException("Objeto Modificado deve ser do mesmo tipo de ${objetoOriginal.class.name} ou instancia de ${Map.class.name}");
}
}
}
[/code]

Na linha 6, o construtor recebe o objeto original e o guarda. Já o método comModificado (linha 10), é o responsável pro toda lógica, recebe o objeto modificado, submete o mesmo para validação (linha 41), necessário para saber se o objeto modificado é um Map ou é do mesmo tipo do objeto original, caso contrário não existe a possibilidade de comparação.

Feita a validação, os atributos do objeto modificado é extraídos em um Map, isso facilita a comparação de atributo por atributo. O método responsável pela extração é obterMapaDadosObjetoModificado (linha 32).

Com o Map de atributos do objeto modificado em mãos, fazemos a comparação de cada um dos atributos (linha 15). Se o valor do atributo modificado for diferente do valor do atributo original (linha 17), então o mesmo adicionado no Map de diferenças (linha 18), se não, a tributo é adicionado no Map de igualdades (linha 20).

Isso é basicamente com o utilitário funciona.

Mas ainda temos o método peloCampos (linha 27) que recebe uma lista de atributos que o usuário deseja comparar. Caso esse método não seja chamado, o comparador comparará todos os atributos da classe.

Já segunda inner-class (OperacoesComparacao), recebe dois Maps, um com as diferenças e outro com as igualdades (os mesmos criados entre as linhas 17 e 19, disponibilizando métodos de acesso para obtenção dos mesmos.

[code language=”java”]
class OperacoesComparacao {

private Map mapaComDiferencas
private Map mapaComIgualdades

public OperacoesComparacao(Map pMapaComDiferencas, Map pMapaComIgualdades) {
mapaComDiferencas = pMapaComDiferencas
mapaComIgualdades = pMapaComIgualdades
}
/**
* @return Mapa contendo as diferenças entre o objetoOriginal e o modificado
* (Chave: nome do atributo, Valor: valor do atributo)
*/
public Map<String, Object> obtendoMapaComDiferencas() {
return mapaComDiferencas;
}
/**
* @return Mapa contendo as diferenças entre o objetoOriginal e o modificado
* (Chave: nome do atributo, Valor: valor do atributo)
*/
public Map<String, Object> obtendoMapaComIgualdades() {
return mapaComIgualdades;
}
}
[/code]

Na linha 6, o construtor recebe os Maps e o guarda, disponibilizando-os para consulta do usuário nas linhas 19 e 21.

Disponibilizei no Github o código completo do utilitário. Quem sabe possa ajuda-lo.

Aproveito para lembra-los, que dia 17/03/2016, é o dia limite para envio da solução do CodeKata #3: Restaurante, participe!!

Abraços.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *