PRG DELPHI/CLASES

De Pragma Wiki
Ir a la navegación Ir a la búsqueda

Introducción

Las clases definen estructuras de datos (o campos), métodos (funciones y procedimientos) y propiedades (que acceden a los campos). Estas clases son solo definiciones y no se ejecutan, pero se pueden 'instanciar' (crear) dentro de un software para poder inicializar sus propiedades o ejecutar sus métodos. Al instanciar una clase estamos creando un objeto.

Las clases a su vez pueden ser derivadas para crear nuevas clases que heredan todas las propiedades y métodos definidos (similar a lo presentado en *Crear una clase para derivar).

Delphi usa Object Pascal, que es un Pascal orientado a objetos, como lenguaje de programación.

La aplicación de Pragma está construida con distintas clases, muchas de Delphi, y muchas desarrolladas específicamente.

(Ver Clases y objetos en Delphi).

Los métodos y propiedades tienen distintos grados de visibilidad desde otros objetos:

  • private (o privado) solo pueden consultarse o ejecutarse dentro de la misma clase.
  • protected (o protegido) solo pueden consultarse o ejecutarse dentro de la misma clase o sus derivadas.
  • public (o pública) pueden consultarse o ejecutarse desde cualquier objeto.
  • published (o publicadas) se usan en los inspectores de objetos de Delphi (aplica a los componentes visuales (o controles), más sobre esto más adelante).

Clase de ejemplo

Como ayuda para comprender la estructura y funcionamiento de las clases he creado una llamada TPersona, que representaría (de una manera muy básica) a una persona. El código completo es:

unit Persona;

interface

uses
  SysUtils, Classes;

type
  TPersona = class(TComponent)
  private
    { Declaraciones privadas }

    FDocumento: String;
    FApellido: String;
    FNombre: String;
  protected
    { Declaraciones protegidas }
  public
    { Declaraciones públicas }

    constructor Create(AOwner: TComponent); override;
    destructor  Destroy; override;
    procedure   Loaded; override;

    property    Documento: String read FDocumento write FDocumento;
    property    Apellido: String read FApellido write FApellido;
    property    Nombre: String read FNombre write FNombre;

    function    Cargar(aDocumento: String): Boolean;
    function    Grabar: Boolean;
  end;

procedure Register;

implementation

constructor TPersona.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  if csDesigning in ComponentState = False then
     begin
     { Programación ejecutada al crear el objeto... }
     end;
end;

destructor TPersona.Destroy;
begin
  if csDesigning in ComponentState = False then
     begin
     { Programación ejecutada al destruir el objeto... }
     end;

  inherited Destroy;
end;

procedure TPersona.Loaded;
begin
  inherited Loaded;

  if csDesigning in ComponentState = False then
     begin
     { Programación ejecutada al instanciarse la pantalla en la que reside el componente... }
     end;
end;

function TPersona.Cargar(aDocumento: String): Boolean;
begin
  Result := True;

  { Cargamos los datos del documento indicado de la base de datos e inicializamos 
    las propiedades Apellido y Nombre...

    Si encontramos el registro retornamos true, sino retornamos False...
  }  
end;

function TPersona.Grabar: Boolean;
begin
  Result := True;

  { Grabamos los datos Documento, Apellido y Nombre en la base de datos, si 
    tuvimos éxito retornamos True, sino retornamos False...
  }    
end;

procedure Register;
begin
  RegisterComponents('prxStandard', [TPersona]);
end;

end.

Partes básicas

unit

Indica el nombre del archivo en el que se encuentra definida la clase, en este caso el archivo se llama Persona. La extensión de los archivos pascal es ".pas", es decir que el archivo completo sería Persona.pas.

interface

Indica que lo que sigue es la denominada interfaz de la clase, que es su definición, pero no su implementación. Definimos las propiedades y funciones que tiene la clase, pero la programación de éstas la dejamos para más adelante, para la sección de implementación.

uses

La clase puede usar la definición de otras clases o archivos. Esos archivos se listan en esta sección, aunque también se pueden listar después de la palabra clave implementation, también usando uses. En este caso la clase usa dos archivos con funcionalidad de Delphi llamados SysUtils y Classes.

Register

La función Register sirve para registrar el componente en la paleta de Delphi, por ahora no es importante.

implementation

Indica que lo que sigue es la implementación de la clase, es decir toda la programación de las propiedades y métodos.

Interfaz de la clase

En esta sección analizamos la interfaz de la clase que es la siguiente:

type
  TPersona = class(TComponent)
  private
    { Declaraciones privadas }

    FDocumento: String;
    FApellido: String;
    FNombre: String;
  protected
    { Declaraciones protegidas }
  public
    { Declaraciones públicas }

    constructor Create(AOwner: TComponent); override;
    destructor  Destroy; override;
    procedure   Loaded; override;

    property    Documento: String read FDocumento write FDocumento;
    property    Apellido: String read FApellido write FApellido;
    property    Nombre: String read FNombre write FNombre;

    function    Cargar(aDocumento: String): Boolean;
    function    Grabar: Boolean;
  end;

type

Es una palabra clave que indica que lo que sigue es el nombre de la clase, en este caso la clase se llama TPersona y el texto class(TComponent) indica que deriva de la clase TComponent de Delphi. Las clases pueden derivar de otras heredando todas sus propiedades y funcionalidad.

TComponent es una clase de Delphi que tiene todo lo necesario para definir un componente, su jerarquía (el gráfico es informativo) es:

Jerarquía de clases de Delphi

private

Define las propiedades o métodos privados, aquellos que solo son visibles desde la misma clase. En este caso definimos tres variables para guardar el documento, el apellido y el nombre. Estas se corresponden con posiciones de memoria donde se guardarán strings. En un momento definiremos métodos de acceso a estas variables en la sección public.

protected

Define propiedades y métodos que son visibles dentro de la clase y de sus derivadas, en este caso no hemos definido nada.

public

Define propiedades y métodos visibles a todas las clases que interactúan con esta.

En esta sección definimos primero tres funciones:

constructor Create(AOwner: TComponent); override;
destructor  Destroy; override;
procedure   Loaded; override;

El constructor Create es una función especial que se ejecuta cuando creamos el objeto, el destructor Destroy se ejecuta cuando el objeto es destruido (cuando se libera la memoria que lo guardaba) y el procedimiento Loaded se ejecuta en aquellos objetos que 'viven' dentro de una pantalla (por ejemplo, un botón) cuando esa pantalla es instanciada.

Notar que estas funciones tienen la, palabra clave override a derecha indicando que extienden la función de los ancestros. Si no agregáramos override estas funciones solo ejecutarán el código existente en esta clase.

Estas funciones provienen de la clase ancestro que es TComponent.

También definimos 3 propiedades que acceden a las variables de la sección private, por ejemplo:

property    Documento: String read FDocumento write FDocumento;

Aquí definimos una propiedad llamada Documento que es visible a otros objetos, indicamos que es de tipo String y que accede a la variable FDocumento que hemos definido en private. de este modo publicamos la propiedad Documento para que todos accedan a la variable FDocumento (que es privada a la clase e inaccesible a otras clases).

Podríamos haber definido directamente el código de abajo en la sección púlica pero más adelante veremos que estos métodos (getters y setters según su nombre en inglés) pueden ser muy convenientes además de crear una programación prolija.

FDocumento: String;

Finalmente declaramos dos funciones más:

function    Cargar(aDocumento: String): Boolean;
function    Grabar: Boolean;

Que usaremos para recuperar los datos de la persona de la base de datos, y para grabarlos posteriormente.

Implementación de la clase

A partir de la palabra clave implementation podemos escribir el código de la clase.

Los objetos deben crearse para ser usados, y es buena práctica destruirlos cuando ya no los necesitamos más.

constructor Create

El constructor Create se ejecuta cuando se instancia esta clase creando un objeto:

constructor TPersona.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  if csDesigning in ComponentState = False then
     begin
     { Programación ejecutada al crear el objeto... }
     end;
end;

Recibe como argumento la referencia al objeto que lo está creando (AOwner: TComponent), el objeto creado le pertenece a quién lo creó. Si eliminamos el objeto que lo creó éste también será eliminado.

Lo primero que se ejecuta en esta función es el Create del ancestro, es habitual que en clases derivadas cada una de ellas haga algo específico en cada función.

Luego podemos agregar el código que necesitemos, por ejemplo, inicializar algunas variables.

El texto if csDesigning in ComponentState = False then indica a Delphi que no ejecute ese código cuando estamos en diseño que es la etapa en la que en Delphi armamos las pantallas.

Cómo crear un objeto desde una clase

Para crear un objeto usaremos un código muy similar al de abajo:

var
  Persona: TPersona;
begin
  Persona := TPersona.Create(Self);

  { Ahora podemos usar persona, por ejemplo llamar a la función Cargar }

  Persona.Cargar('1234');
end;

Definimos una variable Persona de tipo TPersona', similar a cuando definimos una variable de tipo entero, fecha, caracteres, etc.

Luego asignamos a esa variable (en Pascal el := es asignación) el texto TPersona.Create(Self) que indica que estamos creando una instancia de la clase TPersona e indicamos que el dueño es el mismo objeto (Self) que lo está creando.

Finalmente podemos acceder a las propiedades y métodos de la clase, por ejemplo, para cargar el documento '1234'. En Pascal las llaves {} contienen comentarios.

destructor Destroy

El destructor Destroy se ejecuta cuando liberamos la memoria asignada a un objeto:

destructor TPersona.Destroy;
begin
  if csDesigning in ComponentState = False then
     begin
     { Programación ejecutada al destruir el objeto... }
     end;

  inherited Destroy;
end;

En los destructores la llamada al inherited se hace al final de modo que podamos hacer cualquier limpieza que necesitemos antes de ir a los ancestros.

El resto es bastante directo, no requiere argumentos ya que estamos haciendo referencia al objeto que creamos originalmente.

Cómo destruir un objeto

Para destruir un objeto usaremos un código muy similar al de abajo usando la propiedad que usamos en el Create:

begin
  Persona.Free;
  Persona := nil;
end;

Con el procedimiento Free (que todo objeto define) eliminamos ese objeto.

Finalmente puede ser buena práctica asignar nil (valor nulo) a la variable.

Con esto habremos eliminado el objeto.

procedimiento Loaded

Se ejecuta en aquellos componentes (en realidad serían controles) que están pegados a una pantalla justo en el momento en que se instancia esa pantalla, veremos más sobre esto cuando veamos las pantallas.

procedure TPersona.Loaded;
begin
  inherited Loaded;

  if csDesigning in ComponentState = False then
     begin
     { Programación ejecutada al instanciarse la pantalla en la que reside el componente... }
     end;
end;

El procedimiento es similar a los anteriores, el inherited se ejecuta al inicio.

función Cargar

La función Cargar es propia de esta clase (no proviene del ancestro) y serviría para cargar los datos de la persona según el documento recibido como argumento.

function TPersona.Cargar(aDocumento: String): Boolean;
begin
  Result := True;

  { Cargamos los datos del documento indicado de la base de datos e inicializamos
    las propiedades Apellido y Nombre...

    Si encontramos el registro retornamos true, sino retornamos False...
  }
end;

Definimos que la función retorna un Boolean por lo que deberíamos usar un valor de retorno que sea:

  • Result := True;
  • o Result := False;

En este caso seguramente retornaríamos True si encontramos ese documento en la base de datos, o False si no lo encontramos.

Como comentarios la función indica que podríamos asignar los valores de la base de datos a las propiedades de la clase (Apellido y Nombre).

función Grabar

La función Grabar también es propia de esta clase y serviría para guardar los datos de la persona en la base de datos.

function TPersona.Grabar: Boolean;
begin
  Result := True;

  { Grabamos los datos Documento, Apellido y Nombre en la base de datos, si
    tuvimos éxito retornamos True, sino retornamos False...
  }
end;

Definimos que la función retorna un Boolean por lo que deberíamos usar un valor de retorno que sea:

  • Result := True;
  • o Result := False;

En este caso seguramente retornaríamos True si pudimos grabar el registro exitosamente, o False si no pudimos.

Modificar los métodos de acceso a la variable FDocumento

En la interfaz original definimos Documento de la siguiente manera, accediendo directamente a la variable FDocumento:

type
  TPersona = class(TComponent)
  private
    { Declaraciones privadas }

    FDocumento: String;
  public
    { Declaraciones públicas }

    property    Documento: String read FDocumento write FDocumento;
  end;

Pero podríamos mejorar esto de la siguiente manera, primero tenemos que definir un procedimiento similar a:

procedure TPersona.DocumentoSet(aDocumento: String);
begin
  { Primero seteamos la variable privada al valor informado... }

  FDocumento := aDocumento;

  { Intentamos cargar el registro que tenga ese documento, si tenemos éxito
    inicializamos las otras variables... }

  if Cargar(FDocumento) then
     begin
       ;
     end;
end;

Donde recibimos un argumento exactamente del mismo tipo que la variable FDocumento. Dentro del procedimiento tenemos que asignar el valor del argumento a la variable privada FDocumento. Luego podemos hacer cualquier otra cosa, por ejemplo, llamar al procedimiento Cargar.

El interfaz lo definimos como protected.

  protected
    { Declaraciones protegidas }

    procedure DocumentoSet(aDocumento: String);

Luego tenemos que modificar la declaración de la propiedad Documento de la siguiente manera:

property    Documento: String read FDocumento write DocumentoSet;

Con esto estamos asociando la operación de escritura de la propiedad Documento directamente al procedimiento DocumentoSet por lo que cuando asignen un valor a la propiedad Documento, por ejemplo, usando Persona.Documento := '1234' la propiedad va a llamar directamente al procedimiento DocumentoSet completando el argumento con ese valor y procederá según la definición del procedimiento.

Sugerencia:

 - Por ejemplo, en una clase de automovil podriamos tener una propiedad llamada encendido que se setee en un procedimiento que haga todo lo necesario para que el automóvil se encienda, y además setee el valor de esa propiedad según el estado del auto.