jueves, 28 de diciembre de 2017

Componentes e interfaces (finales distintos)

Finales distintos

Los componentes y las interfaces tienen mecanismos de destrucción diferentes. Los componentes tienen un mecanismo de destrucción automático basado en el dueño (Owner) Las interfaces tienen un mecanismo de destrucción automático basado en la cantidad de referencias vigentes.

Componentes

Cuando creamos un componente (una instancia de la clase TComponent o descendiente) el constructor recibe un parámetro de entrada de tipo TComponent. Si le pasamos nil entonces tendremos que encargarnos de destruir la instancia creada. Si le pasamos un objeto entonces cuando dicho objeto sea destruido se encargará de destruir las instancias de todos los objetos de los cuales es el dueño.

procedure TMiForm.DoMiComponente;
var
  oMiComponente: TMiComponente;
begin
  oMiComponente := TMiComponente.Create(Self);
  oMiComponente.Free;
end;

En el código anterior se crea una instancia de la clase TMiComponente y se mantiene una referencia a dicha instancia en la variable oMiComponente. Luego, a través de la referencia mantenida en dicha variable, se destruye la instancia. Como el Owner de la instancia de TMiComponente es Self, es decir, la instancia de TMiForm, si no destruyéramos explícitamente la instancia de TMiComponente, ésta sería destruída implícitamente al destruir la instancia de TMiForm.

Interfaces

Cuando creamos una instancia de una clase que implementa una interface y obtenemos referencias a la interface, internamente se lleva la cuenta de la cantidad de referencias vigentes. Cuando dicha cantidad llega a 0, es decir, ya no hay referencias vigentes, la instancia de la clase puede ser destruida. El código responsable de hacerlo es insertado automáticamente por el compilador y no tenemos ningún control sobre el mismo.

procedure TMiForm.DoMiInterface;
var
  oMiInterface: IMiInterface;
begin
  oMiInterface := TMiInterface.Create as IMiInterface;
  oMiInterface := nil;
end;

En el código anterior se crea una instancia de la clase TMiInterface que implementa la interface IMiInterface y se obtiene una referencia a dicha interface que se mantiene en la variable oMiInterface. Cuando dicha referencia deja de estar vigente, en este caso al asignarle nil, la instancia será destruida por el código insertado por el compilador. Si no le asignáramos nil a la variable oMiInterface, la referencia dejaría de estar vigente al finalizar la ejecución del método DoMiInterface porque la variable es local para dicho método.

Un ejemplo

Veamos un ejemplo muy simple.

type
  IMiInterface = interface(IInterface)
  ['{F0E8209C-4D47-46C5-B28F-D19ACEEC9552}']
    procedure DoMiMetodo;
  end;

  TMiObjeto = class(TInterfacedObject, IMiInterface)

  { IMiInterface }
  protected
    procedure DoMiMetodo;
  { IMiInterface }

  public
    destructor Destroy; override;
  end;

  TfrmMiForm = class(TForm)
    btnMiMetodo: TButton;
    procedure btnMiMetodoClick(Sender: TObject);
  end;


En la sección Interface, primero declaramos la interface, luego la clase que implementa la interface y finalmente el Form (la parte del Form la hace Delphi)

{ TfrmMiForm }

procedure TfrmMiForm.btnMiMetodoClick(Sender: TObject);
begin
  (TMiObjeto.Create as IMiInterface).DoMiMetodo;
end;

{ TMiObjeto }

destructor TMiObjeto.Destroy;
begin

  inherited;
end;

procedure TMiObjeto.DoMiMetodo;
begin
  ShowMessage('MiMetodo');
end;


En la sección Implementation implementamos los métodos. La implementación del método Destroy es sólo para poner un breakpoint y ver cómo el código insertado por el compilador destruye la instancia de TMiObjeto.

TInterfacedObject

Para que todo esto sea posible es necesario que la clase que implementa la interface descienda de la clase TInterfacedObject, que es donde se implementa el mecanismo de destrucción automático basado en la cantidad de referencias vigentes.

Descargar proyecto Delphi 2010

miércoles, 27 de diciembre de 2017

Datos en capas

Dos capas

El uso de los componentes ClientDataSet y DataSetProvider nos permiten trabajar en dos capas: una de acceso a datos y otra de lógica de datos. La primera más cerca de la base de datos y la segunda más cerca del usuario.

Un DataModule para cada capa

Cada una de estas capas puede estar contenida en su propio DataModule. Así, tenemos dos tipos de DataModule: unos de lógica de datos, con componentes ClientDataSet; otros de acceso a datos, con componentes DataSet específicos para cada tecnología de acceso a datos. Y para mayor flexibilidad vamos a usar un tercer tipo de DataModule con componentes DataSetProvider.
Para obtener datos la ruta es la siguiente: se abre el ClientDataSet, que le pide datos al DataSetProvider enlazado, que le pide datos al DataSet enlazado. El DataSetProvider empaqueta los datos obtenidos y se los pasa al ClientDataSet.
Para guardar datos la ruta es la siguiente: el ClientDataSet guarda los cambios en un paquete especial (Delta Packet) y cuando se ejecuta el método ApplyUpdates le pasa dicho paquete al DataSetProvider que puede hacer dos cosas: generar sentencias SQL de tipo INSERT, UPDATE o DELETE saltándose los DataSet usados para obtener los datos o aplicar los cambios sobre dichos DataSet para que ellos mismos actualicen la base de datos.

Acceso a datos

Lo ideal será crear un DataModule de este tipo por cada tecnología de acceso a datos que usemos. Normalmente usamos una, pero al migrar nuestras aplicaciones de una versión a otra de Delphi muchas veces nos vemos obligados a cambiar la tecnología de acceso a datos usada.
En este tipo de DataModule nunca deberíamos implementar reglas de negocio ni crear campos persitentes (TField) Y si fuera posible deberíamos colocar las sentencias SELECT fuera del código fuente.

Lógica de datos

En este tipo de DataModule van los componentes ClientDataSet, tantos como sean necesarios.
Aquí sí podemos implementar reglas de negocio, pero sólo aquellas asociadas a los eventos de los campos (por ejemplo, OnChange) y de los DataSet (por ejemplo, BeforeDelete)

DataSetProvider

En este tipo de DataModule van los componentes DataSetProvider, que conectan los DataSet de los DataModule de acceso a datos con los ClientDataSet de los DataModule de lógica de datos.
El DataSetProvider tiene varios eventos que permiten manipular los datos que se obtienen de los DataSet enlazados antes de pasárselos al ClientDataSet y personalizar el proceso de actualización de los cambios recibidos desde el ClientDataSet.
Aquí también podemos implementar reglas de negocio, pero sólo aquellas asociadas al proceso de actualización en sí (por ejemplo, generar un consecutivo para el código de un cliente nuevo)

Descargar proyecto Delphi 2010

martes, 19 de diciembre de 2017

Desacoplamiento (interfaces)

Principios

Los cuatro principios fundamentales de la programación orientada a objetos son: encapsulación, herencia, polimorfismo y abstracción. No voy a comentar cada uno de ellos porque sería redundante. Ya hay mucha información al respecto en Internet.

Desacoplamiento

A estos cuatro principios le agregaría uno: desacoplamiento. Y la mejor forma de conseguirlo en nuestro código fuente es mediante el uso de interfaces para evitar hacer referencias directas entre clases (y entre las unidades en las que se implementan esas clases)

Ejemplo

Supongamos que tenemos un Form y un DataModule, es decir, una parte visual y una parte con datos. En el Form tenemos un DataSource que deberíamos enlazar con un DataSet del DataModule. Lo habitual es agregar la unidad del DataModule a la cláusula uses de la unidad del Form y enlazar el DataSource del Form con el DataSet del DataModule. Pero así estaríamos haciendo una referencia directa entre ambas clases, la del Form y la del DataModule, algo que sería mejor evitar.
¿Cómo lo evitamos?
Mediante el uso de interfaces.

El primer paso es declarar la interface en su propia unidad.

unit UMiDataModuleIntf;
...
IMiDataModule = interface(IInterface)
  function GetDataSet(const aNombre: string): TDataSet;
end;

El segundo paso es implementar la interface en la clase del DataModule.

unit DMMiDataModule;
...
TdtmdlMiDataModule = class(TDataModule, IMiDataModule)  { IMiDataModule }
  protected
  function GetDataSet(const aNombre: string): TDataSet;
  { IMiDataModule }end;
...
function TdtmdlMiDataModule.GetDataSet(const aNombre: string): TDataSet;
begin
  Result := nil;
  if SameText(aNombre, 'cdsMiDS') then
    Result := MiDS;
end;

Aquí asumo que en el DataModule "MiDataModule" hay un DataSet (por ejemplo, un ClientDataSet) llamado "cdsMiDS".

El tercer paso es registrar la clase del DataModule de alguna forma que nos permita evitar una referencia directa. Para simplificar y sólo con fines didácticos, usaré una unidad con una variable pública.

unit UMiDataModuleClss;
...
var
  oMiDataModuleClass: TComponentClass = nil;

Y en la sección Initialization de la unidad del DataModule registramos la clase.

unit DMMiDataModule;
...
uses
  UMiDataModuleClss;
...
initialization
  oMiDataModuleClass := TdtmdlMiDataModule; 

El cuarto y último paso es crear una instancia del DataModule en la unidad del Form para obtener el DataSet.

unit FMiForm;
...
uses
  UMiDataModuleClss, UMiDataModuleIntf;
...
procedure TfrmMiForm.OnCreate(Sender: TObject);
begin
  FMiDataModule := oMiDataModuleClass.Create(Self);
  dtsrcMiDS.DataSet := (FMiDataModule as IMiDataModule).GetDataSet('cdsMiDS');
end;

Esta implementación nos permite cambiar la unidad del DataModule sin tener que tocar nada del resto del código fuente porque el DataModule está totalmente desacoplado del Form. El único requisito es que la clase del DataModule debe implementar la interface IMiDataModule.

Descargar proyecto Delphi 2010