O contrato de um FastLane é muito semelhante ao de um `Iterator` em que os dados só podem ser usados um só laço.
Fornecer acesso rápido a coleções de dados em camadas inferiores, diminuindo a criação de objetos sem violar os contratos entre camadas.
Fastlane Reader (Leitor Via-Rápida), ou apenas, Fastlane, é um padrão de projeto que visa diminuir a criação de objetos durante a leitura de uma coleção de objetos diminuindo o caminho entre o consultor da lista e o repositorio dos dados reais. [1].
Num cenário de aplicação multi-camada os dados do objetos são lidos da fonte de dados (por exemplo, um banco de dados) e colocados em instâncias, que são depois adicionadas em algum objeto de coleção (como, por exemplo, List
). Criar todos esses objetos e colocar numa coleção seria demasiado oneroso quer em termos de memória, quer em termos de processamento. Estamos facilmente sujeitos a receber um OutOfMemoryError
ou a que o processo demore tempo demais. Isto é muito comum quando precisamos criar um relatório em que precisamos iterar todos os itens selecionados, mas existem demasiados dados para colocar na memória.
A solução é simples de entender, mas não necessariamente de implementar. Em vez de criarmos todos os objetos de uma vez, colocar na coleção e descartar a fonte de dados (normalmente um java.sql.ResultSet
) podemos encapsular a fonte de dados com a interface que queremos e construir o objeto de dados apenas quando necessário e um por vez.
A solução tipica é ter um objeto no padrão Iterator, em que temos um método para saber se ainda existem mais elementos e um métodos para obter o elemento seguinte.
A criação do objeto real que será retornado do iterador pode ser criado internamente, ou no caso geral podemos usar um objeto na padrão Factory. O objeto Factory
está encarregue de pegar os dados brutos da fonte e os transformar em objetos. O objeto resultante não será guardado em lugar algum, diminuindo a quantidade de memória alocada durante a iteração.
O contrato de um FastLane é muito semelhante ao de um `Iterator` em que os dados só podem ser usados um só laço.
Porque mantemos a conexão com o bando de dados aberta, precisamos fechá-la, após termos iterado os dados. Assim, normalmente o contrato de um Fastlane Reader também inclui implementar AutoCloseable
.
public interface FastlaneIterator<T> implements Iterator<T> , AutoCloseable {
public <R> FastlaneIterator<R> map(Function<T,R> mapping);
}
O método map
irá permitir criar outros FastlaneIterator a partir do original apenas transformando o objeto.
A seguir a estrutura de um objeto Fastlane que implementa a interface FastlaneIterator
e lê um java.sql.ResultSet
, que será a nossa fonte de exemplo. O tratamento de exceções é omitido para simplificar o entendimento.
public ResultSetFastlaneIterator<T> implements FastlaneIterator<T>{
private ResultSet rs;
private Factory<T> factory;
public ResultSetFastlaneIterator ( ResultSet rs , Factory<T> factory ){
this.rs = rs;
this.factory = factory;
}
@Override
public boolean hasNext (){
boolean hasNext = rs.next ();
if (!hasNext){ // não ha mais dados
rs.close();
}
return hasNext;
}
@Override
public <T> next (){
return factory.createObject ( rs ) ;
}
@Override
public void close (){
return rs.close(); // fecha o ResultSet só depois dos dados serem lidos
}
public <R> FastlaneIterator<R> map(Function<T,R> mapping){
return new TransformedFastlaneIterator(this, mapping);
}
}
Utilizamos o objeto factory
para construir o objeto a partir do registro atual no ResultSet
. Isso poderia ser feito diretamente no código do método next()
. Isso poderia ser feito diretamente no código do método next()
caso saibamos o tipo específico que será iterado.
Um detalhe importante é que apenas nos livramos da fonte de dados quando não houver mais dados para ler. O objeto de iteração implementado o FastLane pode continuar "vivo", mas liberamos os recursos que ele usa assim que possivel.
Comparemos os códigos utilizando o padrão Fastlane e sem utilizar o padrão. Este código lê objetos Customer
que seria escrito num objeto que implemente o padrão DAO , por exemplo. O tratamento de exeções foi removido para simplificação.
Repare que precisamos de outra implementação de FastlaneIterator
chamada TransformedFastlaneIterator
. Ela será responsável pela magia do padrão. A implementação de TransformedFastlaneIterator
se trata do padrão Decorator e aplica uma transformação a cada iteração.
class TransformedFastlaneIterator<T, O> implements TransformedFastlaneIterator<T>{
FastlaneIterator<O> original;
Function<O,T> transform;
TransformedFastlaneIterator(FastlaneIterator<O> original, Function<O,T> transform){
this.original = original;
this.transform = transform;
}
@Override
public boolean hasNext (){
return original.hasNext();
}
@Override
public <T> next (){
return transform.apply(original.next());
}
@Override
public void close (){
return original.close();
}
public <R> FastlaneIterator<R> map(Function<T,R> mapping){
return new TransformedFastlaneIterator(original,transform.andThen(mapping));
}
}
---
Podemos agora comparar como seria com e sem o uso do padrão FastLane Reader.
[source, java]
public Iterator<Customer> getIteratorForQuery (){
// obtém resultados do banco try(ResultSet rs = executeQuery ()){
Factory<Customer> factory = new CustomerFactory() ;
List<Customer> list = new LinkedList<Customer>() ;
// itera TODOS os objetos while ( rs.next ()){ list.add ( factory.createObject ( rs )) ; }
return list.iterator () ;
} // fecha e descarta o resultSet
}
public FastlaneIterator<Customer> getIteratorForQuery (){
// obtém resultados do banco ResultSet rs = executeQuery () ;
Factory<Customer> factory = new CustomerFactory() ;
// não acontece nenhuma iteração. A conexão não é fechada. return new ResultSetFastlaneIterator ( rs,factory ) ;
}
O código que usa o padrão é apenas uma inicialização do objeto que implementa o padrão Fastlane, no caso o objeto `ResultSetFastlaneIterator` . Não ha iteração nem fabricação de nenhum objeto. Quando temos mais de uma camada, frequentemente, temos que transformar os objetos recebidos da camada inferior para os objetos da camada atual. Vamos fazer uma tabela das diferenças para ficar mais claro. Em geral, para um sistema com K camadas, para uma pesquisa que retorna N objetos, seria: |==== | | Objetos na memória | Número de Iterações | Número de itens iterados | sem Fastlane | N * K | K | N * K | com Fastlane | 2 | 1 | N |==== Usando Fastlane dimuimos os itens na memória e minimizamos o número de iterações sobre os dados. A criação do objeto é delegada a um Factory. Eis um exemplo de como seria o código do método `createObject` == Discussão O padrão FastLane Reader pretender acelerar a leitura de um conjunto de registros de forma eficiente, mesmo na presença de várias camadas. A forma de fazer isso é elidir da separação de camadas e acessar internamente à camada de dados de uma qualquer outra camada superior, mas sem violar o encapsulamento. O utilizador do objeto que implementa o padrão não tem conhecimento de que os dados estão vindo diretamente da camada mais inferior. Para que este processo de comunicação "trans-camada" funcione, a camada onde o objeto é utilizado e a camada onde os dados são lidos têm que existir no mesmo nodo ( no caso, na mesma máquina virtual). Caso contrário teremos que incluir logicas de [.pattern]*Proxy* remoto e com isso colocando barreiras na comunicação entre as camadas. A via de leitura não é mais livre e, portanto, não mais é rápida. O padrão FastLane fornece uma otimização de eficiência se utlizando do conceito de encapsulamento. Com a introdução da classe Stream no Java 8, a implementação de Fastlane ficou muito mais fácil. Basta usar Stream. Contudo, ha alguns riscos e é recomendável ter uma outra interface para indicar o uso de Fastlane Reader. Os problemas de Stream se relacionam com a sua capacidade de usar operações que acumular os dados em memórica, como `sort` ou `distinct`. Estes métodos destroem o objetivo principal que é só ter um item, por vez, em memória. Outro problema é a possibilidade de usar operações paralelas, que no caso nunca serão úteis pois apenas um item, por vez, é lido. O uso paralelo implica que mais do que um item estaria na memória, um por _thread_ de processamento. == Exemplos em APIs Na API padrão temos o exemplo do uso do padrão Fastlane Reader o próprio `ResultSet` . Este objeto é implementado pelo _driver_ JDBC e se comunica diretamente com o banco de dados, normalmente através de algum protocolo. Ao executar a pesquisa o _driver_ é livre para não criar todos os objetos de todas as linhas, podendo usar um mecanismo de Fastlane para minimizar o uso de memória. Com a resalvas mencionadas antes, a classe `Stream` também pode ser interpretada como uma implementação do padrão Fastlane Reader. == Padrões associados FastLane Reader começa como uma fusão entre o padrão [.pattern]*Iterator* - que permite apenas uma iteração - e o padrão [.pattern]*Proxy*, já que o objeto comunica com objetos em outras camadas formando um túnel entre a camada de dados e camada que utiliza os dados, mesmo que eles sejam transformados no meio do caminho. Outro padrão relacionado ao FastLane Reader é o [.pattern]*Flyweight*. O objeto criado pelo FastLane Reader pode ser uma versão simplificada do objeto real. Desta forma, não só diminuímos o número de objetos em memória mas para cada um deles, diminuímos o número de atributos preenchidos. No caso de Fastlane Reader ter o contrato de uma coleção ao invés de um iterador, podemos entender o próprio Fastlane Reader como uma especialização de *Flyweight* já que ao não carregar a coleção toda mantém o objeto de coleção leve ( como é o caso de `ResultSet` ). == Bibliografia [1] [.book=0131422464]*Deepak Alur, John Crupi, Dan Malks* _https://www.codedesign.world/library/java/core-j2ee-patterns.html[Core J2EE Patterns: Best Practices and Design Strategies]_: Prentice Hall Professional (2003) [2] [.book=0321356683]*Joshua Bloch* _https://www.codedesign.world/library/design/effectivejava.html[Effective Java, 2nd Edition]_: Fonenix inc (2008)