Como o framework tiOPF para delphi / lazarus funciona. Modelo de visitante

Do tradutor


Há duas razões pelas quais eu me comprometi a traduzir vários materiais sobre a estrutura desenvolvida há vinte anos para o ambiente de programação não muito popular:

1. Alguns anos atrás, tendo aprendido muitas das delícias de trabalhar com o Entity Framework como ORM para a plataforma .Net, procurei em vão análogos para o ambiente Lazarus e, em geral, para freepascal.
Surpreendentemente, faltam boas ORMs para ela. Tudo o que foi encontrado foi um projeto de código aberto chamado tiOPF , desenvolvido no final dos anos 90 para o delphi, mais tarde portado para o freepascal. No entanto, essa estrutura é fundamentalmente diferente da aparência usual de ORMs grandes e grossas.

Não há métodos visuais para projetar objetos (no Entity - modelo primeiro) e mapear objetos para campos de tabelas em um banco de dados relacional (no Entity - banco de dados primeiro) no tiOPF. O próprio desenvolvedor posiciona esse fato como uma das deficiências do projeto, no entanto, como mérito, ele oferece uma orientação completa especificamente sobre o modelo de negócios do objeto, vale apenas um código rígido ...

Foi no nível do código proposto que eu tive problemas. Naquela época, eu não era muito versado nesses paradigmas e métodos que o desenvolvedor de framework usou na íntegra e mencionou na documentação várias vezes por parágrafo (padrões de design do visitante, vinculador, observador, vários níveis de abstração para independência do DBMS, etc. .). Meu grande projeto de trabalho com o banco de dados da época estava completamente focado nos componentes visuais do Lazarus e na maneira de trabalhar com os bancos de dados oferecidos pelo ambiente visual, como resultado, toneladas do mesmo código: três tabelas no próprio banco de dados com quase a mesma estrutura e dados homogêneos, três formulários idênticos para visualização, três formulários idênticos para edição, três formulários idênticos para relatórios e tudo mais, desde o início do cabeçalho "Como não projetar software".

Depois de ler bastante literatura sobre os princípios do design correto de bancos de dados e sistemas de informação, incluindo o estudo de modelos, além de me familiarizar com o Entity Framework, decidi fazer uma refatoração completa do banco de dados e do meu aplicativo. E se eu lidei completamente com a primeira tarefa, para a implementação da segunda havia duas estradas indo em direções diferentes: ou estudasse .net, C # e o Entity Framework completamente, ou encontrasse um ORM adequado para o familiar sistema Lazarus. Havia também uma terceira, primeira e discreta trilha de bicicleta - escrever ORM para atender às suas necessidades, mas esse não é o ponto agora.

O código fonte da estrutura não é muito comentado, mas os desenvolvedores, no entanto, prepararam (aparentemente no período inicial de desenvolvimento) uma certa quantidade de documentação. Tudo isso, é claro, é de língua inglesa e a experiência mostra que, apesar da abundância de códigos, diagramas e frases de programação de modelos, muitos programadores de língua russa ainda são pouco orientados na documentação em inglês. Nem sempre e nem todo mundo deseja treinar a capacidade de entender o texto técnico em inglês sem a necessidade de a mente traduzi-lo para o russo.

Além disso, a revisão repetida do texto para tradução permite que você veja o que eu perdi quando conheci a documentação, não a compreendi completamente ou incorretamente. Ou seja, esta é para si uma oportunidade de aprender melhor a estrutura em estudo.

2. Na documentação, o autor intencionalmente ou não pula algumas partes do código, provavelmente óbvias em sua opinião. Devido à limitação de sua escrita, a documentação usa mecanismos e objetos desatualizados como exemplos, excluídos ou não mais usados ​​em novas versões da estrutura (mas eu não disse que ela mesma continua se desenvolvendo?). Além disso, quando repeti os exemplos desenvolvidos, encontrei alguns erros que deveriam ser corrigidos. Portanto, em alguns lugares me permiti não apenas traduzir o texto, mas também complementá-lo ou revisá-lo para que ele permaneça relevante, e os exemplos estavam funcionando.

Quero começar a tradução de materiais de um artigo de Peter Henrikson sobre a primeira “baleia” na qual toda a estrutura se apoia - o modelo Visitor. Texto original publicado aqui .

Modelo de visitante e tiOPF


O objetivo deste artigo é apresentar o modelo Visitor, cujo uso é um dos principais conceitos da estrutura tiOPF (TechInsite Object Persistence Framework). Vamos considerar o problema em detalhes, depois de analisar soluções alternativas antes de usar o Visitante. No processo de desenvolvimento de nosso próprio conceito de visitante, enfrentaremos outro desafio: a necessidade de percorrer todos os objetos da coleção. Esta questão também será estudada.

A tarefa principal é apresentar uma maneira generalizada de executar um conjunto de métodos relacionados em alguns objetos da coleção. Os métodos executados podem variar dependendo do estado interno dos objetos. Não podemos executar métodos, mas podemos executar vários métodos nos mesmos objetos.

O nível necessário de treinamento


O leitor deve estar familiarizado com o objeto pascal e dominar os princípios básicos da programação orientada a objetos.

Exemplo de tarefa de negócios neste artigo


Como exemplo, desenvolveremos um catálogo de endereços que permite criar registros de pessoas e suas informações de contato. Com o aumento das possíveis formas de comunicação entre as pessoas, o aplicativo deve permitir a flexibilidade de adicionar esses métodos sem processamento significativo de código (lembro que depois de terminar de processar o código para adicionar um número de telefone, eu imediatamente precisava processá-lo novamente para adicionar email). Precisamos fornecer duas categorias de endereços: real, como endereço residencial, postal, comercial e eletrônico: telefone fixo, fax, celular, email, site.

No nível da apresentação, nosso aplicativo deve se parecer com o Explorer / Outlook, ou seja, deve-se usar componentes padrão, como o TreeView e o ListView. O aplicativo deve funcionar rapidamente e não dar a impressão de software cliente-servidor volumoso.

Um aplicativo pode ser algo como isto:



No menu de contexto da árvore, você pode optar por adicionar / remover o contato de uma pessoa ou empresa e clicar com o botão direito do mouse na lista de dados de contato para acessar sua caixa de diálogo de edição, excluir ou adicionar dados.

Os dados podem ser salvos de várias formas e, no futuro, consideraremos como usar esse modelo para implementar esse recurso.

Antes de começar


Começaremos com uma coleção simples de objetos - uma lista de pessoas que, por sua vez, têm duas propriedades - nome (Nome) e endereço (EmailAdrs). Para começar, a lista será preenchida com dados no construtor e, posteriormente, será carregada de um arquivo ou banco de dados. Obviamente, este é um exemplo muito simplificado, mas é suficiente para implementar completamente o modelo do Visitante.

Crie um novo aplicativo e adicione duas classes da seção de interface do módulo principal: TPersonList (herdado de TObjectList e requer um plug-in no módulo contnrs) e TPerson (herdado de TObject):

TPersonList = class(TObjectList)  public    constructor Create;  end;  TPerson = class(TObject)  private    FEMailAdrs: string;    FName: string;  public    property Name: string read FName write FName;    property EMailAdrs: string read FEMailAdrs write FEMailAdrs;  end; 

No construtor TPersonList, criamos três objetos TPerson e adicionamos à lista:

 constructor TPersonList.Create; var lData: TPerson; begin inherited; lData := TPerson.Create; lData.Name := 'Malcolm Groves'; lData.EMailAdrs := 'malcolm@dontspamme.com';  // (ADUG Vice President) Add(lData); lData := TPerson.Create; lData.Name := 'Don MacRae';  // (ADUG President) lData.EMailAdrs := 'don@dontspamme.com'; Add(lData); lData := TPerson.Create; lData.Name := 'Peter Hinrichsen';  // (Yours truly) lData.EMailAdrs := 'peter_hinrichsen@dontspamme.com'; Add(lData); end; 

Primeiro, examinaremos a lista e realizaremos duas operações em cada elemento da lista. As operações são semelhantes, mas não são as mesmas: uma simples chamada ShowMessage para exibir o conteúdo das propriedades Name e EmailAdrs dos objetos TPerson. Adicione dois botões ao formulário e nomeie-os da seguinte forma:



No escopo preferido do seu formulário, você também deve adicionar uma propriedade (ou apenas um campo) FPersonList do tipo TPersonList (se o tipo for declarado abaixo do formulário, altere a ordem ou faça uma declaração de tipo preliminar) e chame o construtor no manipulador de eventos onCreate:

 FPersonList := TPersonList.Create; 

Para liberar memória corretamente no manipulador de eventos onClose do formulário, esse objeto deve ser destruído:

 FPersonList.Free. 

Etapa 1. Iteração de código rígido


Para mostrar nomes de objetos TPerson, adicione o seguinte código ao manipulador de eventos onClick do primeiro botão:

 procedure TForm1.Button1Click(Sender: TObject); var i: integer; begin for i := 0 to FPersonList.Count - 1 do   ShowMessage(TPerson(FPersonList.Items[i]).Name); end; 

Para o segundo botão, o código do manipulador será o seguinte:

 procedure TForm1.Button2Click(Sender: TObject); var i: integer; begin for i := 0 to FPersonList.Count - 1 do   ShowMessage(TPerson(FPersonList.Items[i]).EMailAdrs); end; 

Aqui estão os cardumes óbvios desse código:

  • dois métodos que fazem quase a mesma coisa. Toda a diferença está apenas no nome da propriedade do objeto que eles mostram;
  • a iteração será tediosa, especialmente quando você for forçado a escrever um loop semelhante em centenas de lugares no código;
  • um elenco difícil para TPerson é repleto de situações excepcionais. E se houver uma instância de TAnimal na lista sem uma propriedade de endereço? Não há mecanismo para interromper o erro e se defender contra ele neste código.

Vamos descobrir como melhorar o código introduzindo uma abstração: passamos o código do iterador para a classe pai.

Etapa 2. Abstraindo o iterador


Portanto, queremos mover a lógica do iterador para a classe base. O iterador da lista em si é muito simples:

 for i := 0 to FList.Count - 1 do // -    … 

Parece que estamos planejando usar um modelo Iterator . No livro sobre padrões de design do Gang-of-Four , é sabido que o Iterator pode ser externo e interno. Ao usar um iterador externo, o cliente controla explicitamente a travessia chamando o método Next (por exemplo, a enumeração de elementos TCollection é controlada pelos métodos First, Next, Last). Usaremos o iterador interno aqui, pois é mais fácil implementar a travessia de árvore com sua ajuda, que é nosso objetivo. Adicionaremos o método Iterate à nossa classe list e passaremos um método de retorno de chamada para ele, que deve ser executado em cada elemento da lista. O retorno de chamada no objeto pascal é declarado como um tipo de procedimento; teremos, por exemplo, TDoSomethingToAPerson.

Portanto, declaramos um tipo de procedimento TDoSomethingToAPerson, que aceita um parâmetro do tipo TPerson. O tipo de procedimento permite usar o método como parâmetro de outro método, ou seja, implementar retorno de chamada. Dessa maneira, criaremos dois métodos, um dos quais mostrará a propriedade Name do objeto e o outro - a propriedade EmailAdrs, e eles mesmos serão passados ​​como parâmetro ao iterador geral. Por fim, a seção de declaração de tipo deve ficar assim:

 { TPerson } TPerson = class(TObject) private   FEMailAdrs: string;   FName: string; public   property Name: string read FName write FName;   property EMailAdrs: string read FEMailAdrs write FEMailAdrs; end; TDoSomethingToAPerson = procedure(const pData: TPerson) of object; { TPersonList } TPersonList = class(TObjectList) public   constructor Create;   procedure   DoSomething(pMethod: TDoSomethingToAPerson); end;   DoSomething: procedure TPersonList.DoSomething(pMethod: TDoSomethingToAPerson); var i: integer; begin for i := 0 to Count - 1 do   pMethod(TPerson(Items[i])); end; 

Agora, para executar as ações necessárias nos itens da lista, precisamos fazer duas coisas. Primeiro, defina as operações necessárias usando métodos que possuem a assinatura especificada por TDoSomethingToAPerson e, em segundo lugar, escreva chamadas DoSomething com os ponteiros para esses métodos passados ​​como parâmetro. Na seção de descrição do formulário, adicione duas declarações:

 private   FPersonList: TPersonList;   procedure DoShowName(const pData: TPerson);   procedure DoShowEmail(const pData: TPerson); 

Na implementação desses métodos, indicamos:

 procedure TForm1.DoShowName(const pData: TPerson); begin ShowMessage(pData.Name); end; procedure TForm1.DoShowEmail(const pData: TPerson); begin ShowMessage(pData.EMailAdrs); end; 

O código para manipuladores de botão é alterado da seguinte maneira:

 procedure TForm1.Button1Click(Sender: TObject); begin FPersonList.DoSomething(@DoShowName); end; procedure TForm1.Button2Click(Sender: TObject); begin FPersonList.DoSomething(@DoShowEmail); end; 

Já está melhor. Agora temos três níveis de abstração em nosso código. Um iterador geral é um método de uma classe que implementa uma coleção de objetos. A lógica de negócios (até agora apenas a saída de mensagens sem fim através do ShowMessage) é colocada separadamente. No nível da apresentação (interface gráfica), a lógica de negócios é chamada em uma linha.

É fácil imaginar como uma chamada para ShowMessage pode ser substituída por código que salva nossos dados do TPerson em um banco de dados relacional usando a consulta SQL do objeto TQuery. Por exemplo, assim:

 procedure TForm1.SavePerson(const pData: TPerson); var lQuery: TQuery; begin lQuery := TQuery.Create(nil); try   lQuery.SQL.Text := 'insert into people values (:Name, :EMailAdrs)';   lQuery.ParamByName('Name').AsString := pData.Name;   lQuery.ParamByName('EMailAdrs').AsString := pData.EMailAdrs;   lQuery.Datababase := gAppDatabase;   lQuery.ExecSQL; finally   lQuery.Free; end; end; 

A propósito, isso introduz um novo problema de manutenção de uma conexão com o banco de dados. Em nossa solicitação, a conexão com o banco de dados é realizada através de algum objeto global gAppDatabase. Mas onde será localizado e como trabalhar? Além disso, somos atormentados em cada etapa do iterador para criar objetos TQuery, configurar a conexão, executar a consulta e não se esqueça de liberar a memória. Seria melhor agrupar esse código em uma classe que encapsule a lógica da criação e execução de consultas SQL, além de configurar e manter uma conexão com o banco de dados.

Etapa 3. Passando um objeto em vez de passar um ponteiro para um retorno de chamada


Passar o objeto para o método iterador da classe base resolverá o problema da manutenção do estado. Criaremos uma classe Visitor abstrata TPersonVisitor com um único método Execute e passaremos o objeto para esse método como parâmetro. A interface abstrata do visitante é apresentada abaixo:

   TPersonVisitor = class(TObject) public   procedure Execute(pPerson: TPerson); virtual; abstract; end; 

Em seguida, adicione o método Iterate à nossa classe TPersonList:

 TPersonList = class(TObjectList) public   constructor Create;   procedure Iterate(pVisitor: TPersonVisitor); end; 

A implementação deste método será a seguinte:

 procedure TPersonList.Iterate(pVisitor: TPersonVisitor); var i: integer; begin for i := 0 to Count - 1 do   pVisitor.Execute(TPerson(Items[i])); end; 

Um objeto do Visitor implementado da classe TPersonVisitor é passado para o método Iterate e, ao enumerar itens da lista para cada um deles, o Visitor especificado (seu método de execução) é chamado com a instância TPerson como parâmetro.

Vamos criar duas implementações do Visitor - TShowNameVisitor e TShowEmailVistor, que realizarão o trabalho necessário. Veja como reabastecer a seção de interfaces do módulo:

 { TShowNameVisitor } TShowNameVisitor = class(TPersonVisitor) public   procedure Execute(pPerson: TPerson); override; end; { TShowEmailVisitor } TShowEmailVisitor = class(TPersonVisitor) public   procedure Execute(pPerson: TPerson); override; end; 

Por uma questão de simplicidade, a implementação dos métodos de execução neles ainda será uma única linha - ShowMessage (pPerson.Name) e ShowMessage (pPerson.EMailAdrs).

E altere o código para os manipuladores de cliques do botão:

 procedure TForm1.Button1Click(Sender: TObject); var lVis: TPersonVisitor; begin lVis := TShowNameVisitor.Create; try   FPersonList.Iterate(lVis); finally   lVis.Free; end; end; procedure TForm1.Button2Click(Sender: TObject); var lVis: TPersonVisitor; begin lVis := TShowEmailVisitor.Create; try   FPersonList.Iterate(lVis); finally   lVis.Free; end; end; 

Agora, tendo resolvido um problema, criamos outro para nós mesmos. A lógica do iterador é encapsulada em uma classe separada; as operações executadas durante a iteração são agrupadas em objetos, o que nos permite salvar algumas informações sobre o estado, mas o tamanho do código aumentou de uma linha (FPersonList.DoSomething (@DoShowName)) para nove linhas para cada manipulador de botão. Agora, isso nos ajudará - este é o Visitor Manager, que cuidará da criação e liberação de suas cópias.Potencialmente, podemos fornecer várias operações com objetos a serem executados durante a iteração; para isso, o Visitor Manager armazenará sua lista e passará a cada etapa, você . Olnyaya única operações seleccionadas seguinte irá demonstrar claramente os benefícios desta abordagem, vamos usar os visitantes para salvar os dados em um banco de dados relacional como um dado simples poupança operação pode ser realizada por três operadores diferentes SQL: criar, apagar e o UPDATE.

Etapa 4. Encapsulamento adicional do visitante


Antes de prosseguir, precisamos encapsular a lógica do trabalho do Visitante, separando-a da lógica comercial do aplicativo para que ele não retorne a ele. Serão necessários três etapas para fazer isso: criar as classes base TVisited e TVisitor, depois as classes base para o objeto de negócios e a coleção de objetos de negócios e ajustar ligeiramente nossas classes específicas TPerson e TPersonList (ou TPeople) para que se tornem herdeiros da base criada. aulas. Em termos gerais, a estrutura das classes corresponderá a esse diagrama:



O objeto TVisitor implementa dois métodos: a função AcceptVisitor e o procedimento Execute, nos quais o objeto do tipo TVisited é passado. O objeto TVisited, por sua vez, implementa o método Iterate com um parâmetro do tipo TVisitor. Ou seja, TVisited.Iterate deve chamar o método Execute no objeto TVisitor transferido, enviando um link para sua própria instância como parâmetro e, se a instância for uma coleção, o método Execute será chamado para cada elemento da coleção. A função AcceptVisitor é necessária, pois estamos desenvolvendo um sistema generalizado. Será possível passar para o Visitor, que opera apenas com os tipos TPerson, uma instância da classe TDog, por exemplo, e deve haver um mecanismo para evitar exceções e erros de acesso devido à incompatibilidade de tipos. A classe TVisited é descendente da classe TPersistent, já que um pouco mais tarde precisaremos implementar funções relacionadas ao uso do RTTI.

A parte da interface do módulo agora será assim:

 TVisited = class; { TVisitor } TVisitor = class(TObject) protected   function AcceptVisitor(pVisited: TVisited): boolean; virtual; abstract; public   procedure Execute(pVisited: TVisited); virtual; abstract; end; { TVisited } TVisited = class(TPersistent) public   procedure Iterate(pVisitor: TVisitor); virtual; end; 

Os métodos da classe abstrata TVisitor serão implementados pelos herdeiros, e a implementação geral do método Iterate para TVisited é fornecida abaixo:

 procedure TVisited.Iterate(pVisitor: TVisitor); begin pVisitor.Execute(self); end; 

Ao mesmo tempo, o método é declarado virtual para a possibilidade de sua substituição nos herdeiros.

Etapa 5. Crie um objeto de negócios e uma coleção compartilhados


Nossa estrutura precisa de mais duas classes base: para definir um objeto de negócios e uma coleção desses objetos. Chame-os de TtiObject e TtiObjectList. A interface do primeiro deles:

 TtiObject = class(TVisited) public   constructor Create; virtual; end; 

Posteriormente no processo de desenvolvimento, complicaremos essa classe, mas para a tarefa atual, apenas um construtor virtual com a possibilidade de substituí-la nos herdeiros é suficiente.

Planejamos gerar a classe TtiObjectList da TVisited para usar o comportamento em métodos que já foram implementados pelo ancestral (também existem outros motivos para essa herança que serão discutidos em seu lugar). Além disso, nada proíbe o uso de interfaces (interfaces) em vez de classes abstratas.

A parte da interface da classe TtiObjectList será a seguinte:

 TtiObjectList = class(TtiObject) private   FList: TObjectList; public   constructor Create; override;   destructor Destroy; override;   procedure Clear;   procedure Iterate(pVisitor: TVisitor); override;   procedure Add(pData: TObject); end; 

Como você pode ver, o próprio contêiner com os elementos do objeto está localizado na seção protegida e não estará disponível para os clientes desta classe. A parte mais importante da classe é a implementação do método Iterate substituído. Se na classe base o método simplesmente chamado pVisitor.Execute (self), aqui a implementação está conectada à enumeração da lista:

 procedure TtiObjectList.Iterate(pVisitor: TVisitor); var i: integer; begin inherited Iterate(pVisitor); for i := 0 to FList.Count - 1 do   (FList.Items[i] as TVisited).Iterate(pVisitor); end; 

A implementação de outros métodos de classe leva uma linha de código sem levar em consideração as expressões herdadas colocadas automaticamente:

 Create: FList := TObjectList.Create; Destroy: FList.Free; Clear: if Assigned(FList) then FList.Clear; Add: if Assigned(FList) then FList.Add(pData); 

Esta é uma parte importante de todo o sistema. Temos duas classes básicas de lógica de negócios: TtiObject e TtiObjectList. Ambos têm um método Iterate para o qual uma instância da classe TVisited é passada. O próprio iterador chama o método Execute da classe TVisitor e passa uma referência ao próprio objeto. Essa chamada é predefinida no comportamento da classe no nível superior de herança. Para uma classe de contêiner, cada objeto armazenado na lista também possui seu método Iterate, chamado com um parâmetro do tipo TVisitor, ou seja, é garantido que cada Visitante específico ignore todos os objetos armazenados na lista, bem como a própria lista como um objeto de contêiner.

Etapa 6. Criando um Gerenciador de Visitantes


Então, voltando ao problema que nós mesmos desenhamos no terceiro passo. Como não queremos criar e destruir cópias de visitantes sempre, o desenvolvimento do gerente será a solução. Ele deve executar duas tarefas principais: gerenciar a lista de Visitantes (que são registrados como tal na seção de inicialização de módulos individuais) e executá-los quando receberem o comando apropriado do cliente.
Para implementar o gerenciador, suplementaremos nosso módulo com três classes adicionais: TVisClassRef, TVisMapping e TtiVisitorManager.

 TVisClassRef = class of TVisitor; 

TVisClassRef é um tipo de referência e indica o nome de uma classe específica - um descendente do TVisitor. O significado de usar um tipo de referência é o seguinte: quando o método Execute base com uma assinatura é chamado

 procedure Execute(const pData: TVisited; const pVisClass: TVisClassRef), 

internamente, esse método pode usar uma expressão como lVisitor: = pVisClass.Create para criar uma instância de um Visitante específico, sem conhecer seu tipo inicialmente. Ou seja, qualquer classe - um descendente do TVisitor pode ser criado dinamicamente dentro do mesmo método Execute ao passar o nome de sua classe como parâmetro.

A segunda classe, TVisMapping, é uma estrutura de dados simples com duas propriedades: uma referência ao tipo TVisClassRef e uma propriedade de cadeia Command. É necessária uma classe para comparar as operações executadas pelo nome (um comando, por exemplo, "save") e a classe Visitor, que esses comandos executam. Adicione seu código ao projeto:

 TVisMapping = class(TObject) private   FCommand: string;   FVisitorClass: TVisClassRef; public   property VisitorClass: TVisClassRef read FVisitorClass write FVisitorClass;   property Command: string read FCommand write FCommand; end; 

E a última classe é TtiVisitorManager. Quando registramos o Visitor usando o Manager, uma instância da classe TVisMapping é criada, que é inserida na lista Manager.
Assim, no Gerenciador, é criada uma lista de Visitantes com um comando de sequência correspondente, após o recebimento do qual eles serão executados. A interface da classe é adicionada ao módulo:

 TtiVisitorManager = class(TObject) private   FList: TObjectList; public   constructor Create;   destructor Destroy; override;   procedure RegisterVisitor(const pCommand: string; pVisitorClass: TVisClassRef);   procedure Execute(const pCommand: string; pData: TVisited); end; 

Seus principais métodos são RegisterVisitor e Execute. O primeiro é normalmente chamado na seção de inicialização do módulo, que descreve a classe Visitor, e se parece com isso:

 initialization  gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowNameVisitor);  gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowEMailAdrsVisitor); 

O código do método em si será o seguinte:

 procedure TtiVisitorManager.RegisterVisitor(const pCommand: string; pVisitorClass: TVisClassRef); var lData: TVisMapping; begin lData := TVisMapping.Create; lData.Command := pCommand; lData.VisitorClass := pVisitorClass; FList.Add(lData); end; 

Não é difícil perceber que esse código é muito semelhante à implementação do modelo Factory do Pascal.

Outro método Execute importante aceita dois parâmetros: o comando pelo qual o Visitante ou seu grupo a ser identificado será identificado, bem como o objeto de dados cujo método Iterate será chamado com um link para a instância do Visitante desejado. O código completo para o método Execute é fornecido abaixo:

 procedure TtiVisitorManager.Execute(const pCommand: string; pData: TVisited); var i: integer; lVisitor: TVisitor; begin for i := 0 to FList.Count - 1 do   if SameText(pCommand, TVisMapping(FList.Items[i]).Command) then   begin     lVisitor := TVisMapping(FList.Items[i]).VisitorClass.Create;     try       pData.Iterate(lVisitor);     finally       lVisitor.Free;     end;   end; end; 

Portanto, para executar dois visitantes registrados anteriormente com uma equipe, precisamos de apenas uma linha de código:

 gTIOPFManager.VisitorManager.Execute('show', FPeople); 

Em seguida, suplementaremos nosso projeto para que você possa chamar comandos semelhantes:

 //      gTIOPFManager.VisitorManager.Execute('read', FPeople); //      gTIOPFManager.VisitorManager.Execute('save', FPeople). 

Etapa 7. Ajustando classes de lógica de negócios


Adicionar o ancestral das classes TtiObject e TtiObjectList para nossos objetos de negócios TPerson e TPeople nos permitirá encapsular a lógica do iterador na classe base e não tocá-la mais, além disso, torna-se possível transferir objetos com dados para o Visitor Manager.

A nova declaração de classe de contêiner terá a seguinte aparência:

 TPeople = class(TtiObjectList); 

De fato, a classe TPeople nem precisa implementar nada. Teoricamente, poderíamos ficar sem uma declaração TPeople e armazenar objetos em uma instância da classe TtiObjectList, mas como planejamos escrever Visitors processando apenas instâncias TPeople, precisamos dessa classe. Na função AcceptVisitor, as seguintes verificações serão realizadas:

 Result := pVisited is TPeople. 

Para a classe TPerson, adicionamos o ancestral TtiObject e movemos as duas propriedades existentes para o escopo publicado, pois no futuro precisaremos trabalhar com o RTTI com essas propriedades. É isso muito mais tarde que reduzirá significativamente o código envolvido no mapeamento de objetos e registros em um banco de dados relacional:

 TPerson = class(TtiObject) private   FEMailAdrs: string;   FName: string; published   property Name: string read FName write FName;   property EMailAdrs: string read FEMailAdrs write FEMailAdrs; end; 

Etapa 8. Crie uma visualização de protótipo


Observação . No artigo original, a GUI foi baseada nos componentes que o autor do tiOPF criou para a conveniência de trabalhar com sua estrutura no delphi. Estes eram análogos dos componentes do DB Aware, que eram controles padrão, como rótulos, campos de entrada, caixas de seleção, listas etc., mas estavam associados a determinadas propriedades dos objetos tiObject da mesma maneira que os componentes de exibição de dados estavam associados a campos nas tabelas do banco de dados. Com o tempo, o autor da estrutura marcou os pacotes com esses componentes visuais como obsoletos e indesejáveis ​​de usar.Em troca, ele sugere criar um link entre componentes visuais e propriedades de classe usando o padrão de design do Mediador. Este modelo é o segundo mais importante em toda a arquitetura da estrutura. A descrição do intermediário pelo autor é coberta por um artigo separado, comparável em volume a este manual, portanto, ofereço minha versão simplificada aqui como uma GUI.

Renomeie o botão 1 no formulário do projeto para "show command" e o botão 2 deixa-o sem manipulador por enquanto ou nomeie-o imediatamente "save command". Jogue um componente de memorando no formulário e coloque todos os elementos ao seu gosto.

Adicione uma classe Visitor que implementará o comando show:

Interface -

 TShowVisitor = class(TVisitor) protected   function AcceptVisitor(pVisited: TVisited): boolean; override; public   procedure Execute(pVisited: TVisited); override; end; 

E a implementação é -
 function TShowVisitor.AcceptVisitor(pVisited: TVisited): boolean; begin Result := (pVisited is TPerson); end; procedure TShowVisitor.Execute(pVisited: TVisited); begin if not AcceptVisitor(pVisited) then   exit; Form1.Memo1.Lines.Add(TPerson(pVisited).Name + ': ' + TPerson(pVisited).EMailAdrs); end; 

AcceptVisitor verifica se o objeto que está sendo transferido é uma instância de TPerson, porque o Visitante deve executar apenas o comando com esses objetos. Se o tipo corresponder, o comando será executado e uma linha com propriedades do objeto será adicionada ao campo de texto.

As ações de suporte para a integridade do código serão as seguintes. Adicione duas propriedades à descrição do próprio formulário na seção particular: FPeople do tipo TPeople e VM do tipo TtiVisitorManager. No manipulador de eventos de criação de formulários, precisamos iniciar essas propriedades, além de registrar o Visitor com o comando "show":

 FPeople := TPeople.Create; FillPeople; VM := TtiVisitorManager.Create; VM.RegisterVisitor('show',TShowVisitor); 

O FilPeople também é um procedimento auxiliar que preenche uma lista com três objetos; seu código é obtido do construtor de lista anterior. Não se esqueça de destruir todos os objetos criados. Nesse caso, escrevemos FPeople.Free e VM.Free no manipulador de fechamento de formulário.

E agora - bams! - manipulador do primeiro botão:

 Memo1.Clear; VM.Execute('show',FPeople); 

Concordo, muito mais divertido. E não jure pelo hash de todas as classes em um módulo. No final do manual, coletaremos esses escombros.

Etapa 9. A classe base do Visitor que trabalha com arquivos de texto


Nesta fase, criaremos a classe base do Visitante que sabe trabalhar com arquivos de texto. Existem três maneiras de trabalhar com arquivos no objeto pascal: procedimentos antigos a partir do primeiro pascal (como AssignFile e ReadLn), trabalhar com fluxos (TStringStream ou TFileStream) e usar o objeto TStringList.

Se o primeiro método estiver muito desatualizado, o segundo e o terceiro serão uma boa alternativa baseada no POO. Ao mesmo tempo, trabalhar com fluxos fornece benefícios adicionais, como a capacidade de compactar e criptografar dados, mas a leitura e gravação linha a linha em um fluxo é um tipo de redundância em nosso exemplo. Para simplificar, escolheremos um TStringList, que possui dois métodos simples - LoadFromFile e SaveToFile. Mas lembre-se de que, com arquivos grandes, esses métodos ficam significativamente mais lentos, portanto o fluxo será a escolha ideal para eles.

Interface da classe base TVisFile:

 TVisFile = class(TVisitor) protected   FList: TStringList;   FFileName: TFileName; public   constructor Create; virtual;   destructor Destroy; override; end; 

E a implementação do construtor e destruidor:

 constructor TVisFile.Create; begin inherited Create; FList := TStringList.Create; if FileExists(FFileName) then   FList.LoadFromFile(FFileName); end; destructor TVisFile.Destroy; begin FList.SaveToFile(FFileName); FList.Free; inherited; end; 

O valor da propriedade FFileName será atribuído nos construtores dos descendentes dessa classe base (apenas não use o código rígido, que iremos organizar aqui, como o principal estilo de programação depois!). O diagrama das classes Visitor que trabalham com arquivos é o seguinte:



De acordo com o diagrama abaixo, criamos dois descendentes da classe base TVisFile: TVisTXTFile e TVisCSVFile. Um trabalhará com arquivos * .csv nos quais os campos de dados são separados por um símbolo (vírgula), o segundo - com arquivos de texto nos quais os campos de dados individuais terão um comprimento fixo em uma linha. Para essas classes, redefinimos apenas os construtores da seguinte maneira:

 constructor TVisCSVFile.Create; begin FFileName := 'contacts.csv'; inherited Create; end; constructor TVisTXTFile.Create; begin FFileName := 'contacts.txt'; inherited Create; end. 

Etapa 10. Adicione o manipulador de visitantes de arquivos de texto


Aqui vamos adicionar dois visitantes específicos, um lerá um arquivo de texto e o segundo escreverá nele. O visitante de leitura deve substituir os métodos da classe base AcceptVisitor e Execute. AcceptVisitor verifica se o objeto da classe TPeople é passado para o Visitor:

 Result := pVisited is TPeople; 

A implementação de execução é a seguinte:

 procedure TVisTXtRead.Execute(pVisited: TVisited); var i: integer; lData: TPerson; begin if not AcceptVisitor(pVisited) then   Exit; //==> TPeople(pVisited).Clear; for i := 0 to FList.Count - 1 do begin   lData := TPerson.Create;   lData.Name := Trim(Copy(FList.Strings[i], 1, 20));   lData.EMailAdrs := Trim(Copy(FList.Strings[i], 21, 80));   TPeople(pVisited).Add(lData); end; end; 

O visitante limpa primeiro a lista do objeto TPeople passado a ele pelo parâmetro, depois lê as linhas de seu objeto TStringList, nas quais o conteúdo do arquivo é carregado, cria um objeto TPerson em cada linha e o adiciona à lista de contêineres TPeople. Para simplificar, as propriedades name e emailadrs no arquivo de texto são separadas por espaços.

O visitante do registro implementa a operação inversa. Seu construtor (substituído) limpa a TStringList interna (ou seja, executa a operação FList.Clear; é obrigatória após a herança), AcceptVisitor verifica se o objeto da classe TPerson foi passado, o que não é um erro, mas uma diferença importante do mesmo método de leitura do Visitor. Parece mais fácil implementar a gravação da mesma maneira - verifique todos os objetos do contêiner, adicione-os a um StringList e salve-o em um arquivo. Tudo isso foi assim, se realmente estávamos falando sobre a gravação final dos dados em um arquivo, no entanto, planejamos mapear dados para um banco de dados relacional, isso deve ser lembrado. E, neste caso, devemos executar o código SQL apenas para os objetos que foram alterados (criados, excluídos ou editados). É por isso que antes que o Visitante execute uma operação no objeto,ele deve verificar a correspondência de seu tipo:

 Result := pVisited is Tperson; 

O método execute simplesmente adiciona ao StringList interno uma string formatada com a regra especificada: primeiro, o conteúdo da propriedade name do objeto passado, preenchido com espaços de até 20 caracteres, depois o conteúdo da propriedade emaiadrs:

 procedure TVisTXTSave.Execute(pVisited: TVisited); begin if not AcceptVisitor(pVisited) then   exit; FList.Add(PadRight(TPerson(pVisited).Name,20)+PadRight(TPerson(pVisited).EMailAdrs,60)); end; 

Etapa 11. Adicione o manipulador de visitantes de arquivos CSV


Os visitantes que lêem e escrevem são semelhantes em quase todos os colegas das classes TXT, exceto na maneira de formatar a linha final de um arquivo: no padrão CSV, os valores das propriedades são separados por vírgulas. Para ler linhas e analisá-las em propriedades, usamos a função ExtractDelimited do módulo strutils, e a gravação é realizada simplesmente concatenando as linhas:

 procedure TVisCSVRead.Execute(pVisited: TVisited); var i: integer; lData: TPerson; begin if not AcceptVisitor(pVisited) then   exit; TPeople(pVisited).Clear; for i := 0 to FList.Count - 1 do begin   lData := TPerson.Create;   lData.Name := ExtractDelimited(1, FList.Strings[i], [',']);   lData.EMailAdrs := ExtractDelimited(2, FList.Strings[i], [',']);   TPeople(pVisited).Add(lData); end; end; procedure TVisCSVSave.Execute(pVisited: TVisited); begin if not AcceptVisitor(pVisited) then   exit; FList.Add(TPerson(pVisited).Name + ',' + TPerson(pVisited).EMailAdrs); end; 

Tudo o que resta para nós é registrar novos visitantes no gerente e verificar o funcionamento do aplicativo. No manipulador de criação de formulário, adicione o seguinte código:

 VM.RegisterVisitor('readTXT', TVisTXTRead); VM.RegisterVisitor('saveTXT',TVisTXTSave); VM.RegisterVisitor('readCSV',TVisCSVRead); VM.RegisterVisitor('saveCSV',TVisCSVSave); 

Encaixe os botões necessários no formulário e atribua a eles os manipuladores apropriados:



 procedure TForm1.ReadCSVbtnClick(Sender: TObject); begin VM.Execute('readCSV', FPeople); end; procedure TForm1.ReadTXTbtnClick(Sender: TObject); begin VM.Execute('readTXT', FPeople); end; procedure TForm1.SaveCSVbtnClick(Sender: TObject); begin VM.Execute('saveCSV', FPeople); end; procedure TForm1.SaveTXTbtnClick(Sender: TObject); begin VM.Execute('saveTXT', FPeople); end; 

Formatos de arquivo adicionais para salvar dados são implementados simplesmente adicionando os Visitantes apropriados e registrando-os no Gerenciador. E preste atenção ao seguinte: intencionalmente nomeamos os comandos de forma diferente, ou seja, saveTXT e saveCSV. Se os dois visitantes corresponderem a um comando de salvamento, os dois iniciarão no mesmo comando, verifique você mesmo.

Etapa 12. Limpeza final do código


Para maior beleza e pureza do código, além de preparar um projeto para o desenvolvimento adicional da interação com o DBMS, distribuiremos nossas classes em diferentes módulos de acordo com a lógica e sua finalidade. No final, devemos ter a seguinte estrutura de módulos na pasta do projeto, o que nos permite prescindir de uma relação circular entre eles (ao se montar, organize os módulos necessários nas seções de uso):

Módulo
Função
Aulas
tivisitor.pas
Classes base do modelo Visitor e Manager
TVisitor
TVisited
TVisMapping
TtiVisitorManager
tiobject.pas
Classes base de lógica de negócios
TtiObject
TtiObjectList
people_BOM.pas
Classes específicas de lógica de negócios
TPerson
TPeople
people_SRV.pas
Classes concretas responsáveis ​​pela interação
TVisFile
TVisTXTFile
TVisCSVFile
TVisCSVSave
TVisCSVRead
TVisTXTSalve
TVisTXTRead

Conclusão


Neste artigo, examinamos o problema de iterar sobre uma coleção ou lista de objetos que podem ter tipos diferentes. Usamos o modelo de visitante proposto pelo GoF para implementar de maneira ideal duas maneiras diferentes de mapear dados de objetos para arquivos de diferentes formatos. Ao mesmo tempo, diferentes métodos podem ser executados por uma equipe devido à criação do Visitor Manager. Por fim, os exemplos simples e ilustrativos discutidos no artigo nos ajudarão a desenvolver um sistema semelhante para mapear objetos para um banco de dados relacional.

Arquivo com código fonte de exemplos - aqui

Source: https://habr.com/ru/post/pt441266/


All Articles