Olá, leitores, estou de volta! Peço desculpas pela ausência.
O artigo de hoje finalmente retoma a série de artigos sobre Design Patterns. Em continuidade, discutiremos sobre um padrão de projeto que é pouco conhecido na teoria, mas bastante aplicado na prática: o Iterator. Talvez você mesmo já tenha codificado este padrão sem ter ciência. Acompanhe!
Â
Em programação, é muito comum trabalhar com listas, coleções, mapas, ou qualquer outra estrutura que seja “iterável”. Quando percorremos os registros de um DataSet, por exemplo, estamos iterando uma tabela, partindo do primeiro registro e movendo para os próximos até chegar ao último. Os métodos First
e Next
nos permite a navegação no DataSet para que possamos trabalhar com os dados.
A ideia por trás do Iterator segue este mesmo conceito. Podemos “transformar” uma classe em uma estrutura que permita a iteração dos dados processados por ela. Mas este não é o objetivo principal do padrão. A proposta do Iterator é disponibilizar uma forma de percorrer uma coleção (ou lista) sem a necessidade de conhecer a representação dos dados. A origem dos dados é conhecida, mas o modo como eles são lidos e processados em uma lista é encapsulado. Para o cliente do Iterator, os métodos de acesso aos itens serão sempre os mesmos, independente do formato dos dados. Tecnicamente, imagine, por exemplo, que fosse possÃvel navegar entre os itens de um TObjectList
com os métodos First
, Next
, Prior
e Last
, tal como fazemos com um DataSet. Este tipo de “padrão de navegação” é o que aspiramos com o Iterator.
Uma das maiores vantagens deste padrão de projeto – além da padronização no mecanismo de navegação – é a imparcialidade dos tipos de dados carregados. Em um mundo tão versátil, como este da programação, é altamente recomendável modelar sistemas que possam trabalhar com diferentes tipos de dados de modo uniforme.
Â
Teremos um exemplo prático?
Claro que sim! Para este artigo, utilizaremos basicamente o mesmo cenário de negócio do artigo sobre o Chain of Responsibility. Apenas para recordação, neste cenário mencionado, carregamos e enviamos um arquivo em uma “corrente de classes” até que uma delas consiga processá-lo, inserindo o conteúdo em um DataSet. Já com o Iterator, o comportamento será ligeiramente diferente. As classes responsáveis pela leitura dos arquivos receberão métodos especiais para navegação dos dados, definidos em uma Interface. Ao final da codificação, observaremos que, embora os dados sejam oriundos de arquivos de diferentes formatos, poderemos navegar no conteúdo de maneira homogênea.
Para demonstrar essa ação, a aplicação fará a leitura de dados de clientes que estão armazenados em arquivos CSV e XML, simulando ambientes em que é necessário importar dados para a aplicação, mas cada usuário trabalha com um formato diferente. Eu prezo por essa abordagem por refletir a realidade de alguns projetos.
No contexto do Iterator, quatro elementos devem ser criados. Os dois primeiros são Interfaces: Iterator e Aggregate. A primeira define o contrato dos métodos de navegação, enquanto a segunda define um método para criação do Iterator. Os dois últimos elementos são implementações dessas Interfaces: a classe Concrete Iterator, que define a codificação dos métodos de navegação; e as classes Concrete Aggregate, responsáveis pela criação do Iterator, informando a lista que será manipulada.
Â
Quanta coisa!
Podemos resumir o parágrafo anterior em uma única frase: o cliente utiliza um Aggregate para obter a instância do um Iterator. Este, por sua vez, possui os métodos para navegação em uma lista.
Em primeiro lugar, considere a seguinte classe de modelagem:
type
TCliente = class
private
FCodigo: integer;
FNome: string;
FEndereco: string;
FPais: string;
FEmail: string;
public
property Codigo: integer read FCodigo write FCodigo;
property Nome: string read FNome write FNome;
property Endereco: string read FEndereco write FEndereco;
property Pais: string read FPais write FPais;
property Email: string read FEmail write FEmail;
end;
A ideia é trabalhar com uma lista preenchida com objetos do modelo acima.
Começaremos, então, pelo Iterator, definindo alguns métodos para navegação:
type
IIterator = interface
// Move para o primeiro objeto da lista
procedure PrimeiroObjeto;
// Move para o próximo objeto da lista
procedure ProximoObjeto;
// Retorna o objeto atual da lista
function ObjetoAtual: TObject;
// Verifica se está no fim da lista
function FimLista: boolean;
end;
Â
Em seguida, escreveremos também a Interface Aggregate, que possui apenas dois métodos: um para obter uma instância do Iterator e outro para obter a referência da lista de objetos:
type
IAggregate = interface
// Retorna uma referência da lista de objetos
function GetLista: TObjectList;
// Retorna uma instância do Iterator
function GetIterator: IIterator;
end;
Â
O próximo passo é definir a implementação concreta das Interfaces. O Concrete Iterator receberá a codificação a seguir. Observe que, para manipular a lista, faz-se necessária a utilização de uma variável de controle que, neste caso, será FIndice
. Além disso, precisamos da referência de um Aggregate para acessar a lista de objetos.
uses
Contnrs;
type
TConcreteIterator = class(TInterfacedObject, IIterator)
private
FAggregate: IAggregate;
FIndice: integer;
public
constructor Create(Aggregate: IAggregate);
procedure PrimeiroObjeto;
procedure ProximoObjeto;
function ObjetoAtual: TObject;
function FimLista: boolean;
end;
implementation
{ TConcreteIterator }
constructor TConcreteIterator.Create(Aggregate: IAggregate);
begin
FAggregate := Aggregate;
end;
function TConcreteIterator.FimLista: boolean;
begin
// Verifica se está no fim da lista
// ao comparar o Ãndice atual com a quantidade de objetos existentes
result := FIndice = Pred(FAggregate.GetLista.Count);
end;
function TConcreteIterator.ObjetoAtual: TObject;
begin
// Retorna o objeto que está no Ãndice atual
result := FAggregate.GetLista.Items[FIndice];
end;
procedure TConcreteIterator.PrimeiroObjeto;
begin
// "Reseta" o Ãndice, atribuindo o valor zero
FIndice := 0;
end;
procedure TConcreteIterator.ProximoObjeto;
begin
// Incrementa o Ãndice para avançar uma posição na lista
Inc(FIndice);
end;
Â
Bem simples, não é?
A etapa mais “complicada” deste contexto é a implementação das classes Concrete Aggregate, pois envolve a leitura dos arquivos, logo, haverá um Concrete Aggregate para cada formato. No nosso cenário, como há apenas dois tipos de arquivo (CSV e XML), definiremos, então, duas classes Concrete Aggregate. Cada uma receberá o caminho do arquivo no construtor para que possamos carregá-lo e popular a lista de objetos. Vale destacar também que precisamos criar e destruir a lista de objetos no construtor e destrutor, respectivamente.
O primeiro Concrete Aggregate refere-se ao formato CSV:
type
TConcreteAggregateCSV = class(TInterfacedObject, IAggregate)
private
FLista: TObjectList;
procedure PreencherLista(const CaminhoArquivo: string);
public
constructor Create(const CaminhoArquivo: string);
destructor Destroy; override;
function GetLista: TObjectList;
function GetIterator: IIterator;
end;
implementation
{ TConcreteAggregateCSV }
constructor TConcreteAggregateCSV.Create(const CaminhoArquivo: string);
begin
// Cria a lista de objetos
FLista := TObjectList.Create;
// Chama um método para carregar o arquivo e popular a lista de objetos
PreencherLista(CaminhoArquivo);
end;
destructor TConcreteAggregateCSV.Destroy;
begin
// Libera a lista de objetos da memória
FLista.Free;
inherited;
end;
function TConcreteAggregateCSV.GetIterator: IIterator;
begin
// Cria o Iterator, informando o próprio Aggregate como parâmetro
result := TConcreteIterator.Create(Self);
end;
function TConcreteAggregateCSV.GetLista: TObjectList;
begin
// Retorna uma referência da lista
result := FLista;
end;
procedure TConcreteAggregateCSV.PreencherLista(const CaminhoArquivo: string);
var
ArquivoCSV: TextFile;
StringListValores: TStringList;
Linha: string;
Cliente: TCliente;
begin
// Carrega o arquivo CSV
AssignFile(ArquivoCSV, CaminhoArquivo);
Reset(ArquivoCSV);
StringListValores := TStringList.Create;
try
// Percorre as linhas do arquivo
while not Eof(ArquivoCSV) do
begin
// Faz a leitura da linha do arquivo
ReadLn(ArquivoCSV, Linha);
// Atribui o conteúdo da linha na propriedade CommaText da StringList
// para extrair cada valor em diferentes posições
StringListValores.StrictDelimiter := True;
StringListValores.CommaText := Linha;
// Cria e preenche o objeto com os dados da linha do arquivo
Cliente := TCliente.Create;
Cliente.Codigo := StrToIntDef(StringListValores[0], 0);
Cliente.Nome := StringListValores[1];
Cliente.Endereco := StringListValores[2];
Cliente.Pais := StringListValores[3];
Cliente.Email := StringListValores[4];
// Adiciona o objeto na lista
FLista.Add(Cliente);
end;
finally
StringListValores.Free;
CloseFile(ArquivoCSV);
end;
end;
Â
O segundo Concrete Aggregate é responsável pelo processamento de arquivos XML:
type
TConcreteAggregateXML = class(TInterfacedObject, IAggregate)
private
FLista: TObjectList;
procedure PreencherLista(const CaminhoArquivo: string);
public
constructor Create(const CaminhoArquivo: string);
destructor Destroy; override;
function GetLista: TObjectList;
function GetIterator: IIterator;
end;
implementation
{ TConcreteAggregate }
constructor TConcreteAggregateXML.Create(const CaminhoArquivo: string);
begin
// Cria a lista de objetos
FLista := TObjectList.Create;
// Chama um método para carregar o arquivo e popular a lista de objetos
PreencherLista(CaminhoArquivo);
end;
destructor TConcreteAggregateXML.Destroy;
begin
// Cria o Iterator, informando o próprio Aggregate como parâmetro
FLista.Free;
inherited;
end;
function TConcreteAggregateXML.GetIterator: IIterator;
begin
result := TConcreteIterator.Create(Self);
end;
function TConcreteAggregateXML.GetLista: TObjectList;
begin
// Retorna uma referência da lista
result := FLista;
end;
procedure TConcreteAggregateXML.PreencherLista(const CaminhoArquivo: string);
var
XMLDocument: IXMLDocument;
NodeImportacao: IXMLNode;
NodeDados: IXMLNode;
Contador: Integer;
Cliente: TCliente;
begin
// Carrega o arquivo XML
XMLDocument := LoadXMLDocument(CaminhoArquivo);
XMLDocument.Active := True;
// Seleciona o nó principal do XML (chamado "dataset")
NodeImportacao := XMLDocument.DocumentElement;
for Contador := 0 to Pred(NodeImportacao.ChildNodes.Count) do
begin
// Acessa o nó filho
NodeDados := NodeImportacao.ChildNodes[Contador];
// Cria e preenche o objeto com os dados do nó
Cliente := TCliente.Create;
Cliente.Codigo := StrToInt(NodeDados.ChildNodes['codigo'].Text);
Cliente.Nome := NodeDados.ChildNodes['nome'].Text;
Cliente.Endereco := NodeDados.ChildNodes['endereco'].Text;
Cliente.Pais := NodeDados.ChildNodes['pais'].Text;
Cliente.Email := NodeDados.ChildNodes['email'].Text;
// Adiciona o objeto na lista
FLista.Add(Cliente);
end;
end;
Â
Agora, vamos conferir: Iterator? OK. Aggregate? OK. Concrete Iterator? OK. Concrete Aggregate? OK. Bom, tudo pronto para colocar o Iterator em ação! 🙂
Como exemplo, o método abaixo navega na lista de objetos para adicionar o nome do cliente em uma TListBox
:
procedure TForm1.CarregarCSV;
var
Aggregate: IAggregate;
Iterator: IIterator;
begin
// Cria o Concrete Aggregate, informado o caminho do arquivo CSV
Aggregate := TConcreteAggregateCSV.Create('C:\Dados\Clientes.csv');
// Obtém a instância do Iterator
Iterator := Aggregate.GetIterator;
// Utiliza os métodos de navegação
Iterator.PrimeiroObjeto;
while not Iterator.FimLista do
begin
Iterator.ProximoObjeto;
ListBoxClientes.Items.Add((Iterator.ObjetoAtual as TCliente).Nome);
end;
end;
A navegação na lista fica bem mais compreensÃvel, não acham? Mas a legibilidade não é a única vantagem. Lembram-se que também devemos ler arquivos XML? Pois bem, a codificação do método é praticamente a mesma, com exceção apenas do caminho do arquivo e da criação do Concrete Aggregate. Ganhamos essa facilidade pelo fato de que o Iterator é o mesmo!
begin
// Cria o Concrete Aggregate, informado o caminho do arquivo XML
Aggregate := TConcreteAggregateXML.Create('C:\Dados\Clientes.xml');
...
Caso surja um novo formato, como JSON, basta apenas criar um novo Concrete Aggregate e associá-lo ao Iterator. Fácil, fácil.
Â
Leitores, a codificação deste artigo o fizeram lembrar de algo? Talvez, sim. Em abril, publiquei um artigo sobre 3 formas de percorrer uma lista. A última delas consiste no método GetEnumerator
, que provê a possibilidade de iterar uma determinada lista com o método MoveNext
.
Então o GetEnumerator retorna um Iterator?
Exato! Quando possÃvel, acesse o artigo novamente e veja que o método MoveNext
é utilizado tanto para uma lista de strings quanto para uma lista de objetos. Para o cliente deste método (ou seja, nós, desenvolvedores), a representação dos dados é algo que não devemos nos preocupar. 😉
O projeto de exemplo deste artigo está no endereço do GitHub abaixo. Neste projeto, há algumas modificações, como a busca do objeto (pelo Iterator) e exibição dos dados em componentes TEdit
. Para fins de teste, disponibilizei um arquivo CSV e um aquivo XML no subdiretório “Dados” do projeto.
https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/Iterator-AndreCelestino
Até a próxima, pessoal!
Olá, parabéns. No entanto estou com um problema de vazamento de memória que talvez você possa me ajudar.
Ao fechar o programa aponta vazamento de memória no TJSONPair. Buscando uma solução na net encontrei a propriedade “Owned” pois passando para “False” o JsPair ela deixa de criar referencias ao JsObj onde é possÃvel destruir usando “Free” sem dar outros problemas, já que sem essa propriedade não dá pra destruir JsObj, porém ainda assim continua o vazamento de memória. Será que você tem alguma ideia de como resolver esse problema?
Olá, João!
Já estamos nos falando pela página do blog no Facebook.
Abraço!