Olá, leitores, estou de volta!
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!
Introdução
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
, Prior
, Next
e Last
, tal como fazemos com um DataSet. Este tipo de “padrão de navegação” é o que alcançamos 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.
Exemplo de codificação do Iterator
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.
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.
Classe de modelagem
Em primeiro lugar, considere a seguinte classe de modelagem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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.
Interface Iterator
Começaremos, então, pelo Iterator, definindo alguns métodos para navegação:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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; |
Interface Aggregate
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:
1 2 3 4 5 6 7 8 9 |
type IAggregate = interface // Retorna uma referência da lista de objetos function GetLista: TObjectList; // Retorna uma instância do Iterator function GetIterator: IIterator; end; |
Classe Concrete Iterator
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
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 é?
Classe Concrete Aggregate
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
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; |
Em ação!
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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!
1 2 3 4 |
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.
Conclusão
Leitores, a codificação deste artigo os 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
.
Podemos afirmar, portanto, que GetEnumerator
retorna um Iterator. 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.
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!