Usando Objetos para guardar la configuración del programa

Introducción

El propósito de este artículo es explicar cómo usar objetos para sustituir a los viejos archivos de inicio (y otras técnicas similares) para guardar parámetros de configuración de una aplicación. El uso de objetos nos permitirá ahorrar tiempo de codificación, reducir la posibilidad de errores, encapsular los parámetros de configuración, usar archivos o el registro de windows sin inconvenientes, etc, etc.

Problemas al usar archivos de inicio

Algunas de las desventajas asociadas al uso de archivos .ini:

  • Debemos declarar una variable para cada opción a guardar, lo que implica tener unas 20 variables (o mas) globales perdidas por el código del programa.
  • Cuando necesitas guardar otro parámetro más debemos recordar escribir las instrucciones IniFile.ReadXXX e IniFile.WriteXXX
  • Si cambias el tipo de dato de un parámetro debemos recordar cambiar las instrucciones de lectura y escritura.
  • Si guardamos usando esta instrucción
    IniFile.WriteString( 'Main','LastUser', UserName); //« LastUser
    

    y leemos usando esta instrucción

    UserName := IniFile.ReadString('Main', 'LatsUser', ''); //LatsUser
    

    Nunca podremos recuperar la información almacenada. (Puede solucionarse usando constantes, pero implica más declaraciones y asignaciones.)

  • Los usuarios pueden modificar el contenido del archivo de inicio usando cualquier editor de texto y tal vez no sea conveniente.
  • Si deseas realizar validaciones sobre la información que se lee o escribe, debes crear procedimientos o funciones, lo que implica más y más código.
  • No puedes almacenar TStrings (e.g.: líneas de un memo, Parámetros de la base de datos, items de un ComboBox) de una manera directa, debes crear más funciones o procedimientos para adaptarlos.
  • No puedes almacenar TPoint, TFont, TRect y estructuras similares sin escribir más y más código, más y más validaciones.
  • Al cambiar el nombre de una variable (UserName por LastUserName) debes cambiar muchas referencias.

La gran solución : OBJETOS

(NOTA: El código del demo adjunto contiene más detalles y comentarios, que han sido omitidos en el texto de este artículo.)

Observemos esta declaración

TOpciones = class(TComponent)
//.... 
public 
  constructor Create(aOwner: TComponent); override; 
  destructor Destroy; override; 

  procedure ReadConfig; 
  procedure SaveConfig; 
  property ScrennRes: TPoint read GetScrennRes; 
published 
  property NoSplash    : Boolean  read fNoSpl    write fNoSpl; 
  property UserName    : string   read FUserName write SetUserName; 
  property Lines       : TStrings read fLin      write fLin; 
  property Font        : TFont    read FFont     write SetFont; 
  property LastUse     : TDateTime read fLastUse write flastUse; 
 {....más propiedades....} 
end;

La idea principal es crear una clase (que herede de TComponent), definir un par de propiedades, escribir un par de procedimientos y listo.

¿Cuánto vivirá nuestro objeto? Para aplicaciones medianas y chichas sólo hace falta un objeto de configuración y puede ser necesario que exista desde que se crea la aplicación. Para asegurarnos de que el objeto estará siempre disponible escribimos final de la unit (antes del end.)

initialization 
  Opciones := TOpciones.Create(nil); 
  Opciones.ReadConfig; 

finalization 
  Opciones.Free; 
//Este es el end. de la unit 
end.

Esto nos permitirá utilizar el objeto desde cualquier parte de nuestra aplicación (incluso desde el archivo .DPR ! -Ver demo adjunto- )

Propiedades

A continuación veremos algunas alternativas para declarar las propiedades, utilizaremos una u otra dependiendo de las restricciones que tengamos.

  • Declaración Básica: No tenemos control sobre lo que se guarda o lee
  • Declaración con Restricciones: Podemos evaluar los valores que se intentan asignar a la propiedad, antes de la asignación.
  • Declaración con Máscaras: Nos permite trabajar con el valor de la propiedad cuando esta es leída.
  • Combinaciones de las anteriores

Las propiedades pueden declararse en la sección protected, public y published, sólo las declaradas dentro de published serán almacenadas con el objeto.

Declaración Básica: En esta declaración la parte de lectura (read) y la de escritura (write) hacen referencia a una misma variable interna. Luego de escribir la instrucción

property NoSplash: Boolean  read fNoSplash write fNoSplash;

presionamos Ctrl + Shift + C y Delphi crea una variable privada FNoSplash del tipo boolean. De esta manera no podemos restringir los valores que se asignan a la variable ni controlar el formato con que se muestra.

¿Cómo funciona? Cuando escribimos… el compilador lo traduce en …

Opciones.NoSplash := False;    Opciones.FNoSplash := False;
Ch.checked := Opciones.NoSplash    ch.checked := Opciones.FNoSplash;

Declaración con Restricciones: En esta declaración la parte de escritura se cambia la variable por un procedimiento. (El nombre del procedimiento puede ser cualquiera, por convención es SET + nombre de la propiedad). Luego de escribir

property Lines: TStrings read fLines    write SetLines; 

presionamos Ctrl + Shift + C y Delphi crea una variable privada fLines del tipo TStrings y un procedimiento privado SetLines más el cuerpo del mismo.

procedure TOpciones.SetLines(const Value: TStrings);
begin
  fLines.Assign(Value); //Nunca hacer fLines := Value;
end;

NOTA: Dentro del procedimiento se debe usar la variable interna FLines y NO hacer referencia a Lines. Si dentro de el procedimiento SetLines cambiamos FLines por Lines se producirá una llamada recursiva al procedimiento terminando en un horrible desbordamiento de pila.

NOTA: Si la propiedad es un objeto (TFont, TStrings o cualquier otro heredero de TObject) SIEMPRE se debe utilizar este tipo de declaraciones de propiedades para hacer una copia local (usando assign u otro método), sino la asignación se convierte en una asignación de punteros.

En el cuerpo del procedimiento podemos realizar validaciones para determinar si asignamos o no el valor a la propiedad interna, mostrar mensajes o errores, llamar a otros procedimientos/funciones, modificar otras propiedades, etc (ve en el código del demo adjunto la propiedad UserName)

¿Cómo funciona?

Cuando escribimos… el compilador lo traduce en …

Opciones.Lines := Memo1.Lines;    Opciones.SetLines(Memo1.Lines);
cbx.Lines := Opciones.Lines;   cbx.Lines := Opciones.FLines;