[Delphi] Design Patterns – Iterator

[Delphi] Design Patterns - Iterator

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!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

Deixe uma resposta

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

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.