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