Cómo funciona el marco tiOPF para delphi / lazarus. Plantilla de visitante

Del traductor


Hay dos razones por las cuales me comprometí a traducir varios materiales en el marco desarrollado hace veinte años para el entorno de programación no muy popular:

1. Hace varios años, después de haber aprendido muchas de las delicias de trabajar con Entity Framework como ORM para la plataforma .Net, busqué en vano análogos para el entorno de Lazarus y, en general, freepascal.
Sorprendentemente, le faltan buenos ORM. Todo lo que se descubrió fue un proyecto de código abierto llamado tiOPF , desarrollado a fines de los 90 para Delphi, que luego se transfirió a Freepascal. Sin embargo, este marco es fundamentalmente diferente del aspecto habitual de los ORM grandes y gruesos.

No hay formas visuales de diseñar objetos (en Entidad - modelo primero) y mapear objetos a campos en tablas de bases de datos relacionales (en Entidad - base de datos primero) en tiOPF. El desarrollador mismo posiciona este hecho como una de las deficiencias del proyecto, sin embargo, como mérito, ofrece una orientación completa específicamente sobre el modelo de negocio de objetos, solo vale un código duro ...

Fue al nivel del hardcoding propuesto que tuve problemas. En ese momento, no estaba muy versado en esos paradigmas y métodos que el desarrollador del marco utilizó en su totalidad y mencioné en la documentación varias veces por párrafo (patrones de diseño del visitante, enlazador, observador, varios niveles de abstracción para la independencia del DBMS, etc. .). Mi gran proyecto que trabajaba con la base de datos en ese momento estaba completamente enfocado en los componentes visuales de Lazarus y la forma de trabajar con las bases de datos ofrecidas por el entorno visual, como resultado, toneladas del mismo código: tres tablas en la misma base de datos con casi la misma estructura y datos homogéneos, tres formularios idénticos para ver, tres formularios idénticos para editar, tres formularios idénticos para informes y todo lo demás desde la parte superior del encabezado "Cómo no diseñar software".

Después de leer suficiente literatura sobre los principios del diseño correcto de bases de datos y sistemas de información, incluido el estudio de plantillas, así como conocer el Entity Framework, decidí hacer una refactorización completa tanto de la base de datos como de mi aplicación. Y si hice frente por completo a la primera tarea, entonces, para la implementación de la segunda, había dos caminos que iban en diferentes direcciones: ir completamente a estudiar .net, C # y Entity Framework, o encontrar un ORM adecuado para el conocido sistema Lazarus. También hubo un tercer, primer sendero de bicicleta discreto: escribir ORM para que se ajuste a sus necesidades, pero este no es el punto ahora.

El código fuente del marco no se comenta mucho, pero los desarrolladores sin embargo prepararon (aparentemente en el período inicial de desarrollo) una cierta cantidad de documentación. Todo esto, por supuesto, es de habla inglesa, y la experiencia muestra que, a pesar de la abundancia de códigos, diagramas y frases de programación de plantillas, muchos programadores de habla rusa todavía están mal orientados en la documentación en inglés. No siempre y no todos tienen el deseo de entrenar la capacidad de entender el texto técnico en inglés sin la necesidad de que la mente lo traduzca al ruso.

Además, la revisión reiterada del texto para la traducción le permite ver lo que me perdí cuando conocí la documentación, no lo entendí completa o incorrectamente. Es decir, esta es una oportunidad para aprender mejor el marco de estudio.

2. En la documentación, el autor omite intencionalmente o no algunas piezas de código, probablemente obvio en su opinión. Debido a la limitación de su redacción, la documentación utiliza mecanismos y objetos obsoletos como ejemplos, eliminados o que ya no se utilizan en nuevas versiones del marco (¿pero no dije que continúa desarrollándose?). Además, cuando repetí los ejemplos desarrollados por mí mismo, encontré algunos errores que deberían corregirse. Por lo tanto, en algunos lugares me permití no solo traducir el texto, sino también complementarlo o revisarlo para que siga siendo relevante, y los ejemplos estaban funcionando.

Quiero comenzar la traducción de materiales de un artículo de Peter Henrikson sobre la primera "ballena" en la que se basa todo el marco: la plantilla de visitante. Texto original publicado aquí .

Plantilla visitante y tiOPF


El propósito de este artículo es presentar la plantilla Visitor, cuyo uso es uno de los conceptos principales del marco de trabajo tiOPF (TechInsite Object Persistence Framework). Consideraremos el problema en detalle, después de analizar soluciones alternativas antes de usar el Visitante. En el proceso de desarrollar nuestro propio concepto de visitante, nos enfrentaremos a otro desafío: la necesidad de recorrer todos los objetos de la colección. Este tema también será estudiado.

La tarea principal es encontrar una forma generalizada de realizar un conjunto de métodos relacionados en algunos objetos de la colección. Los métodos realizados pueden variar según el estado interno de los objetos. No podemos ejecutar métodos en absoluto, pero podemos ejecutar varios métodos en los mismos objetos.

El nivel necesario de entrenamiento.


El lector debe estar familiarizado con el objeto pascal y dominar los principios básicos de la programación orientada a objetos.

Ejemplo de tarea empresarial en este artículo


Como ejemplo, desarrollaremos una libreta de direcciones que le permite crear registros de personas y su información de contacto. Con el aumento de las posibles formas de comunicación entre las personas, la aplicación debería permitirle agregar de manera flexible dichos métodos sin un procesamiento de código significativo (recuerdo que una vez que terminé de procesar el código para agregar un número de teléfono, inmediatamente necesité procesarlo nuevamente para agregar correo electrónico). Necesitamos proporcionar dos categorías de direcciones: reales, como domicilio, postal, laboral y electrónica: teléfono fijo, fax, móvil, correo electrónico, sitio web.

En el nivel de presentación, nuestra aplicación debería verse como Explorer / Outlook, es decir, se supone que debe usar componentes estándar como TreeView y ListView. La aplicación debería funcionar rápidamente y no dar la impresión de un software voluminoso cliente-servidor.

Una aplicación podría verse así:



En el menú contextual del árbol, puede elegir agregar / eliminar el contacto de una persona o empresa, y hacer clic con el botón derecho en la lista de datos de contacto para abrir su diálogo de edición, eliminar o agregar datos.

Los datos se pueden guardar de varias formas, y en el futuro consideraremos cómo usar esta plantilla para implementar esta función.

Antes de empezar


Comenzaremos con una simple colección de objetos: una lista de personas que a su vez tienen dos propiedades: nombre (Nombre) y dirección (EmailAdrs). Para empezar, la lista se rellenará con datos en el constructor y posteriormente se cargará desde un archivo o base de datos. Por supuesto, este es un ejemplo muy simplificado, pero es suficiente para implementar completamente la plantilla de visitante.

Cree una nueva aplicación y agregue dos clases de la sección de interfaz del módulo principal: TPersonList (heredado de TObjectList y requiere un complemento en el módulo contnrs) y TPerson (heredado 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; 

En el constructor TPersonList, creamos tres objetos TPerson y los agregamos a la 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; 

Primero, revisaremos la lista y realizaremos dos operaciones en cada elemento de la lista. Las operaciones son similares, pero no iguales: una simple llamada ShowMessage para mostrar el contenido de las propiedades Name y EmailAdrs de los objetos TPerson. Agregue dos botones al formulario y asígneles un nombre similar a este:



En el alcance preferido de su formulario, también debe agregar una propiedad (o solo un campo) FPersonList de tipo TPersonList (si el tipo se declara debajo del formulario, cambie el orden o haga una declaración de tipo preliminar) y llame al constructor en el controlador de eventos onCreate:

 FPersonList := TPersonList.Create; 

Para liberar memoria correctamente en el controlador de eventos onClose del formulario, este objeto debe destruirse:

 FPersonList.Free. 

Paso 1. Iteración del código duro


Para mostrar nombres de objetos TPerson, agregue el siguiente código al controlador de eventos onClick del primer botón:

 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 el segundo botón, el código del controlador será el siguiente:

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

Aquí están los bancos evidentes de este código:

  • dos métodos que hacen casi lo mismo. Toda la diferencia está solo en el nombre de la propiedad del objeto que muestran;
  • la iteración será tediosa, especialmente cuando se ve obligado a escribir un bucle similar en cien lugares en el código;
  • Un duro reparto para TPerson está plagado de situaciones excepcionales. ¿Qué sucede si hay una instancia de TAnimal en la lista sin una propiedad de dirección? No hay ningún mecanismo para detener el error y defenderse contra él en este código.

Vamos a descubrir cómo mejorar el código mediante la introducción de una abstracción: pasamos el código iterador a la clase principal.

Paso 2. Resumen del iterador


Entonces, queremos mover la lógica del iterador a la clase base. El iterador de la lista en sí es muy simple:

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

Parece que estamos planeando usar una plantilla Iterator . Del libro sobre el libro de patrones de diseño Gang-of-Four , se sabe que el iterador puede ser externo e interno. Cuando se utiliza un iterador externo, el cliente controla explícitamente el recorrido llamando al método Next (por ejemplo, la enumeración de los elementos TCollection se controla mediante los métodos First, Next, Last). Aquí usaremos el iterador interno, ya que es más fácil implementar el recorrido del árbol con su ayuda, que es nuestro objetivo. Agregaremos el método Iterate a nuestra clase de lista y le pasaremos un método de devolución de llamada, que debe realizarse en cada elemento de la lista. La devolución de llamada en el objeto pascal se declara como un tipo de procedimiento, tendremos, por ejemplo, TDoSomethingToAPerson.

Entonces, declaramos un tipo de procedimiento TDoSomethingToAPerson, que toma un parámetro del tipo TPerson. El tipo de procedimiento le permite utilizar el método como parámetro de otro método, es decir, implementar la devolución de llamada. De esta forma, crearemos dos métodos, uno de los cuales mostrará la propiedad Name del objeto y el otro, la propiedad EmailAdrs, y ellos mismos se pasarán como un parámetro al iterador general. Finalmente, la sección de declaración de tipo debería verse así:

 { 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; 

Ahora, para realizar las acciones necesarias en los elementos de la lista, debemos hacer dos cosas. Primero, defina las operaciones necesarias utilizando métodos que tengan la firma especificada por TDoSomethingToAPerson y, en segundo lugar, escriba las llamadas DoSomething con los punteros a estos métodos pasados ​​como parámetro. En la sección de descripción del formulario, agregue dos declaraciones:

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

En la implementación de estos 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; 

El código para los manejadores de botones se cambia de la siguiente manera:

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

Ya mejor. Ahora tenemos tres niveles de abstracciones en nuestro código. Un iterador general es un método de una clase que implementa una colección de objetos. La lógica empresarial (hasta ahora solo un mensaje sin fin a través de ShowMessage) se coloca por separado. En el nivel de presentación (interfaz gráfica), la lógica empresarial se llama en una línea.

Es fácil imaginar cómo una llamada a ShowMessage puede ser reemplazada por un código que guarda nuestros datos de TPerson en una base de datos relacional utilizando la consulta SQL del objeto TQuery. Por ejemplo, así:

 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; 

Por cierto, esto introduce un nuevo problema de mantener una conexión a la base de datos. En nuestra solicitud, la conexión a la base de datos se realiza a través de algún objeto global gAppDatabase. ¿Pero dónde se ubicará y cómo trabajar? Además, estamos atormentados en cada paso del iterador para crear objetos TQuery, configurar la conexión, ejecutar la consulta y no olvidar liberar la memoria. Sería mejor incluir este código en una clase que encapsule la lógica de crear y ejecutar consultas SQL, así como configurar y mantener una conexión a la base de datos.

Paso 3. Pasar un objeto en lugar de pasar un puntero a una devolución de llamada


Pasar el objeto al método iterador de la clase base resolverá el problema del mantenimiento del estado. Crearemos una clase de visitante TPersonVisitor abstracta con un único método Execute y pasaremos el objeto a este método como parámetro. La interfaz de visitante abstracta se presenta a continuación:

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

A continuación, agregue el método Iterate a nuestra clase TPersonList:

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

La implementación de este método será la siguiente:

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

Un objeto del visitante implementado de la clase TPersonVisitor se pasa al método Iterate y, al recorrer en iteración los elementos de la lista para cada uno de ellos, se llama al visitante especificado (su método de ejecución) con la instancia de TPerson como parámetro.

Creemos dos implementaciones del visitante: TShowNameVisitor y TShowEmailVistor, que realizarán el trabajo requerido. Aquí se explica cómo reponer la sección de interfaces del 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 simplicidad, la implementación de los métodos de ejecución en ellos seguirá siendo una sola línea: ShowMessage (pPerson.Name) y ShowMessage (pPerson.EMailAdrs).

Y cambie el código para los controladores de clic de botón:

 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; 

Ahora, habiendo resuelto un problema, creamos otro para nosotros mismos. La lógica del iterador se encapsula en una clase separada; Las operaciones realizadas durante la iteración están envueltas en objetos, lo que nos permite guardar información sobre el estado, pero el tamaño del código ha crecido de una línea (FPersonList.DoSomething (@DoShowName); a nueve líneas para cada controlador de botón. Ahora nos ayudará: este es el Administrador de visitantes, que se encargará de crear y liberar sus copias. Potencialmente, podemos proporcionar varias operaciones con objetos para realizar durante la iteración, para esto, el Administrador de visitantes almacenará su lista y la revisará en cada paso, usted . Olnyaya solamente operaciones seleccionadas siguiente demostrará claramente los beneficios de este enfoque, vamos a utilizar los visitantes para guardar los datos en una base de datos relacional como un simple datos de operación de ahorro se pueden llevar a cabo por tres operadores diferentes SQL: crear, borrar y la actualización.

Paso 4. Encapsulación adicional del visitante


Antes de continuar, debemos encapsular la lógica del trabajo del Visitante, separándola de la lógica de negocios de la aplicación para que no vuelva a ella. Nos llevará tres pasos hacer esto: crear las clases base TVisited y TVisitor, luego las clases base para el objeto comercial y la colección de objetos comerciales, luego ajustar ligeramente nuestras clases específicas TPerson y TPersonList (o TPeople) para que se conviertan en herederos de la base creada clases En términos generales, la estructura de clases corresponderá a dicho diagrama:



El objeto TVisitor implementa dos métodos: la función AcceptVisitor y el procedimiento Execute, en el que se pasa el objeto de tipo TVisited. El objeto TVisited a su vez implementa el método Iterate con un parámetro de tipo TVisitor. Es decir, TVisited.Iterate debe llamar al método Execute en el objeto TVisitor transferido, enviando un enlace a su propia instancia como parámetro, y si la instancia es una colección, se llama al método Execute para cada elemento de la colección. La función AcceptVisitor es necesaria ya que estamos desarrollando un sistema generalizado. Será posible pasar al Visitante, que opera solo con tipos TPerson, una instancia de la clase TDog, por ejemplo, y debe haber un mecanismo para evitar excepciones y errores de acceso debido a la falta de coincidencia de tipos. La clase TVisited es descendiente de la clase TPersistent, ya que un poco más tarde tendremos que implementar funciones relacionadas con el uso de RTTI.

La parte de la interfaz del módulo ahora será así:

 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; 

Los herederos implementarán los métodos de la clase abstracta TVisitor, y la implementación general del método Iterate para TVisited se detalla a continuación:

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

Al mismo tiempo, el método se declara virtual por la posibilidad de su anulación en los herederos.

Paso 5. Crear un objeto comercial y una colección compartidos


Nuestro marco necesita dos clases base más: para definir un objeto comercial y una colección de dichos objetos. Llámalos TtiObject y TtiObjectList. La interfaz del primero de ellos:

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

Más adelante en el proceso de desarrollo, complicaremos esta clase, pero para la tarea actual, solo un constructor virtual con la posibilidad de anularlo en los herederos es suficiente.

Planeamos generar la clase TtiObjectList de TVisited para usar el comportamiento en métodos que ya han sido implementados por el ancestro (también hay otras razones para esta herencia que serán discutidas en su lugar). Además, nada prohíbe el uso de interfaces (interfaces) en lugar de clases abstractas.

La parte de la interfaz de la clase TtiObjectList será la siguiente:

 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 puede ver, el contenedor en sí con los elementos del objeto se encuentra en la sección protegida y no estará disponible para los clientes de esta clase. La parte más importante de la clase es la implementación del método Iterate anulado. Si en la clase base el método simplemente se llama pVisitor.Execute (self), entonces la implementación está relacionada con enumerar la 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; 

La implementación de otros métodos de clase toma una línea de código sin tener en cuenta las expresiones heredadas colocadas automáticamente:

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

Esta es una parte importante de todo el sistema. Tenemos dos clases básicas de lógica de negocios: TtiObject y TtiObjectList. Ambos tienen un método Iterate al que se pasa una instancia de la clase TVisited. El iterador mismo llama al método Execute de la clase TVisitor y pasa una referencia al objeto mismo. Esta llamada está predefinida en el comportamiento de la clase en el nivel superior de herencia. Para una clase de contenedor, cada objeto almacenado en la lista también tiene su método Iterate, llamado con un parámetro de tipo TVisitor, es decir, se garantiza que cada visitante específico omitirá todos los objetos almacenados en la lista, así como la lista misma como un objeto contenedor.

Paso 6. Crear un administrador de visitantes


Entonces, volviendo al problema que nosotros mismos dibujamos en el tercer paso. Dado que no queremos crear y destruir copias de Visitantes cada vez, el desarrollo del Administrador será la solución. Debe realizar dos tareas principales: administrar la lista de Visitantes (que están registrados como tales en la sección de inicialización de módulos individuales) y ejecutarlos cuando reciben el comando apropiado del cliente.
Para implementar el administrador, complementaremos nuestro módulo con tres clases adicionales: TVisClassRef, TVisMapping y TtiVisitorManager.

 TVisClassRef = class of TVisitor; 

TVisClassRef es un tipo de referencia e indica el nombre de una clase en particular, un descendiente de TVisitor. El significado de usar un tipo de referencia es el siguiente: cuando se llama al método Execute base con una firma

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

internamente, este método puede usar una expresión como lVisitor: = pVisClass.Create para crear una instancia de un visitante específico, sin conocer primero su tipo. Es decir, cualquier clase: se puede crear dinámicamente un descendiente de TVisitor dentro del mismo método Execute al pasar el nombre de su clase como parámetro.

La segunda clase, TVisMapping, es una estructura de datos simple con dos propiedades: una referencia al tipo TVisClassRef y una propiedad de cadena Comando. Se necesita una clase para comparar las operaciones realizadas por su nombre (un comando, por ejemplo, "guardar") y la clase Visitor, que ejecutan estos comandos. Agregue su código al proyecto:

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

Y la última clase es TtiVisitorManager. Cuando registramos al Visitante usando el Administrador, se crea una instancia de la clase TVisMapping, que se ingresa en la lista del Administrador.
Por lo tanto, en el Administrador, se crea una lista de Visitantes con una coincidencia de comandos de cadena, una vez recibidos, se ejecutarán. La interfaz de clase se agrega al 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; 

Sus métodos clave son RegisterVisitor y Execute. El primero generalmente se llama en la sección de inicialización del módulo, que describe la clase Visitor, y se parece a esto:

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

El código del método en sí será el siguiente:

 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; 

No es difícil notar que este código es muy similar a la implementación de Pascal de la plantilla Factory .

Otro método de ejecución importante acepta dos parámetros: el comando por el cual se identificará al visitante o su grupo a identificar, así como el objeto de datos cuyo método Iterate se llamará con un enlace a la instancia del visitante deseado. El código completo para el método Execute se proporciona a continuación:

 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; 

Por lo tanto, para ejecutar dos Visitantes previamente registrados con un equipo, solo necesitamos una línea de código:

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

A continuación, complementaremos nuestro proyecto para que pueda llamar a comandos similares:

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

Paso 7. Ajuste de las clases de lógica de negocios


Agregar el antepasado de las clases TtiObject y TtiObjectList para nuestros objetos comerciales TPerson y TPeople nos permitirá encapsular la lógica del iterador en la clase base y no tocarla más, además, es posible transferir objetos con datos al Administrador de Visitantes.

La nueva declaración de clase de contenedor se verá así:

 TPeople = class(TtiObjectList); 

De hecho, la clase TPeople ni siquiera tiene que implementar nada. Teóricamente, podríamos prescindir de una declaración TPeople y almacenar objetos en una instancia de la clase TtiObjectList, pero dado que planeamos escribir Visitantes procesando solo instancias TPeople, necesitamos esta clase. En la función AcceptVisitor, se realizarán las siguientes verificaciones:

 Result := pVisited is TPeople. 

Para la clase TPerson, agregamos el ancestro TtiObject y movemos las dos propiedades existentes al ámbito publicado, ya que en el futuro tendremos que trabajar a través de RTTI con estas propiedades. Es mucho más tarde lo que reducirá significativamente el código involucrado en el mapeo de objetos y registros en una base de datos 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; 

Paso 8. Crear una vista de prototipo


Observación En el artículo original, la GUI se basaba en componentes que el autor de tiOPF hizo para la conveniencia de trabajar con su marco delphi. Estos eran análogos de componentes DB Aware, que eran controles estándar como etiquetas, campos de entrada, casillas de verificación, listas, etc., pero estaban asociados con ciertas propiedades de los objetos tiObject de la misma manera que los componentes de visualización de datos estaban asociados con campos en tablas de bases de datos. Con el tiempo, el autor del marco marcó los paquetes con estos componentes visuales como obsoletos e indeseables de usar.A cambio, sugiere crear un enlace entre los componentes visuales y las propiedades de la clase utilizando el patrón de diseño Mediator. Esta plantilla es la segunda más importante en toda la arquitectura del marco. La descripción del autor del intermediario está cubierta por un artículo separado, comparable en volumen a este manual, por lo tanto, ofrezco mi versión simplificada aquí como GUI.

Cambie el nombre del botón 1 en el formulario del proyecto a "mostrar comando", y el botón 2 lo deja sin un controlador por ahora, o inmediatamente llámelo "guardar comando". Lanza un componente memo en el formulario y coloca todos los elementos a tu gusto.

Agregue una clase Visitor que implementará el comando show:

Interface -

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

Y la implementación es -
 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 que el objeto que se transfiere es una instancia de TPerson, porque el visitante solo debe ejecutar el comando con dichos objetos. Si el tipo coincide, el comando se ejecuta y se agrega una línea con propiedades de objeto al campo de texto.

Las acciones de apoyo para la salud del código serán las siguientes. Agregue dos propiedades a la descripción del formulario en la sección privada: FPeople de tipo TPeople y VM de tipo TtiVisitorManager. En el controlador de eventos de creación de formularios, necesitamos iniciar estas propiedades, así como registrar al visitante con el comando "show":

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

FilPeople también es un procedimiento auxiliar que llena una lista con tres objetos; su código se toma del constructor de la lista anterior. No olvides destruir todos los objetos creados. En este caso, escribimos FPeople.Free y VM.Free en el controlador de cierre de formulario.

Y ahora, ¡bams! - controlador del primer botón:

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

De acuerdo, mucho más divertido. Y no jures por el hash de todas las clases en un módulo. Al final del manual, rastrillaremos estos escombros.

Paso 9. La clase base del visitante que trabaja con archivos de texto


En esta etapa, crearemos la clase base del Visitante que sabe cómo trabajar con archivos de texto. Hay tres formas de trabajar con archivos en el pascal de objeto: procedimientos antiguos desde el momento del primer pascal (como AssignFile y ReadLn), trabajar a través de secuencias (TStringStream o TFileStream) y usar el objeto TStringList.

Si el primer método está muy desactualizado, entonces el segundo y el tercero son una buena alternativa basada en OOP. Al mismo tiempo, trabajar con transmisiones además brinda beneficios tales como la capacidad de comprimir y cifrar datos, pero la lectura y escritura línea por línea en una transmisión es una especie de redundancia en nuestro ejemplo. Para simplificar, elegiremos una TStringList, que tiene dos métodos simples: LoadFromFile y SaveToFile. Pero recuerde que con archivos grandes, estos métodos se ralentizarán significativamente, por lo que la transmisión será la mejor opción para ellos.

Interfaz de clase base de TVisFile:

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

Y la implementación del constructor y destructor:

 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; 

El valor de la propiedad FFileName se asignará a los constructores de los descendientes de esta clase base (simplemente no use el hardcoding, que arreglaremos aquí, ¡como el estilo de programación principal después!). El diagrama de las clases Visitor que trabajan con archivos es el siguiente:



De acuerdo con el diagrama a continuación, creamos dos descendientes de la clase base TVisFile: TVisTXTFile y TVisCSVFile. Uno trabajará con archivos * .csv en los que los campos de datos están separados por un símbolo (coma), el segundo, con archivos de texto en los que los campos de datos individuales tendrán una longitud fija por línea. Para estas clases, solo redefinimos los constructores de la siguiente manera:

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

Paso 10. Agregue el controlador de visitante de archivos de texto


Aquí agregaremos dos Visitantes específicos, uno leerá un archivo de texto, el segundo lo escribirá. El visitante de lectura debe anular los métodos de clase base AcceptVisitor y Execute. AcceptVisitor verifica que el objeto de la clase TPeople se pase al visitante:

 Result := pVisited is TPeople; 

La implementación de ejecución es la siguiente:

 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; 

El visitante primero borra la lista del objeto TPeople que le pasó por el parámetro, luego lee las líneas de su objeto TStringList, en el que se cargan los contenidos del archivo, crea un objeto TPerson en cada línea y lo agrega a la lista de contenedores TPeople. Para simplificar, las propiedades de nombre y correo electrónico en el archivo de texto están separadas por espacios.

El visitante de registro implementa la operación inversa. Su constructor (anulado) borra la TStringList interna (es decir, realiza la operación FList.Clear; es obligatoria después de heredada), AcceptVisitor comprueba que se pasa el objeto de la clase TPerson, lo cual no es un error, sino una diferencia importante con respecto al mismo método de lectura de Visitor. Parece más fácil implementar la grabación de la misma manera: escanee todos los objetos del contenedor, agréguelos a una StringList y luego guárdelos en un archivo. Todo esto fue así, si realmente estuviéramos hablando de la escritura final de datos en un archivo, sin embargo, planeamos asignar datos a una base de datos relacional, esto debería recordarse. Y en este caso, debemos ejecutar el código SQL solo para aquellos objetos que han sido cambiados (creados, eliminados o editados). Es por eso que antes de que el Visitante realice una operación en el objeto,debe verificar la correspondencia de su tipo:

 Result := pVisited is Tperson; 

El método execute simplemente agrega a la StringList interna una cadena formateada con la regla especificada: primero, el contenido de la propiedad del nombre del objeto pasado, rellenado con espacios de hasta 20 caracteres, luego el contenido de la propiedad 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; 

Paso 11. Agregue el controlador de visitante de archivos CSV


Los visitantes de lectura y escritura son similares en casi todos sus colegas de las clases TXT, excepto por la forma de formatear la línea final de un archivo: en el estándar CSV, los valores de propiedad están separados por comas. Para leer líneas y analizarlas en propiedades, utilizamos la función ExtractDelimited del módulo strutils, y la escritura se realiza simplemente concatenando las líneas:

 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; 

Todo lo que nos queda es registrar nuevos Visitantes en el Administrador y verificar el funcionamiento de la aplicación. En el controlador de creación de formularios, agregue el siguiente código:

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

Acople los botones necesarios en el formulario y asígneles los controladores apropiados:



 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; 

Los formatos de archivo adicionales para guardar datos se implementan simplemente agregando los Visitantes apropiados y registrándolos en el Administrador. Y preste atención a lo siguiente: intencionalmente nombramos los comandos de manera diferente, es decir, saveTXT y saveCSV. Si ambos visitantes coinciden con un comando de guardar, ambos comenzarán con el mismo comando, compruébelo usted mismo.

Paso 12. Limpieza final del código


Para la mayor belleza y pureza del código, así como para preparar un proyecto para el desarrollo posterior de la interacción con el DBMS, distribuiremos nuestras clases en diferentes módulos de acuerdo con la lógica y su propósito. Al final, deberíamos tener la siguiente estructura de módulos en la carpeta del proyecto, lo que nos permite prescindir de una relación circular entre ellos (al ensamblarlo, organizar los módulos necesarios en las secciones de usos):

Modulo
Función
Clases
tivisitor.pas
Clases base de la plantilla de visitante y administrador
TVisitor
TVisited
TVisMapping
TtiVisitorManager
tiobject.pas
Clases básicas de lógica de negocios
TtiObject
TtiObjectList
people_BOM.pas
Clases específicas de lógica de negocios
TPerson
TPeople
people_SRV.pas
Clases concretas responsables de la interacción.
TVisFile
TVisTXTFile
TVisCSVFile
TVisCSVGuardar
TVisCSVRead
TVisTXTSave
TVisTXTRead

Conclusión


En este artículo, examinamos el problema de iterar sobre una colección o lista de objetos que pueden tener diferentes tipos. Utilizamos la plantilla Visitor propuesta por GoF para implementar de manera óptima dos formas diferentes de mapear datos de objetos a archivos de diferentes formatos. Al mismo tiempo, un equipo puede realizar diferentes métodos debido a la creación del Administrador de Visitantes. Finalmente, los ejemplos simples e ilustrativos discutidos en el artículo nos ayudarán a desarrollar un sistema similar para mapear objetos a una base de datos relacional.

Archivo con código fuente de ejemplos - aquí

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


All Articles