Sistemas de vendas costumam ter bases com grande volume de dados e muitas vezes, para evitar que os sistemas façam muitas consultas desnecessárias à estas bases, implementamos caches de objetos.
Existem "N" formas de implementar um cache, a mais simples é utilizar uma implementação de java.util.Map, como por exemplo um HashMap como veremos exemplo a seguir:
Imagine uma classe hipotética Pedido que possui uma lista de objetos do tipo Item, no nosso exemplo, vamos assumir que consultar a base de dados para obter os itens de um pedido, é uma operação extremamente demorada, por isso vamos utilizar um HashMap como cache para os itens de um pedido.
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Pedido {
// cache dos itens do pedido
private static final Map<Pedido, List<Item>> CACHE = new HashMap<Pedido, List<Item>>();
// numero do pedido
private int numero;
// itens do pedido
private List<Item> itens;
// método que obtém os itens deste pedido
public List<Item> getItens() {
Pedido pedido = this;
// Tentamos obter os itens do cache
itens = CACHE.get(pedido);
// encontramos itens para este pedido?
if (itens == null) {
// não, então sincronizamos o acesso ao cache
synchronized (CACHE) {
// e tentamos de novo
itens = CACHE.get(pedido);
// encontramos itens para este pedido?
if (itens == null) {
// ainda não
// então acessamos a base de dados para obter os itens deste pedido
itens = BaseDados.carregarItensPedido(pedido.getNumero());
// uma vez obtidos os itens, armazenamos eles no cache
CACHE.put(pedido, itens);
}
}
}
// e devolvemos os itens para quem solicitou
return itens;
}
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
}
Perceba que a implementação do cache é algo relativamente simples, um HashMap onde a chave é o próprio Pedido e o valor é uma lista de objetos do tipo Item. O problema é quando começamos a ter objetos com mais tipos de dados, por exemplo uma nota fiscal com uma lista de itens correspondentes à produtos e outra lista correspondente a serviços. Aí precisaremos duplicar códigos parecidos com os do método getItens() para verificar o cache e carregar os objetos se necessário. Aos poucos teremos um código gigante, duplicado e pouco robusto.
Uma alternativa elegante, é desacoplar o algoritmo que verifica o cache do algorimo que faz o carregamento dos dados.
Primeiro vamos definir uma interface para os algoritmos responsáveis por realizar a carga de dados, ou qualquer outra operação que possa ser custosa. Chamaremos esta interface de CacheLoader:
package br.com.staroski.tools.cache;
/**
* Interface para carregar objetos para dentro de um {@link Cache cache}.
*
* @author Ricardo Artur Staroski
*
* @param <K> Tipo de dado da chave de busca do {@link Cache cache}.
* @param <V> Tipo de dado do objeto carregado a partir da chave de busca.
* @see Cache
*/
public interface CacheLoader<K, V> {
/**
* Utiliza a chave de busca informada para carregar um valor.
*
* @param key A chave de busca.
* @return O valor carregado a partir da chave de busca.
*/
public V load(K key);
}
Para armazenar os objetos carregados pelo CacheLoader, vamos precisar de uma chave no nosso Cache, não se preocupe com os detalhes da implementação desta chave, você entenderá mais a frente, vamos chamar esta classe de MultiKey:
package br.com.staroski.tools.cache;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* Esta classe representa uma chave composta por mais de um objeto.
*
* @author Ricardo Artur Staroski
*/
@SuppressWarnings("unchecked")
public final class MultiKey implements Comparable<MultiKey> {
private final int hashCode;
private final List<Object> objects;
/**
* Instancia uma nova {@link MultiKey chave composta}.
*
* @param key1 O primeiro objeto da chave
* @param key2 O segundo objeto da chave.
* @param moreKeys Os outros objetos da chave. (<B><I>Opcional</I></B>)
*/
public MultiKey(Object key1, Object key2, Object... moreKeys) {
List<Object> objects = new LinkedList<>();
objects.add(key1);
objects.add(key2);
objects.addAll(Arrays.asList(moreKeys));
this.objects = objects;
this.hashCode = objects.hashCode();
}
@Override
public int compareTo(MultiKey other) {
return this.hashCode - (other == null ? 0 : other.hashCode);
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object instanceof MultiKey) {
final MultiKey that = (MultiKey) object;
// se os hashes são diferentes, então de cara já sei que os objetos também são diferentes
if (this.hashCode != that.hashCode) {
return false;
}
// objetos diferentes podem ter hashes iguais, então comparo eles
return this.objects.equals(that.objects);
}
return false;
}
/**
* Obtém o objeto do índice informado desta {@link MultiKey chave composta}.
*
* @param index O índece do objeto que se deseja obter.
* @return O objeto no índice informado.
*/
public <T> T get(int index) {
return (T) objects.get(index);
}
@Override
public int hashCode() {
return this.hashCode;
}
/**
* @return A quantidade de objetos que compõe esta {@link MultiKey chave composta}.
*/
public int size() {
return objects.size();
}
}
Agora que temos a implementação de uma chave para nosso Cache, vamos implementar o algorimo de consulta ao cache numa classe com um nome mais do que apropriado, Cache:
package br.com.staroski.tools.cache;
import java.util.HashMap;
import java.util.Map;
/**
* Implementação de um cache de objetos.<BR>
* Instâncias desta classe dependem de um {@link CacheLoader} para carregar os objetos sob demanda.<BR>
* Como já dizia o véio deitado: <I>Se cache, cache! Se não cache, diz!</I>
*
* @author Ricardo Artur Staroski
*
* @see CacheLoader
*/
@SuppressWarnings("unchecked")
public final class Cache {
// mapa que armazena os objetos carregados
private final Map<MultiKey, Object> map = new HashMap<>();
/**
* Limpa este {@link Cache cache}.
*/
public void clear() {
map.clear();
}
/**
* Obtém o valor a partir da chave de busca informada.<BR>
* Se o valor ainda não tiver sido carregado, então o {@link CacheLoader} informado será utilizado para carregá-lo e armazenar no {@link Cache cache}.
*
* @param loader O {@link CacheLoader} responsável por carregar o valor.
* @param key A chave de busca utilizada para obter o valor.
* @return O valor encontrado para a chave informada, se a chave for <code>null</code>, o retorno será <code>null</code>.
* @throws IllegalArgumentException se o {@link CacheLoader} informado for <code>null</code>
*/
public <K, V> V get(final CacheLoader<K, V> loader, final K key) {
if (loader == null) throw new IllegalArgumentException("loader cannot be null");
if (key == null) {
return null;
}
final MultiKey pair = new MultiKey(loader, key);
V value = (V) map.get(pair);
if (value == null) {
synchronized(this) { // os 2 if's são realmente iguais, não é idiotice não, é um "double-checked locking"
value = (V) map.get(pair);
if (value == null) {
value = loader.load((K) key);
map.put(pair, value);
}
}
}
return value;
}
}
Perceba que o algoritmo implementado no método get do Cache é idêntico à verificação realizada no método getItens do nosso primeiro exemplo. A única diferença é que nossa classe Cache espera dois parâmetros: o primeiro é o objeto CacheLoader e o segundo é o objeto que será utilizado como chave de busca no cache. Agora você vai entender a necessidade da classe MultiKey, a classe Cache utiliza como chave de busca uma composição entre o CacheLoader informado e a própria chave de busca. Desta forma podemos, por exemplo ter diferentes CacheLoaders para uma mesma chave de busca.
Vejamos agora como ficaria o exemplo do Pedido e sua lista de Itens:
import java.util.List;
import br.com.staroski.tools.cache.Cache;
import br.com.staroski.tools.cache.CacheLoader;
public class Pedido {
// agora temos uma intância de Cache ao invés de HashMap
private static final Cache CACHE = new Cache();
// temos também a implementação de um CacheLoader que será responsável por carregar os itens do pedido recebido como parâmetro
private final CacheLoader<Pedido, List<Item>> itens = new CacheLoader<Pedido, List<Item>>() {
public List<Item> load(Pedido pedido) {
return BaseDados.carregarItensPedido(pedido.getNumero());
}
};
// numero do pedido
private int numero;
// método que obtém os itens deste pedido
public List<Item> getItens() {
// agora basta utilzar nosso objeto CACHE, passando o CacheLoader que chamamos de 'itens' e o próprio Pedido (this)
return CACHE.get(itens, this);
}
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
}
Agora não há mais necessidade de escrever a lógica de verificação do cache, ela está incorporada na classe Cache e pode ser reaproveitada. Para cada tipo de dado diferente que desejarmos carregar, basta implementar um CacheLoader diferente, poderíamos por exemplo ter um CacheLoader para produtos, outro CacheLoader para serviços e assim por diante.
É isso, espero que o exemplo possa ser útil.