Mostrando entradas con la etiqueta lenguaje. Mostrar todas las entradas
Mostrando entradas con la etiqueta lenguaje. Mostrar todas las entradas

15 agosto 2008

Pasando de Delphi 7 a RAD Studio 2007 (6)

En el artículo de hoy vamos a terminar de ver las novedades que incorpora el lenguaje de programación Object Pascal.

LAS CLASES PUEDEN TENER AYUDANTES

Habéis leído bien, ahora las clases pueden tener ayudantes. ¿Qué es un ayudante? Los ayudantes (Helpers) permiten ampliar la funcionalidad de una clase sin tener que heredar de la misma. Con un ejemplo lo vais a ver más claro:

Supongamos que tenemos una clase para controlar el pago de un recibo:

type
TRecibo = class
private
Numero: Integer;
ImpTotal, ImpPagado, ImpPendiente: Real;
public
constructor Create( Cantidad: Real );
procedure Pagar( Cantidad: Real );
end;

Esta sería su implementación:

constructor TRecibo.Create( Cantidad: Real );
begin
ImpTotal := Cantidad;
end;

procedure TRecibo.Pagar( Cantidad: Real );
begin
ImpPagado := ImpPagado + Cantidad;
ImpPendiente := ImpTotal - ImpPagado;
end;

Si yo quisiera añadir un nuevo procedimiento o función a esta clase sin modificarla, debería heredar de la misma y añadirle las nuevas funcionalidades. Pero ahora podemos crear una nueva clase ayudante que permite ampliar funciones o procedimientos a la clase original de este modo:

TAyudanteRecibo = class helper for TRecibo
procedure Devolver( Cantidad: Real );
procedure Pagar( Cantidad: Real ); // vuelvo a redefinir el método Pagar
end;

Esta clase no es una clase normal, ya que no podemos por ejemplo añadir nuevas variables privadas (para eso hay que heredar). Pero si podemos implementar nuevos procedimientos tal como si estuvieran en la clase TRecibo:

procedure TAyudanteRecibo.Devolver( Cantidad: Real );
begin
ImpPagado := ImpPagado - Cantidad;
ImpPendiente := ImpTotal - ImpPagado;
end;

procedure TAyudanteRecibo.Pagar( Cantidad: Real );
begin
ImpPagado := ImpPagado + Cantidad;
ImpPendiente := ImpTotal - ImpPagado;

if ImpPagado >= ImpTotal then
begin
ImpPagado := ImpTotal;
ImpPendiente := 0;
end;
end;

Lo bueno de esto es que el programador que utilice esta clase no notará la diferencia a la hora de programar:

var
Recibo: TRecibo;
begin
Recibo := TRecibo.Create( 100 ); // constructor de la clase TRecibo
Recibo.Pagar( 50 ); // método de la clase TAyudanteRecibo
Recibo.Devolver( 10 ); // método de la clase TAyudanteRecibo
ShowMessage( 'Imp. pendiente=' + FloatToStr( Recibo.ImpPendiente ) );
Recibo.Free;
end;

Este sería el resultado de la operación:


De este modo cualquier programador puede ampliar las funcionalidades de una clase hechas por otro programador escribiendo menos código y ahorrándose la herencia.

LOS ATRIBUTOS ESTRICTOS

Hasta ahora teníamos tres formas de definir un atributo (variable) en una clase:

Private: Sólo se puede acceder al mismo en la misma clase. Ni siquiera las clases que heredan pueden acceder a estas variables.

Protected: Las variables que se encuentren en la sección protected podrán ser accesibles por la clase original y por sus herederas.

Public: Se podrá acceder a las variables públicas tanto en la clase original como en sus herederas así como desde fuera de la clase.

En RAD Studio 2007 ahora tenemos dos nuevos tipos de atributos:

Strict private: Sólo se pueden acceder a estos atributos mediante métodos de la misma clase y nunca de los que heredan de esta. Ni siquiera podemos acceder a estos atributos instanciando el objeto. Por ejemplo:

type
TJuego = class
private
NumeroVidas: Integer;
public
constructor Create;
end;

constructor TJuego.Create;
begin
NumeroVidas := 3;
end;

Así como está definido puedo acceder a la variable NumeroVidas desde el objeto:

var
Juego: TJuego;
begin
Juego := TJuego.Create;
Juego.NumeroVidas := 3;
Juego.Free;
End;

Pero si hago este cambio:

TJuego = class
strict private
NumeroVidas: Integer;
public
constructor Create;
end;

Entonces el compilador me da error en esta línea:

Juego.NumeroVidas := 3;

Es decir, una variable estricta sólo puede ser accesible dentro de la implementación de la clase y nunca desde su instancia (el objeto).

Ahora heredo de esta clase e intento acceder a la variable NumeroVidas:

TMatamarcianos = class( TJuego )
public
procedure QuitarVida;
end;

procedure TMatamarcianos.QuitarVida;
begin
Dec( NumeroVidas );
end;

Al compilar me daría error en la línea:

Dec( NumeroVidas );

Esta variable sólo puede ser utilizada por la implementación de la clase padre. Para solucionar esto tenemos esta otra:

Strict protected: Sólo se pueden acceder a estos atributos mediante métodos de la misma clase y de los que heredan de esta, pero nunca los objetos instanciados de ambas. Por ejemplo:

TJuego = class
strict protected
NumeroVidas: Integer;
public
constructor Create;
end;

TMatamarcianos = class( TJuego )
public
procedure QuitarVida;
end;

procedure TMatamarcianos.QuitarVida;
begin
Dec( NumeroVidas );
end;

Este último procedimiento si está permitido. Lo que no podemos es hacer esto:

var
Matamarcianos: TMatamarcianos;
begin
Matamarcianos := TMatamarcianos.Create;
Matamarcianos.NumeroVidas := 5; // me da error en esta línea
Matamarcianos.QuitarVida;
Matamarcianos.Free;
end;

Resumiendo lo que hemos visto podemos decir que la palabra reservada strict impide acceder a las variables de una clase desde el objeto instanciado.

LOS NUEVOS TIPOS DE CLASES

Mediante una clase cerrada (sealed) podemos evitar que se pueda heredar de la misma. Por ejemplo:

type
TEntidad = class sealed
private
Numero: Integer;
Nombre: String;
public
constructor Create;
procedure SetNombre( s: String );
end;

Si intento hacer esto:

TCliente = class( TEntidad )
private
CIF: String;
end;

El compilador me dirá este error:

Cannot extend sealed class ‘TEntidad’

De este modo creamos una clase hermética que no puede ser ampliada con clases descendentes de la misma. Sería algo así como una clase final.

NUEVOS TIPOS DE DATOS EN LAS CLASES

Otra nueva funcionalidad que tenemos dentro de las clases es definir constantes que pueden consultarse sin tener de instanciarse. Por ejemplo:

type
TProveedor = class
const TIPO = 'ACREEDOR';
end;

Ahora podemos hacer algo como esto sin que provoque un access violation:

ShowMessage( TProveedor.TIPO );

Esto es muy útil para crear nuestras propias constantes dentro de una clase de modo que moviendo la clase de una unidad a otra ya llevamos incorporadas las constantes que necesita.

También podemos definir nuestros propios tipos de datos dentro de una clase:

TPresupuesto = class
type
TNumeracion = record
Serie: Char;
Numero: Integer;
Fecha: TDate;
end;

private
Numeracion: TNumeracion;
public
constructor Create( S: Char; N: Integer; F: TDate );
end;

En la implementación del constructor rellenamos el registro Numeracion:

constructor TPresupuesto.Create( S: Char; N: Integer; F: TDate );
begin
Numeracion.Serie := S;
Numeracion.Numero := N;
Numeracion.Fecha := F;
end;

Esta sería la forma de utilizar la clase TPresupuesto:

var
Presupuesto: TPresupuesto;
begin
Presupuesto := TPresupuesto.Create( 'A', 25, Date );

ShowMessage( Presupuesto.Numeracion.Serie + ' ' +
IntToStr( Presupuesto.Numeracion.Numero ) + ' ' +
DateToSTr( Presupuesto.Numeracion.Fecha ) );

Presupuesto.Free;
end;

Mostrará el resultado con total normalidad:


Pero es que además podemos crear variables en la clase de modo que podamos utilizarlas sin ni siquiera instanciarla. Voy a hacer un pedido al igual que el ejemplo del presupuesto utilizando variables:

TPedido = class
type
TNumeracion = record
Serie: Char;
Numero: Integer;
Fecha: TDate;
end;

class var
Numeracion: TNumeracion;
end;

Ahora podemos asignar un valor a la variable Numeración sin instanciar la clase:

begin
TPedido.Numeracion.Serie := 'B';
TPedido.Numeracion.Numero := 48;
TPedido.Numeracion.Fecha := Date;

ShowMessage( TPedido.Numeracion.Serie + ' ' +
IntToStr( TPedido.Numeracion.Numero ) + ' ' +
DateToSTr( TPedido.Numeracion.Fecha ) );
end;

Estos valores permanecerán siempre estáticos para cualquier objeto de la clase:


Le podemos sacar partido a esto guardando en la clase el contador del último pedido y cuando instanciemos la clase incrementamos el contador. De este modo, todos los objetos siempre saben cual es el último pedido.

También podemos utilizarlo para guardar la configuración global de un programa como si se tratase de un registro (record).

Y por último y no menos importante, podemos crear métodos estáticos en la clases de modo que se pueden llamar sin crear una instancia. Por ejemplo:

TAlumno = class
class var
Nombre: String;
public
class procedure SetNombre( N: String );
class function GetNombre: String;
end;

En esta clase he definido una función y un procedimiento estático que recogen y almacenan la variable estática Nombre:

class procedure TAlumno.SetNombre( N: String );
begin
Nombre := N;
end;

class function TAlumno.GetNombre: String;
begin
Result := Nombre;
end;

Ahora viene lo más cómodo para un programador:

begin
TAlumno.SetNombre( 'JUAN' );
ShowMessage( TAlumno.GetNombre );
end;

Ni he tenido que crear la instancia del objeto ni destruirlo. Ni siquiera he tenido que declarar una variable para utilizarlo, ya que la clase TAlumno funciona como una variable global a todo el programa. ¿Cuántas veces hemos provocado access violations por olvidarnos de instanciar la clase? ¿Y cuantas pérdidas de memoria hemos tenido por olvidarnos del maldito Free? Esto no es que sea una panacea pero para ciertas rutinas es muy útil (incrementar contadores internos, acceder a las opciones del programa, etc.).

ANIDACION DE CLASES

Esto si supone una gran mejora para los que nos gusta estructurar bien la información. Ahora podemos definir una clase dentro de otra:

TAlmacen = class
private
Numero: Integer;
Descripcion: String;
public
type
TProducto = class
private
ID: Integer;
Nombre: String;
Existencias: Real;
public
constructor Create( I: Integer; N: String; E: Real );
end;

constructor Create( N: Integer; D: String );
end;

Esta sería la implementación de sus constructores:

constructor TAlmacen.Create( N: Integer; D: String );
begin
Numero := N;
Descripcion := D;
end;

constructor TAlmacen.TProducto.Create( I: Integer; N: String; E: Real );
begin
ID := I;
Nombre := N;
Existencias := E;
end;

La instanciación de ambas clases se haría de manera normal:

var
Almacen: TAlmacen;
Producto: TAlmacen.TProducto;
begin
Almacen := TAlmacen.Create( 1, 'ALMACEN GENERAL' );
Producto := TAlmacen.TProducto.Create( 1234, 'SOFA CAMA', 5 );
Producto.Free;
Almacen.Free;
end;

Esto hace que nuestro código fuente quede más elegante y con una jerarquía mejor definida.

LOS MÉTODOS FINALES

Si definimos un método como final ya no se podrá reimplementar en las clases descendentes:

type
TBiblioteca = class
private
NumLibros: Integer;
public
procedure CalcularExistencias; virtual; final;
end;

Al definir el prodecimiento CalcularExistencias como final ya no puedo reutilizarlo en las clases descendentes:

TLibreria = class( TBiblioteca )
private
NumArticulos: Integer;
public
procedure CalcularExistencias; override;
end;

El compilador me daría este error:

Cannot override a final method.

Esto tiene su utilidad para evitar que otro programador que herede de esta clase intente reimplementar un método crítico que es esencial en esa clase (reserva de memoria, apertura de archivos, etc.).

En el próximo artículo seguiremos viendo más cosas interesantes de RAD Studio 2007.

Pruebas realizadas con RAD Studio 2007.

08 agosto 2008

Pasando de Delphi 7 a RAD Studio 2007 (5)

Hoy vamos a comenzar a ver las novedades que incorpora RAD Studio 2007 en el lenguaje de programación Object Pascal respecto a Delphi 7.

REGISTROS CON MÉTODOS

Si hasta ahora no se le daba mucha importancia a los registros (record) a favor de las clases para crear y almacenar nuestros propios tipos de datos, ahora podemos aprovecharlos de nuevo con la posibilidad de añadir métodos como si fueran clases.

Y no sólo eso, sino que además se pueden tener su propio constructor e incluso métodos y propiedades. De este modo podemos encapsular tanto los datos así la manipulación de los mismos dentro de un mismo registro y además gozando de la ventaja de no tener que instanciar ni liberar la clase.

Veámoslo con un ejemplo. Supongamos que tenemos este registro para definir un artículo:

type
TArticulo = record
ID: Integer;
Nombre: String;
Existencias, Precio: Real;
end;

Pues ahora podemos hacer algo como esto:

TArticulo = record
var
ID: Integer;
Nombre: String;
Existencias, Precio: Real;

constructor Create( ExistenciasIniciales, Precio: Real );
end;

La palabra reservada var no es obligatoria, aunque deja el código más elegante. Aquí tenemos la implementación de su constructor:

constructor TArticulo.Create(ExistenciasIniciales, Precio: Real);
begin
Existencias := ExistenciasIniciales;
Self.Precio := Precio;
end;

De este modo podemos crear un artículo al igual que una clase sin instanciarlo:

var
Articulo: TArticulo;
begin
Articulo.Create( 10, 132.25 );
ShowMessage( 'Existencias=' + FloatToStr( Articulo.Existencias ) + #13 +
'Precio=' + FloatToStr( Articulo.Precio ) );
end;

Nos mostraría esto en pantalla:


También podemos crear propiedades que accedan a las variables encapsulando de este modo todo el registro:

type
TArticulo = record
var
ID: Integer;
Nombre: String;
Existencias, Precio: Real;
Comprado, Vendido: Real;

constructor Create( ExistenciasIniciales, Precio: Real );
procedure Comprar( Unidades: Real );
procedure Vender(Unidades: Real);
function CalcularExistencias: Real;
property ExistenciasReales: Real Read CalcularExistencias;
end;

Esta sería su implementación:

constructor TArticulo.Create(ExistenciasIniciales, Precio: Real);
begin
Existencias := ExistenciasIniciales;
Self.Precio := Precio;
Comprado := 0;
Vendido := 0;
end;

procedure TArticulo.Comprar(Unidades: Real);
begin
Comprado := Comprado + Unidades;
end;

procedure TArticulo.Vender(Unidades: Real);
begin
Vendido := Vendido + Unidades;
end;

function TArticulo.CalcularExistencias: Real;
begin
Result := Existencias + Comprado - Vendido;
end;

Ahora aprovechamos del registro Articulo como si fuera una clase:

var
Articulo: TArticulo;
begin
Articulo.Create( 10, 132.25 );
Articulo.Comprar( 100 );
Articulo.Vender( 50 );
ShowMessage( 'Existencias Reales=' + FloatToStr( Articulo.ExistenciasReales ) + #13 +
'Precio=' + FloatToStr( Articulo.Precio ) );
end;

Este sería el resultado:


Sin embargo, los registros todavía no gozan de muchas de las características que poseen las clases:

- Un registro no puede tener destructores.

- No se pueden crear métodos virtuales.

- No se puede heredar de otro registro.

LA SOBRECARGA DE OPERADORES EN REGISTROS

La sobrecarga de operadores nos permite operar con los registros como si fueran valores numéricos. Por ejemplo:

TFactura = record
private
Importe: Real;
public
class operator Implicit( Importe: Real ): TFactura;
class operator Add( F1, F2: TFactura ): TFactura;
class operator Subtract( F1, F2: TFactura ): TFactura;
end;

Las palabras reservadas class operator sirven para redefinir los operadores +, -, =, etc. que se utilizan al tratar con los registros. En esta ocasión sólo he redefinido los operadores de suma y resta para aplicarlos a la factura.

Esta sería su implementación:

class operator TFactura.Implicit( Importe: Real ): TFactura;
begin
Result.Importe := Importe;
end;

class operator TFactura.Add( F1, F2: TFactura ): TFactura;
begin
Result.Importe := F1.Importe + F2.Importe;
end;

class operator TFactura.Subtract( F1, F2: TFactura ): TFactura;
begin
Result.Importe := F1.Importe - F2.Importe;
end;

El operador Implicit se utiliza para que podamos asignar valores al registro directamente como si fuera un valor numérico:

var
F1, F2, F3: TFactura;
begin
F1 := 50;
F2 := 100;
F3 := 20;
F1 := F1 + F2 - F3;

ShowMessage( 'Total facturas=' + FloatToStr( F1.Importe ) );
end;

Como puede apreciarse en el código he asignado los valores directamente al objeto en vez de hacer esto:

F1.Importe := 50;
F2.Importe := 100;
F3.Importe := 20;

Esto me lo ha permitido el operador Implicit. Luego he sumado las dos primeras facturas y les he restado la tercera:

F1 := F1 + F2 - F3;

Que equivale a esto:

F1.Importe := F1.Importe + F2.Importe – F3.Importe;

Al ejecutar este código este es el resultado:


Esto nos da mucha potencia a la hora de crear registros para operar con facturas, recibos, calcular saldos, asientos contables, etc.

LOS BUCLES FOR IN

Esto es algo que ya se echaba de menos respecto a otros lenguajes más modernos que Pascal, como pueden ser Java, Python, Ruby, C#, etc. Es la posibilidad de poder crear un bucle sin tener que crear una variable entera para recorrerlo.

Por ejemplo, si tengo que recorrer un array y sumar su resultado esto es lo que tenía que hacer hasta ahora:

var
Facturas: array[1..3] of Real;
Suma: Real;
i: Integer;
begin
Facturas[1] := 10;
Facturas[2] := 20;
Facturas[3] := 30;

for i := 1 to 3 do
Suma := Suma + Facturas[i];

ShowMessage( 'Suma=' + FloatToStr( Suma ) );
end;

Con este método tenemos que saber el número de elementos en el array. Ahora, mediante el bucle for in podemos recorrer un array sin tener que saber cuantos elementos contiene:

var
Facturas: array[1..3] of Real;
Suma, Factura: Real;
begin
Facturas[1] := 10;
Facturas[2] := 20;
Facturas[3] := 30;

for Factura in Facturas do
Suma := Suma + Factura;

ShowMessage( 'Suma=' + FloatToStr( Suma ) );
end;

En este caso ya no es necesaria la variable entera i para recorrer los elementos del bucle. Sólo hace falta una variable que contenga el elemento actual (Factura). Esto es muy útil para recorrer arrays dinámicos sin preocuparse en el número de elementos que tienen.

Aunque esto lo considero algo incompleto, porque no se puede hacer algo como esto:

var
Componente: TComponent;
begin
for Componente in Form1.Components do
ShowMessage( Componente.Name );
end;

Al compilarlo nos da este error:

‘[‘ Expected but ‘DO’found

Es decir, que esperaba los corchetes después de la palabra Components. Si no podemos utilizar un for in para este tipo de elementos la verdad es que nos quedamos casi igual que antes. Si sólo podemos utilizarlo en arrays y en colecciones simples la verdad es que se nos queda algo corto.

LA DIRECTIVA INLINE

Esta directiva puede aplicarse a funciones o procedimientos modificando el modo de compilación de los mismos. Vamos a verlo con un pequeño ejemplo.

Este es el típico bucle para ordenar los elementos de un array por el método de la burbuja:

var
i, j, tmp: Integer;
Elementos: array[1..10000] of Integer;
Tiempo: Dword;
begin
// Rellenamos el array con valores aleatorios

Randomize;
for i := 1 to 100 do
Elementos[i] := Random( 500 ) + 1;

Tiempo := TimeGetTime;

// Ordenamos los elementos del array con el método de la burbuja

for j := 1 to 9999 do
for i := 1 to 9999 do
if Elementos[i] > Elementos[i+1] then
begin
tmp := Elementos[i];
Elementos[i] := Elementos[i+1];
Elementos[i+1] := tmp;
end;

ShowMessage( 'Milisegundos=' + IntToStr( TimeGetTime - Tiempo ) );
end;

Para poder utilizar la función TimeGetTime es necesario añadir en uses la unidad mmsystem. Lo que he hecho es crear un array de 10000 números enteros y les he metido a cada uno un valor aleatorio entre 1 y 500. Después he utilizado el método de la burbuja para ordenarlos. Al ejecutar el programa me tarda 155 milisegundos:


Ahora supongamos que quiero refactorizar el código y quiero sacar a parte el núcleo del bucle en otro procedimiento;

procedure Intercambiar(var a, b: Integer);
var
tmp: Integer;
begin
tmp := a;
a := b;
b := tmp;
end;

De modo que el procedimiento original se nos quedaría así:

var
i, j: Integer;
Elementos: array[1..10000] of Integer;
Tiempo: Dword;
begin
// Rellenamos el array con valores aleatorios

Randomize;
for i := 1 to 100 do
Elementos[i] := Random( 500 );

Tiempo := TimeGetTime;

// Ordenamos los elementos del array con el método de la burbuja
for j := 1 to 9999 do
for i := 1 to 9999 do
if Elementos[i] > Elementos[i+1] then
Intercambiar( Elementos[i], Elementos[i+1] );

ShowMessage( 'Milisegundos=' + IntToStr( TimeGetTime - Tiempo ) );
end;

Al ejecutar el programa vemos que tarda más:


Esto tiene su explicación: cada llamada al procedimiento Intercambiar obliga a realizar un salto a otro procedimiento para luego volver a este. Si el bucle no tiene muchas iteraciones el cambio no suele notarse mucho, pero si se realiza miles de veces entonces la diferencia de tiempo entre uno y otro método suele ser notable.

Para solucionar esto tenemos la directiva inline. Esta directiva aplicada al procedimiento Intercambiar hace que se compile el código de este procedimiento como si realmente nunca lo hubiésemos sacado del bucle. De este modo ganamos claridad en el código fuente sin perder rendimiento en el código compilado.

Sólo hay que añadir la palabra inline al final del procedimiento:

procedure Intercambiar(var a, b: Integer); inline;
var
tmp: Integer;
begin
tmp := a;
a := b;
b := tmp;
end;

Sin ser tan rápido como la manera original si se obtiene un aumento de velocidad:



Quizás en un programa de gestión no es necesario utilizar esta función, pero en la programación de videojuegos algunas funciones se ejecutan tantas veces por segundo que puede marcar la diferencia entre renderizar un mundo 3D con suavidad a 40 o 50 frames por segundo o relentizarse y dar tirores a 20 frames por segundo. Cuando escriba en un futuro articulos de programación de videojuegos 3D con SDL y OpenGL veréis a que me refiero.

En el próximo artículo seguiremos viendo otras novedades en el lenguaje.

Pruebas realizadas en RAD Studio 2007.

18 abril 2008

Formateando código automáticamente con DelForExp

Esta es otra herramienta que con el tiempo se me ha ido haciendo imprescindible junto con Delphi. DelForExp es un experto (y gratuito) que se puede instalar en cualquier versión de Delphi y que formatea el código fuente automáticamente según nuestras preferencias.

Con esta utilidad ya no será necesario utilizar las combinaciones de teclas CTRL + K + I y CTRL + K + U para identar las líneas de código cada vez que copiamos un trozo de código de un lugar a otro. Otro de los “desastres” que suele solucionar es cuando se trabaja en equipo y cada programador escribe el código a su manera (los begin, end, los espacios, los paréntesis, etc.).

Para descargar esta interesante utilidad hay que irse a la siguiente dirección:

http://www.aew.wur.nl/UK/Delforexp/

La instalación va comprimida en un zip de 564 KB. Al no llevar realmente una instalación como Dios manda hay que descomprimirlo en alguna carpeta donde sepamos que no se va a borrar (ya que contiene librerías DLL que se van a enganchar a Delphi como lapas). Después ejecutamos el archivo SetupEx.exe y comenzará a instalarse en todos los Delphi que tengamos instalados:

Es una instalación algo cutre pero efectiva, ya que si abrimos Delphi y miramos en el menú superior la opción Tools aparecerá una opción nueva llamada Source Formatter:

Al ejecutar esta opción nos aparece esta ventana:

Mucho cuidado de pulsar los primeros tres botones (Current File, All Open Files y Whole Project) antes de establecer el tipo de formateo de texto. Antes de hacer experimentos recomiendo hacer una copia de seguridad de nuestro proyecto, sobre todo para evitar sustos.

ESTABLECIENDO EL FORMATO DE TEXTO

Para seleccionar nuestro propio estilo de programación debemos pulsar el botón Options:

En la primera pestaña (ident) establecemos los espacios que vamos a meter para identar nuestro código. Lo ideal es dejar el campo Number of spaces per ident a 2, que es lo estándar en Delphi. También es conveniente identar los comentarios mediante la opción Ident comments para que la alineación de nuestros comentarios no se quede descolocada respecto al código.

Igual se puede hacer con las directivas del compilador, los bloques de código después del comando else, etc. Lo mejor es dejarlo como se ve en la foto que he mostrado, con los comentarios es suficiente.

En la siguiente pestaña (Spacing) le indicamos cuantos espacios queremos alrededor de los símbolos (paréntesis, comas, etc.):

A mí por ejemplo me gusta que deje un espacio después de abrir un paréntesis y otro antes de cerrarlo:

procedure TForm1.FormCreate( Sender: TObject );

Para ello debemos seleccionar en la línea Left Parenthesis el valor Alter only (sólo después) y en la línea Right Parenthesis el valor Befote Only (sólo antes). También se puede hacer lo mismo con los corchetes.

En la tercera pestaña (Line breaks) le podemos indicar cuando debe hacer un Intro para pasar a la línea siguiente. En el siguiente ejemplo lo he puesto para procedimientos y funciones:

La cuarta pestaña (Capitalization) se utiliza para pasar a mayúsculas la primera letra de las palabras reservadas o comandos:

Por defecto, están ambas opciones desactivadas. Si activamos la opción Reserved words pasará a mayúsculas la primera letra de todas la palabras reservadas (While, Begin, etc.).

La quinta pestaña (Align) se utiliza para alinear los comentarios a una línea que solemos poner detrás de nuestro código para dejarlos alineados:

Por ejemplo, en este bloque de código hay comentarios detrás de cada línea:

procedure TFPrincipal.Inicializar( Sender: TObject; var Done: Boolean );
begin
Done := True; // El proceso se ha realizado correctamente
Application.OnIdle := nil; // Liberamos el uso de la aplicación
Liberar_Memoria; // Liberamos memoria
Bloquear_Ventana( True ); // Impedimos que usuario modifique los datos
end;

Como puede verse los comentarios quedan sin alinear respecto al código. Para corregir ese problema activo la opción Aling simple comments alter code con el valor 40 y así quedaría al formatear el código:

procedure TFPrincipal.Inicializar( Sender: TObject; var Done: Boolean );
begin
Done := True; // El proceso se ha realizado correctamente
Application.OnIdle := nil; // Liberamos el uso de la aplicación
Liberar_Memoria; // Liberamos memoria
Bloquear_Ventana( True ); // Impedimos que usuario modifique los datos
end;

Como puede verse el código queda mucho más elegante y nos evita machacar la barra de espacio o el tabulador.

La sexta pestaña (Misc.) se utiliza para dar formateo a las directivas del compilador. También podemos establecer qué combinación de teclas vamos a utilizar para formatear el código fuente de la unidad que estamos tocando en ese momento (que por defecto es la combinación de teclas CTRL + D):

Y en la última pestaña (Preview) podemos ir viendo en tiempo real cómo quedaría el código fuente según apliquemos unas opciones u otras:

También podemos pulsar el botón Borland Style para que lo deje al estilo clásico de Borland (por si la hemos fastidiado con alguna opción y no sabemos dejarlo como estaba).

Después de haber establecido nuestro estilo de código, ahora podemos aplicarlo pulsando los botones:

Current File -> Formatea el código fuente de la unidad donde estamos situados.

All Open Files -> Formatea el código de todas las pestañas de código que tengamos abiertas en el proyecto.

Whole Proyect -> Formatea el código de todas las unidades del proyecto.

Como he dicho anteriormente, antes de pulsar los botones All Open files o Whole Proyect sería aconsejable hacer copia de seguridad del proyecto (o por lo menos de los archivos .pas).

De todas formas, llevo más de una año utilizando esta herramienta y jamás me ha jodido el código fuente o me ha provocado ningún problema. Es fiable 100%.

Cuando estemos escribiendo código fuente y queramos formatear el código sólo hay que pulsar la combinación de teclas CTRL + D (aparecerá la ventana de DelForExp) y luego la tecla Intro (ya que estará enfocado el botón Current File).

Con herramientas como esta se ahorra mucho tiempo y además se puede compartir el código fuente con otros usuarios sin que hayan quejas sobre los estilos de escritura de cada programador.

Pruebas realizadas en Delphi 7.

18 octubre 2007

Implementando interfaces en Delphi (y III)

Una de las propiedades más interesantes que incorporan las interfaces es la de añadir un identificador único que la diferencie del resto.

IDENTIFICACION DE INTERFACES


Una interfaz puede tener un identificador unico a nivel global llamado GUID. Este identificador tiene la siguiente forma:

['{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}']

donde cada X es un dígito hexadecimal. Cada GUID es un valor binario de 16 bytes que hace que cada interfaz sea única. Los tipos TGUID y PGUID están declarados dentro de la unidad System del siguiente modo:

type
PGUID = ^TGUID;
TGUID = record
D1: Integer;
D2: Word;
D3: Word;
D4: array[0..7] of Byte;

Este sería un ejemplo de declaración de interfaz con GUID:

type
IFactura = interface
['{78EAC667-ED60-443B-B84B-5D7AF161B28B}']
procedure Calcular;
end;

¿De donde sacamos ese código? Pues cuando en el editor de Delphi se pulsa la combinación de teclas CTRL + MAYÚSCULAS + G entonces nos genera una GUID aleatoria.

CREANDO PROPIEDADES EN LA INTERFAZ

Pese que la declarión de interfaces es similar a la de las clases hay que tener varias cosas presentes:

- No permiten declarar variables internas.

- No pueden ser instanciadas.

- No se permite especificar la visibilidad de los métodos (private, protected o public).

- Las propiedades no pueden apuntar a variables sino sólo a métodos.

Entonces, ¿como podemos definir propiedades? Hay que definirlas utilizando métodos en vez de variables cuando se definen los parámetros read y write de la propiedad. Sería de este modo:

type
IAlbaran = interface
function Get_Numero: Integer;
function Get_Importe: Real;
procedure Set_Numero( iNumero: Integer );
procedure Set_Importe( rImporte: Real );
procedure Imprimir;

property Numero: Integer read Get_Numero write Set_Numero;
property Importe: Real read Get_Importe write Set_Importe;
end;

y su implementación a través de la clase:

TAlbaran = class( TInterfacedObject, IAlbaran )
private
iNumero: Integer;
rImporte: Real;

public
function Get_Numero: Integer;
function Get_Importe: Real;
procedure Set_Numero( iNumero: Integer );
procedure Set_Importe( rImporte: Real );
procedure Imprimir;

property Numero: Integer read Get_Numero write Set_Numero;
property Importe: Real read Get_Importe write Set_Importe;
end;

implementation

{ TAlbaran }

function TAlbaran.Get_Importe: Real;
begin
Result := rImporte;
end;

function TAlbaran.Get_Numero: Integer;
begin
Result := iNumero;
end;

procedure TAlbaran.Imprimir;
begin
ShowMessage( 'Imprimiendo...' );
end;

procedure TAlbaran.Set_Importe( rImporte: Real );
begin
Self.rImporte := rImporte;
end;

procedure TAlbaran.Set_Numero( iNumero: Integer );
begin
Self.iNumero := iNumero;
end;

Esto nos permite utilizar nuestra interfaz sin llamar a los procedimientos Set y Get de cada campo:

var
Albaran: IAlbaran;
begin
Albaran := TAlbaran.Create;
Albaran.Numero := 1;
Albaran.Importe := 68.21;

ShowMessage( 'Numero=' + IntToStr( Albaran.Numero ) );
ShowMessage( 'Importe=' + FloatToStr( Albaran.Importe ) );
end;

Con esta comodidad se pueden instanciar clases a partir de interfaces manteniendo la claridad de código tanto dentro como fuera de la implementación.

Con esto finalizamos la parte básica de implementación de interfaces en Delphi.

Pruebas realizadas en Delphi 7.

17 octubre 2007

Implementando interfaces en Delphi (II)

Cuando creamos clases para nuestros programas de gestión uno se pregunta que ventajas pueden aportar las interfaces dentro de nuestro programa. Pues una de las ventajas es que encapsula aún más nuestra clase dejando sólo accesible los métodos y no las variables internas (ya sean private, protected o public). Por ejemplo, supongamos que deseo implementar una interfaz y una clase para el control de mis clientes:

type
ICliente = interface
function Get_ID: Integer;
function Get_Nombre: string;
function Get_NIF: string;
function Get_Saldo: Real;
procedure Set_ID( ID: Integer );
procedure Set_Nombre( sNombre: string );
procedure Set_NIF( sNIF: string );
procedure Set_Saldo( rSaldo: Real );
end;

TCliente = class( TInterfacedObject, ICliente )
private
ID: Integer;
sNombre, sNIF: string;
rSaldo: Real;
public
function Get_ID: Integer;
function Get_Nombre: string;
function Get_NIF: string;
function Get_Saldo: Real;
procedure Set_ID( ID: Integer );
procedure Set_Nombre( sNombre: string );
procedure Set_NIF( sNIF: string );
procedure Set_Saldo( rSaldo: Real );
end;

Esta clase se puede instanciar de dos formas. Si se hace en una variable de clase:

var
Cliente: TCliente;
begin
Cliente := TCliente.Create;
Cliente.Set_ID( 1 );
Cliente.Set_Nombre( 'CARLOS MARTINEZ RUIZ' );
Cliente.Set_NIF( '67876453F' );
Cliente.Set_Saldo( 145.87 );
end;

entonces se comporta como una clase normal, permitiendo acceder incluso a las variables privadas cuando en el editor de Delphi ponemos:

Cliente.

y esperamos a que el asistente nos muestre las propiedades y métodos de la clase. Y no sólo eso, sino que además nos muestra todas propiedades y métodos que hemos heredado de TObject haciendo más engorrosa la búsqueda de variables y métodos.

Pero si implementamos la clase a partir de la interfaz:

var
Cliente: ICliente;
begin
Cliente := TCliente.Create;
Cliente.Set_ID( 1 );
Cliente.Set_Nombre( 'CARLOS MARTINEZ RUIZ' );
Cliente.Set_NIF( '67876453F' );
Cliente.Set_Saldo( 145.87 );
end;

al teclear en el editor de Delphi:

Cliente.

no sólo hemos eliminado las variables privadas de la lista, sino que además sólo muestra nuestros métodos y los de las interfaz (QueryInterface, _AddRef y _Release). Esto aporta mucha mayor claridad al programador a la hora de instanciar sus clases, sobre todo cuando tenemos que distribuir nuestra librería a otros programadores (ellos sólo tienen que fijarse como se utiliza la interfaz, ni siquiera tienen porque saber como está implementada la clase).

La única desventaja es que hay que crear tantas funciones y procedimientos Set y Get como propiedades tenga nuestra clase. Pero es bueno acostumbrarse a hacerlo así como ocurre con los Beans de Java.

USANDO INTERFACES COMO PARAMETROS EN PROCEDIMIENTOS

Utilizando el polimorfismo mediante interfaces ahora podemos crear procedimientos genéricos que manejen objetos de distinta clase de una manera simple. Usando las interfaces IArticulo e IVehiculo definidas en artículo anterior podemos escribir los siguientes procedimientos:

procedure FacturarArticulos( Articulos: array of IArticulo );
var
i: Integer;
begin
for i := Low( Articulos ) to High( Articulos ) do
Articulos[i].Facturar;
end;

procedure MatricularVehiculos( Vehiculos: array of IVehiculo );
var
i: Integer;
begin
for i := Low( Vehiculos ) to High( Vehiculos ) do
Vehiculos[i].Matricular;
end;

El procedimiento FacturarArticulos no tiene porque saber como se factura internamente cada artículo. Lo mismo ocurre con el procedimiento MatricularVehiculos, ya sea de la clase TCoche o TCamion ya se encargará internamente el objeto de llamar a su método correspondiente, abstrayéndonos a nosotros de como funciona internamente cada clase.

LA INTERFAZ IINTERFACE

Al igual que todos los objetos heredan directa o indirectamente de TObject, todas las interfaces heredan de la interfaz IInterface. Esta interfaz incorpora el método QueryInterface el cual es muy útil para descubrir y usar otras interfaces que implementan el mismo objeto.

Para contar el número de referencias introduce también los métodos _AddRef y _Release. El compilador de Delphi automáticamente proporciona llamadas a estos métodos cuando las interfaces son utilizadas. Para no tener que implementar nosotros a mano estos métodos (ya que una interfaz nos obliga a implementar todos sus métodos), para ello heredamos de la clase TInterfaceObject que proporciona una implementación base para interfaces. Heredar de TInterfaceObject no es obligatorio pero muy útil.

La clase TInterfacedObject está declarada en la unidad System de la siguiente manera:

type
TInterfacedObject = class ( TObject, IInterface )
protected
FRefCount: Integer;
function QueryInterface( const IID: TGUID; out Obj ): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
class function NewInstance: TObject; override;
property RefCount: Integer; read FRefCount;
end;

Por ello en los ejemplos que he mostrado heredo de esa clase aparte de la interfaz:

type
TCliente = class( TInterfacedObject, ICliente )
...

Como puede apreciarse la clase TInterfacedObject hereda de la clase TObject y de la interfaz IUnknown. La interfaz IUnknown es utilizada para crear objetos COM.

En el próximo artículo terminaremos de ver las características más importantes de las interfaces.

Pruebas realizadas en Delphi 7.

15 octubre 2007

Implementando interfaces en Delphi (I)

Delphi es un lenguaje que utiliza la herencia simple al contrario de C++ que permite herencia múltiple. Esto significa que cualquier clase sólo puede heredar de una clase padre. Por lo tanto, si queremos que una clase herede métodos de más de una clase entonces hay que utilizar interfaces (interface).

Una interfaz es como una clase que contiene sólo métodos abstractos (métodos sin implentación) definiendo limpiamente su funcionalidad. Por convención, los nombres de las interfaces comienzan con la letra mayúscula I.

Veamos un ejemplo de definición de interfaz para un artículo:

type
IArticulo = interface
procedure IncrementarExistencias( rCantidad: Real );
procedure DecrementarExistencias( rCantidad: Real );
function Facturar: Integer;
end;

Las interfaces nunca pueden ser instanciadas. Es decir, no se puede hacer:

var
Articulo: IArticulo;
begin
Articulo := IArticulo.Create;
end;

Al compilar nos daría el error:

Object or class typed required (se requiere una clase u objeto)

Para utilizar una interfaz necesitamos implementarla a través de una clase. Se haría de la siguiente manera:

type
TArticulo = class( TInterfacedObject, IArticulo )
procedure IncrementarExistencias( rCantidad: Real );
procedure DecrementarExistencias( rCantidad: Real );
function Facturar: Integer;

¿Que ventajas aporta esto respecto a una clase abstracta? Pues en este caso la definición es más limpia, nada más. Ahora veremos que ventaja de utilizar interfaces.

UTILIZANDO EL POLIMORFIRMO PARA LA CREACION DE CLASES

Las clases polimórficas son aquellas clases que teniendo una definición común se pueden utilizar para distintos tipos de objeto. Supongamos que tenemos la siguiente interfaz:

IVehiculo = interface
procedure Matricular;
end;

Utilizando la misma interfaz vamos a definir una clase para cada tipo de vehículo:

type
TCoche = class( TInterfacedObject, IVehiculo )
procedure Matricular;
end;

TCamion = class( TInterfacedObject, IVehiculo )
procedure Matricular;
end;

Ahora instanciamos ambas clases utilizando la misma interfaz:

var
Vehiculo: IVehiculo;
begin
Vehiculo := TCoche.Create;
Vehiculo.Matricular;
Vehiculo := TCamion.Create;
Vehiculo.Matricular;
end;

En este caso todas las clases que implementan la interfaz IVehiculo tienen un método común pero podría darse el caso de que tuvieran métodos distintos, lo cual significa que hay que utilizar la herencia múltiple.

UTILIZANDO HERENCIA MULTIPLE MEDIANTE INTERFACES

Supongamos que tenemos estas dos interfaces:

type
IVehiculo = interface
procedure Matricular;
end;

IRemolque = interface
procedure Cargar;
end;

El vehículo tiene el método Matricular y el remolque tiene el método Cargar. A mi me interesa que un coche utilice el método Matricular, pero sólo el camión tiene que poder Cargar. Entonces la implementación de las clases de haría así:

TCoche = class( TInterfacedObject, IVehiculo )
procedure Matricular;
end;

TCamion = class( TInterfacedObject, IVehiculo, IRemolque )
procedure Matricular;
procedure Cargar;
end;

Como puede apreciarse la clase TCoche sólo implementa la interfaz IVehiculo pero la clase TCamion hereda implementa simultáneamente las interfaces IVehiculo y IRemolque pudiendo utilizar sus métodos indistintamente. Esto es lo que se conoce como herencia múltiple.

En el próximo artículo seguiremos viendo más características sobre las interfaces.

Pruebas realizadas en Delphi 7.

12 octubre 2007

Trabajando con documentos XML (y III)

Aunque es posible trabajar con un documento XML usando solamente el componente XMLDocument y la interfaz IXMLNode es mucho más fácil utilizar el asistente XML Data Binding.

Dicho asistente toma los datos del esquema de un archivo XML a través de su archivo asociado DTD y realiza una nueva unidad con una interfaz que maneja nuestro documento XML sin tener que navegar por los nodos.

CREANDO UNA INTERFAZ PARA CLIENTES.XML

Para crear una unidad nueva para nuestro documento clientes.xml hay que hacer lo siguiente:

1. Seleccionamos en Delphi la opción File -> New -> Other...

2. Estando situados en la pestaña New seleccionamos el icono XML Data Binding y pulsamos OK.


3. En la ventana siguiente seleccionamos el archivo cli.dtd y pulsamos Next.


4. Después nos aparece otra ventana donde se nos permite modificar el nombre de la clase a crear así como los nombres que van tener las propiedades y métodos de la nueva clase. Lo dejamos como está y pulsamos Next.



5. La última pantalla del asistente nos muestra como va a ser definitivamente la interfaz que va a crear. Sólo nos queda pulsar Finish.



Va a generar la siguiente unidad:

{**************************************************************************}
{ }
{ XML Data Binding }
{ }
{ Generated on: 11/10/2007 11:15:57 }
{ Generated from: D:\Desarrollo\DelphiAlLimite\XML\cli.dtd }
{ Settings stored in: D:\Desarrollo\DelphiAlLimite\XML\cli.xdb }
{ }
{**************************************************************************}

unit UInterfazClientes;

interface

uses xmldom, XMLDoc, XMLIntf;

type

{ Forward Decls }

IXMLClientesType = interface;
IXMLClienteType = interface;

{ IXMLClientesType }

IXMLClientesType = interface(IXMLNodeCollection)
['{A21F5A80-EBA7-48F5-BFE1-EB9F390DFBBD}']
{ Property Accessors }
function Get_Cliente(Index: Integer): IXMLClienteType;
{ Methods & Properties }
function Add: IXMLClienteType;
function Insert(const Index: Integer): IXMLClienteType;
property Cliente[Index: Integer]: IXMLClienteType read Get_Cliente; default;
end;

{ IXMLClienteType }

IXMLClienteType = interface(IXMLNode)
['{624B6C2E-4E94-4CE0-A8D4-FE719AA7D975}']
{ Property Accessors }
function Get_Nombre: WideString;
function Get_Nif: WideString;
function Get_Saldopte: WideString;
function Get_Diaspago: WideString;
procedure Set_Nombre(Value: WideString);
procedure Set_Nif(Value: WideString);
procedure Set_Saldopte(Value: WideString);
procedure Set_Diaspago(Value: WideString);
{ Methods & Properties }
property Nombre: WideString read Get_Nombre write Set_Nombre;
property Nif: WideString read Get_Nif write Set_Nif;
property Saldopte: WideString read Get_Saldopte write Set_Saldopte;
property Diaspago: WideString read Get_Diaspago write Set_Diaspago;
end;

{ Forward Decls }

TXMLClientesType = class;
TXMLClienteType = class;

{ TXMLClientesType }

TXMLClientesType = class(TXMLNodeCollection, IXMLClientesType)
protected
{ IXMLClientesType }
function Get_Cliente(Index: Integer): IXMLClienteType;
function Add: IXMLClienteType;
function Insert(const Index: Integer): IXMLClienteType;
public
procedure AfterConstruction; override;
end;

{ TXMLClienteType }

TXMLClienteType = class(TXMLNode, IXMLClienteType)
protected
{ IXMLClienteType }
function Get_Nombre: WideString;
function Get_Nif: WideString;
function Get_Saldopte: WideString;
function Get_Diaspago: WideString;
procedure Set_Nombre(Value: WideString);
procedure Set_Nif(Value: WideString);
procedure Set_Saldopte(Value: WideString);
procedure Set_Diaspago(Value: WideString);
end;

{ Global Functions }

function GetClientes(Doc: IXMLDocument): IXMLClientesType;
function LoadClientes(const FileName: WideString): IXMLClientesType;
function NewClientes: IXMLClientesType;

const
TargetNamespace = '';

implementation

{ Global Functions }

function GetClientes(Doc: IXMLDocument): IXMLClientesType;
begin
Result := Doc.GetDocBinding('Clientes', TXMLClientesType, TargetNamespace) as IXMLClientesType;
end;

function LoadClientes(const FileName: WideString): IXMLClientesType;
begin
Result := LoadXMLDocument(FileName).GetDocBinding('Clientes', TXMLClientesType, TargetNamespace) as IXMLClientesType;
end;

function NewClientes: IXMLClientesType;
begin
Result := NewXMLDocument.GetDocBinding('Clientes', TXMLClientesType, TargetNamespace) as IXMLClientesType;
end;

{ TXMLClientesType }

procedure TXMLClientesType.AfterConstruction;
begin
RegisterChildNode('Cliente', TXMLClienteType);
ItemTag := 'Cliente';
ItemInterface := IXMLClienteType;
inherited;
end;

function TXMLClientesType.Get_Cliente(Index: Integer): IXMLClienteType;
begin
Result := List[Index] as IXMLClienteType;
end;

function TXMLClientesType.Add: IXMLClienteType;
begin
Result := AddItem(-1) as IXMLClienteType;
end;

function TXMLClientesType.Insert(const Index: Integer): IXMLClienteType;
begin
Result := AddItem(Index) as IXMLClienteType;
end;

{ TXMLClienteType }

function TXMLClienteType.Get_Nombre: WideString;
begin
Result := ChildNodes['nombre'].Text;
end;

procedure TXMLClienteType.Set_Nombre(Value: WideString);
begin
ChildNodes['nombre'].NodeValue := Value;
end;

function TXMLClienteType.Get_Nif: WideString;
begin
Result := ChildNodes['nif'].Text;
end;

procedure TXMLClienteType.Set_Nif(Value: WideString);
begin
ChildNodes['nif'].NodeValue := Value;
end;

function TXMLClienteType.Get_Saldopte: WideString;
begin
Result := ChildNodes['saldopte'].Text;
end;

procedure TXMLClienteType.Set_Saldopte(Value: WideString);
begin
ChildNodes['saldopte'].NodeValue := Value;
end;

function TXMLClienteType.Get_Diaspago: WideString;
begin
Result := ChildNodes['diaspago'].Text;
end;

procedure TXMLClienteType.Set_Diaspago(Value: WideString);
begin
ChildNodes['diaspago'].NodeValue := Value;
end;

end.

UTILIZANDO LA NUEVA INTERFAZ PARA CREAR DOCUMENTOS XML

La unidad creada por el asistente la he guardado con el nombre UInterfazClientes.pas lo que significa que hay que introducirla en la sección uses del formulario donde vayamos a utilizarla.

Vamos a ver como damos de alta un cliente utilizando dicha interfaz:

var
Clientes: IXMLClientesType;
begin
XML.FileName := ExtractFilePath( Application.ExeName ) + 'clientes.xml';
XML.Active := True;
Clientes := GetClientes( XML );

with Clientes.Add do
begin
Attributes['id'] := '3';
Nombre := 'FERNANDO RUIZ BERNAL';
Nif := '58965478T';
Saldopte := '150.66';
Diaspago := '45';
end;

XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes5.xml' );
end;

He cargado el documento XML como siempre y utilizando la interfaz IXMLClientesType cargo la lista de clientes del documento clientes.xml. Después doy de alta un cliente y grabo el documento como clientes5.xml.

Una cosa cosa interesante que se puede hacer es activar la propiedad doAutoSave que está dentro de la propiedad Options del componente XMLDocument lo que implica que el documento se guardará automáticamente conforme vayamos modificando los elementos del documento XML.

La ventaja de convertir nuestro documento XML en una interfaz persistente es que ahora podemos manejar la lista de clientes como si fuera una base de datos utilizando los métodos get y set de cada uno de los campos así como las rutinas para alta, baja o modificación de elementos dentro del documento. Sólo es cuestión de echarle una mirada a la nueva unidad creada para ver las posibiliades de la misma.

Pruebas realizadas en Delphi 7.

11 octubre 2007

Trabajando con documentos XML (II)

Anteriormente vimos que cada documento XML podía llevar asociado un documento DTD. Los documentos DTD (Document Type Definition) definen la estructura y la sintaxis de un documento XML. Su misión es mantener la consistencia entre los elementos del documento XML para que todos los documentos de la misma clase mantengan un mismo estilo.

Nuestro documento cli.dtd va a ser el siguiente:

Veamos que hace cada línea:

!ELEMENT Clientes (Cliente*)

Aquí establecemos que el documento XML se va a llamar Clientes y que cada elemento se llamará Cliente. El asterístico significa que va a tener cero o más elementos de tipo Cliente. Si quisieramos que el nodo Cliente apareciese una o más veces utilizaríamos el caracter +.

!ELEMENT Cliente (nombre, nif, saldopte?, diaspago?)

La segunda línea nos dice de que campos está compuesto el nodo Cliente: nombre, nif, saldopte y diaspago. Todos los campos que no lleven el signo de interrogación son obligatorios (en este caso nombre y nif).

!ELEMENT nombre (#PCDATA)
!ELEMENT nif (#PCDATA)
!ELEMENT saldopte (#PCDATA)
!ELEMENT diaspago (#PCDATA)

Por último se describe el tipo de dato que van a contener los nodos finales. El atributo #PCDATA significa que puede contener cualquier valor.

Con este archivo al lado de nuestros documentos XML podemos hacer doble clic sobre los mismos para que se vean en cualquier navegador.

NAVEGANDO POR LOS NODOS DEL DOCUMENTO XML

Una vez que el documento ha sido analizado sintácticamente por la implementación DOM, los datos representados estarán disponibles en la jerarquía de nodos. Cada nodo corresponde a un elemento etiqueta del documento. Por ejemplo, para el documento de clientes.xml que hemos visto anteriormente tendríamos la siguiente jerarquía: El nodo raiz es Clientes que a su vez tiene nodos hijos con el nombre Cliente. Cada uno de esos nodos hijos tienen a su vez otros nodos hijos tales como (nombre, nif, importepte y diaspago). Esos nodos hijos actuan como los nodos finales de un arból ya no tienen más nodos hijos.

Para acceder a cada nodo se utiliza la interfaz IXMLNode, comenzando por el nodo raiz a través de la propiedad DocumentElement del componente XMLDocument.

LEYENDO EL VALOR DE LOS NODOS

Vamos a ver un ejemplo de cómo leer el saldo pentiende (saldopte) del primer cliente y mostrarlo por pantalla:

var
Cliente: IXMLNode;
begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
Cliente := XML.DocumentElement.ChildNodes[0];
ShowMessage( Cliente.ChildNodes['saldopte'].Text );
end;

Suponemos que el documento clientes.xml está en el mismo directorio que nuestro programa. Como se puede apreciar en el código hemos abierto el documento XML, lo hemos activado y a continuación hemos recogido el nodo del primer cliente dentro de la variable Cliente que es de tipo IXMLNode. Después hemos accedido a su nodo hijo llamado saldopte para leer su valor.

MODIFICANDO EL VALOR DE LOS NODOS

Utilizando el mismo sistema podemos modificar el valor de cada nodo. En el siguiente ejemplo voy a modificar el nombre del segundo cliente (ROSA MARTINEZ GUILLEN) por el de MARIA PEREZ ROJO:

var
Cliente: IXMLNode;
begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
Cliente := XML.DocumentElement.ChildNodes[1];
Cliente.ChildNodes['nombre'].Text := 'MARIA PEREZ ROJO';
XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes2.xml' );
end;

Una vez modificado el nombre grabo todo el documento XML en otro archivo llamado clientes2.xml (aunque podía haber utilizado el mismo).

AÑADIENDO Y BORRANDO NODOS

Mediante el método AddChild podemos crear nuevos nodos hijos dentro de un documento XML. Veamos como dar de alta un nuevo cliente y guardar el archivo como clientes3.xml:

var
Cliente, Nodo: IXMLNode;
begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
Cliente := XML.DocumentElement.AddChild( 'Cliente' );
Cliente.Attributes['id'] := '3';
Nodo := Cliente.AddChild( 'nombre' );
Nodo.Text := 'PABLO PALAZON ALCOLEA';
Nodo := Cliente.AddChild( 'nif' );
Nodo.Text := '79469163E';
Nodo := Cliente.AddChild( 'saldopte' );
Nodo.Text := '0.00';
Nodo := Cliente.AddChild( 'diaspago' );
Nodo.Text := '15';
XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes3.xml' );
end;

En esta ocasión utilizamos dos nodos: Cliente para crear el nodo padre y Nodo para crear cada uno de los nodos hijos.

Por último podemos eliminar elementos utilizando el método Delete cuyo parámetro es el número de nodo hijo que deseamos eliminar (siendo cero el primer elemento). Para eliminar el primer cliente de la lista hacemos los siguiente:

begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
XML.DocumentElement.ChildNodes.Delete( 0 );
XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes4.xml' );
end;

Este código coge la lista de los dos clientes iniciales, elimina el primero y guarda el documento como clientes4.xml. De esta manera podemos crear una pequeña base de datos utilizando documentos XML.

En el próximo artículo veremos como simplificar aún más el manejo de archivos XML utilizando el asistente XML Data Binding.

Pruebas realizadas en Delphi 7.

10 octubre 2007

Trabajando con documentos XML (I)

Un documento XML (Extensible Markup Language) contiene un lenguaje de etiquetas para almacenar información de forma estructurada. Es similar a las páginas web HTML, exceptuando que sólo guarda la información y no la forma de visualizarla. Los documentos XML proporcionan una forma simple para almacenar la información que permite ser comprendida fácilmente.

Además se ha convertido de hecho en un estándar, siendo utilizado en aplicaciones web, comunicaciones, etc. Se trata de un lenguaje de intercambio de datos que ya es soportado por la mayoría de lenguajes de programación.

Los documentos XML manejan la información de forma jerárquica, siendo las etiquetas las que describen la información de cada elemento así como los elementos hijos. Veamos un ejemplo de un documento XML:

Este ejemplo muestra como está almacenada la información en un documento XML. La primera línea contiene la declaración de un documento XML. Aunque declaración es opcional es recomendable introducirla en todos los documentos XML. Es este caso nos dice que es un documento XML con versión 1.0 y que utiliza la codificación de caracteres UTF-8.

La segunda línea nos informa que el documento se llama Clientes y que va a estar enlazado con un documento externo llamado cli.dtd. Los documentos DTD se utilizan para definir las reglas de cómo tienen que estar estructurados los documentos XML. En nuestro caso define la regla de cómo deben estar definida la estructura de un cliente. Sería algo así como la definición de un esquema para la jerarquía de los datos del cliente. No es obligatorio definir un DTD por cada documento XML.

La información que se guarda en los documentos XML comienza con un nodo raiz Clientes que a su vez tiene nodos hijos: Cliente, nombre, etc. Las etiquetas a su vez pueden contener dentro valores predeterminados, por ejemplo Cliente tiene el valor id="1".

Aunque es posible trabajar directamente con un documento XML utilizando cualquier procesador de textos (incluso el Bloc de Notas de Windows) existen aplicaciones para editar este tipo de documentos. El consorcio internacional W3C define un conjunto de reglas estándar que definen como debe crearse un documento XML cuyo nombre es DOM (Document Object Model).

USANDO DOM

Las interfaces denifidas por el estandar DOM contienen la implementación de diferentes documentos XML suministrados por terceras partes llamados vendedores (vendors). Si no deseamos utilizar la implementación DOM de estos vendedores, Delphi permite utilizar un mecanismo de registro adicional para crearnos nuestra propia implementación de documentos XML.

La unidad XMLDOM incluye declaraciones para todas las interfaces DOM definidas por las especificaciones de nivel 2 para documentos XML según el consorcio W3C. Cada vendedor DOM proporciona sus propias interfaces para manejar documentos XML. Una interfaz DOM viene a ser algo así como un driver para manejar documentos XML.

Para que Delphi pueda utilizar un documento DOM de algun vendedor en concreto hay que añadir la unidad correspondiente a dicha implementación DOM. Estas unidades finalizan con la cadena 'xmldom'. Por ejemplo, la unidad para la implementación de Microsoft es MSXMLDOM, la unidad para documentos de IBM es IBMXMLDOM y la unidad para implementaciones Open XML es OXMLDOM.

Si quisieramos crear nuestra propia implementación DOM tendríamos que crear una unidad que defina una clase descendiente de la clase TDOMVendor. Se haría de la siguiente manera:

- En nuestra clase descendiente, debemos sobrescribir dos métodos: el método Description, el cual devuelve una cadena con el identificador del venvedor y el método DOMImplementation donde define la interfaz IDOMImplementation.

- Después registramos nuestra nueva unidad llamando al procedimiento RegistrerDOMVendor y ya estaría lista para poder ser utilizada.

- Y si queremos eliminarla de las implementaciones estándar DOM debemos anular el registro llamando al procedimiento UnRegisterDOMVendor, el cual debería estar en la sección finalization.

Algunos vendedores suministran extensiones a las interfaces estandar DOM. Para que puedas usar esas extensiones la unidad XMLDOM define la interfaz IDOMNodeEx, la cual es descendiente de la interfaz estandar IDOMNode.

Podemos trabajar directamente con interfaces DOM para leer y editar documentos XML. Simplemente llamamos la la función GetDOM para obtener el intefaz IDOMImplementation, el cual proporciona un punto de partida.

TRABAJANDO CON COMPONENTES XML

El punto de partida para comenzar a trabajar con documentos XML es utilizando el componente de la clase TXMLDocument. Para poder crear un documento XML hay que seguir los siguientes pasos:

1. Añadimos el componente XMLDocument a nuestro formulario o módulo de datos. Este componente se encuentra en la pestaña Internet.

2. Seleccionamos en la propiedad DOMVendor la implementación del documento DOM que queremos usar. Por defecto aparece seleccionada la de Microsoft (MSXML).

3. Dependiendo de la implementación seleccionada, podemos modificar sus opciones en propiedad ParseOptions para configurar como va a procesarse el documento XML. Un parser viene a ser algo así como un analizador sintáctico.

4. Para trabajar con un documento XML que ya existe hay que especificar el documento de la siguiente manera:

- Si el documento esta almacenado en un archivo, ponemos en FileName el nombre del archivo.

- O bien podemos meter el texto XML dentro del mismo componente XMLDocument utilizando su propiedad XML.

5. Activamos el documento XML poniendo la propiedad Active a True.

Una vez que tenemos activo el documento, podremos navegar por su jerarquía de nodos leyendo o modificando sus valores. El nodo raiz está disponible en la propiedad DocumentElement.

En el próximo artículo veremos ejemplos de código fuente para leer y modificar documentos XML con Delphi.

Pruebas realizadas en Delphi 7.

08 octubre 2007

Generando números aleatorios

Las funciones de las que disponen los lenguajes de programación para generar números aleatorios se basan en una pequeña semilla según la fecha del sistema y a partir de ahí se van aplicando una serie de fórmulas se van generando números al azar según los milisegundos que lleva el PC arrancado.

Dephi dispone de la función Random para generar números aleatorios entre 0 y el parámetro que se le pase. Pero para que la semilla no sea siempre la misma es conveniente inicializarla utilizando el procedimiento Randomize.

Por ejemplo, si yo quisiera inventarme 10 números del 1 y 100 haría lo siguiente:

procedure TFormulario.Inventar10Numeros;
var
i: Integer;
begin
Randomize;

for i := 1 to 10 do
Memo.Lines.Add( IntToStr( Random( 100 ) + 1 ) );
end;

El resultado lo he volcado a un campo Memo.

INVENTANDO LOS NUMEROS DE LA LOTO

Supongamos que quiero hacer el típico programa que genera automáticamente las combinaciones de la loto. El método es tan simple como hemos visto anteriormente:

procedure TFormulario.InventarLoto;
var
i: Integer;
begin
Randomize;

for i := 1 to 6 do
Memo.Lines.Add( IntToStr( Random( 49 ) + 1 ) );
end;

Pero así como lo genera es algo chapucero. Primero tenemos el problema de que los números inventados no los ordena y luego podría darse el caso de el ordenador se invente un número dos veces.

Para hacerlo como Dios manda vamos a crear la clase TSorteo encargada de inventarse los 6 números de la loto y el complementario. Además lo vamos a hacer como si fuera de verdad, es decir, vamos a crear un bombo, le vamos a introducir las 49 bolas y el programa las va a agitar y sacará una al azar. Y por último también daremos la posibilidad de excluir ciertos números del bombo (por ejemplo el 1 y 49 son los que menos salen por estadística).

Comencemos creando la clase TSorteo en la sección type:

type
TSorteo = class
public
Fecha: TDate;
Numeros: TStringList;
Complementario: String;
Excluidos: TStringList;

constructor Create;
destructor Destroy; override;
procedure Inventar;
procedure Excluir( sNumero: String );
end;

Como podemos ver en la clase los números inventados y los excluidos los voy a meter en un StringList. El constructor de la clase TSorteo va a crear ambos StringList:

constructor TSorteo.Create;
begin
Numeros := TStringList.Create;
Excluidos := TStringList.Create;
end;

Y el destructor los liberará de memoria:

destructor TSorteo.Destroy;
begin
Excluidos.Free;
Numeros.Free;
end;

Nuestra clase TSorteo también va a incluir un método para excluir del sorteo el número que queramos:

procedure TSorteo.Excluir( sNumero: String );
begin
// Antes de excluirlo comprobamos si ya lo esta
if Excluidos.IndexOf( sNumero ) = -1 then
Excluidos.Add( sNumero );
end;

Y aquí tenemos la función que se inventa el sorteo evitando los excluidos:

procedure TSorteo.Inventar;
var
Bombo: TStringList;
i, iPos1, iPos2: Integer;
sNumero, sBola: String;
begin
// Metemos las 49 bolas en el bombo
Bombo := TStringList.Create;
Numeros.Clear;

for i := 1 to 49 do
begin
sNumero := CompletarCodigo( IntToStr( i ), 2 );

if Excluidos.IndexOf( sNumero ) = -1 then
Bombo.Add( sNumero );
end;

// Agitamos las bolas con el método de la burbuja
if Bombo.Count > 0 then
for i := 1 to 10000 + Random( 10000 ) do
begin
// Nos inventamos dos posiciones distintas en el bombo
iPos1 := Random( Bombo.Count );
iPos2 := Random( Bombo.Count );

if ( iPos1 >= 0 ) and ( iPos1 <= 49 ) and ( iPos2 >= 0 ) and ( iPos2 <= 49 ) then
begin
// Intercambiamos las bolas en esas dos posiciones inventadas
sBola := Bombo[iPos1];
Bombo[iPos1] := Bombo[iPos2];
Bombo[iPos2] := sBola;
end;
end;

// Vamos sacando las 6 bolas al azar + complementario
for i := 0 to 6 do
begin
if Bombo.Count > 0 then
iPos1 := Random( Bombo.Count )
else
iPos1 := -1;

if ( iPos1 >= 0 ) and ( iPos1 <= 49 ) then
sBola := Bombo[iPos1]
else
sBola := '';

// ¿Es el complementario?
if i = 6 then
// Lo sacamos aparte
Complementario := sBola
else
// Lo metemos en la lista de números
Numeros.Add( sBola );

// Sacamos la bola extraida del bombo
if ( iPos1 >= 0 ) and ( iPos1 <= 49 ) and ( Bombo.Count > 0 ) then
Bombo.Delete( iPos1 );
end;

// Ordenamos los 6 números
Numeros.Sort;

Bombo.Free;
end;

El procedimiento Inventar hace lo siguiente:

1º Crea un bombo dentro de un StringList y le mete las 49 bolas.

2º Elimina los número excluidos si los hay (los excluidos hay que meterlos en dos cifras, 01, 07, etc.)

3º Agita los números dentro del bombo utilizando del método de la burbuja para que queden todas las bolas desordenadas.

4º Extrae las 6 bolas y el complementario eligiendo a azar dos posiciones del StringList para hacerlo todavía mas rebuscado. Al eliminar la bola extraida evitamos así números repetidos, tal como si fuera el sorteo real.

5º Una vez inventados los números los deposita en el StringList llamado Numeros y elimina de memoria el Bombo.

Ahora vamos a utilizar nuestra clase TSorteo para generar una combinación:

procedure TFormulario.InventarSorteo;
var
S: TSorteo;
begin
Randomize;
S := TSorteo.Create;
S.Inventar;
Memo.Lines.Add( S.Numeros.Text );
S.Free;
end;

Si quisiera excluir del sorteo los número 1 y 49 haría lo siguiente:

var
S: TSorteo;
begin
Randomize;
S := TSorteo.Create;
S.Excluir( '01' );
S.Excluir( '49' );
S.Inventar;
Memo.Lines.Add( S.Numeros.Text );
S.Free;
end;

Este es un método simple para generar los números de la loto pero las variantes que se pueden hacer del mismo son infinitas. Ya depende de la imaginación de cada cual y del uso que le vaya a dar al mismo.

Igualmente sería sencillo realizar algunas modificaciones para inventar otros sorteos tales como el gordo de la primitiva, el sorteo de los euromillones o la quiniela de fútbol.

Pruebas realizadas en Delphi 7.

Publicidad