mayo 04, 2006

Extendiendo la VCL sin usar herencia ni registrando componentes nuevos

Es común que de repente estemos tentados a instalar fulano componente porque hace X monería, el problema con eso es que si nos viciamos en esa practica, llevar los fuentes de nuestro proyecto a otra máquina tiene el inconveniente de que hay que instalar todos estos componentes antes de poder compilar la primera línea de código.

Existen algunas practicas que permiten variar el comportamiento de los componentes que trae Delphi por defecto sin hacer derivaciones de las clases registradas, lo cual permite que sin instalar nada en el IDE tengamos el resultado que deseamos. Una de esas técnicas la describo un poco en el artículo Alternativas a los Skins y es la técnica de las clases sobrepuestas, pero en Delphi 2006 esta no es la única opción... existe una técnica mas estilizada y que se apega más a las buenas costumbres orientadas a objetos.

En BDS 2006 y desde Delphi 8 existe una extensión del lenguaje de Object Pascal llamada Class Helpers esta nueva característica nos permite definir nuevos comportamientos para alguna clase previamente definida de manera natural y su práctica nos ofrece muchos beneficios. Vamos a un ejemplo:

Muchos de nosotros usamos la funcion Trim() de Delphi que elimina los espacios en blanco al principio y al final de una cadena, y solemos usarla en controles como el caso del TEdit con código como este:

Edit1.Text := Trim(Edit1.Text);

Pero, ¿no sería mas claro y elegante que el control Edit1 tuviera un método Ajustar() que hicera eso directamente sobre el control?, veamos como implementariamos eso, primero definimos la clase que servira como ayudante de la clase TEdit.

TMiEdit = class helper for TEdit
public
procedure Ajusta;
end;

He llamado a la clase auxiliar TMiEdit ya que en esta técnica la clase auxiliar no tiene que llevar el mismo nombre de la clase a la que estamos ayudando, y ahora definimos el método nuevo:

procedure TMiEdit.Ajusta;
begin
Text := Trim(Text);
end;

Observen que como esta clase es una ayudante de TEdit puedo acceder a las propiedades de la misma clase TEdit, en este caso la propiedad Text, ahora para hacer uso del nuevo método no tengo que crear nuevas instancias solo usar un objeto común de la clase TEdit de este modo:

procedure TForm2.Button1Click(Sender: TObject);
begin
Edit1.Ajusta;
end;

Pero tambien se pueden modificar métodos de la clase afectada, por ejemplo si quisieramos hacer que al llamar al método Clear del TEdit este en vez de poner una cadena nula pusiera por defecto un cero, hariamos algo como esto:

TMiEdit = class helper for TEdit
public
procedure Clear; overload;
end;

procedure TMiEdit.Clear;
begin
Text := '0';
end;


begin
Edit1.Clear;
end;

En este caso estamos sobrecargando el método Clear pero al ser este un método re-definido en la clase ayudante se toma este último por defecto.

Esto suele sernos útil en casos donde por ejemplo tenemos que ajustar controles como el TDBGrid para que soporte la ruedita del ratón, o para que cambie de casilla con la tecla Enter, o para Editar campos Memo en las celdas, etc... usando el control TDBGrid de toda la vida.

Las limitaciones que tiene esta técnica son que desde las clases ayudantes no se puede tener acceso a propiedades o métodos declarados como estrictamente privados o estrictamente protegidos, asi como tampoco permite la definición de datos adicionales, es decir que no se pueden declarar variables de campos en estas clases auxiliares; lo único que se permite es que se púeden definir variables de clase (una nueva característica tambien), es decir variables que serán comunes a todas las instancias de la clase ayudada, esto es útil para por ejemplo poder llevar la cuenta de cuantas instancias de X clase se han creado y de alguna manera implementar variantes del patrón de diseño singleton sobrecargando el constructor de la clase.

3 comentarios:

  1. ¡Hola a todos!

    Vaya, desconocía esta característica en Delphi (mas no el concepto de "ayudante de clase"), quizá por no haber salido aún de la versión 7. Sería muy bueno poder acceder cuando menos a los elementos protegidos de la clase ayudada.

    De todas formas no deja de tener otras limitantes. Veo este mecanismo de los ayudantes de clases como un paso intermedio hacia el concepto de "Herencia Insertada" que he mencionado en algunas ocasiones:

    http://www.clubdelphi.com/foros/showpost.php?p=29604&postcount=10
    http://www.clubdelphi.com/foros/showpost.php?p=41664&postcount=7

    Buen artículo Carlos.

    Un abrazo ayudante.

    Al González. :)

    ResponderBorrar
  2. Saludos Al,

    A como logro entender el concepto de "Herencia Insertada" me parece que sería como usar esta técnica al nivel de una clase ancestra común para algunas clases que te interesa modificar.

    Aqui voy a poner un ejemplo práctico, supongamos un método para ajustar los anchos de las columnas de un DBGrid tal como se haría en Excel para ajustar el ancho de las columnas al valor mas ancho de las columnas.

    TMiGrid = class helper for TCustomDBGrid
    public
    procedure AjustaColumnas;
    end;

    Aqui estamos aplicando el ayudante a una clase ancestra de TDBGrid, la implementación del método podría ser algo como:

    procedure TMiGrid.AjustaColumnas;
    const
    DEFBORDER = 3;
    var
    temp, n: Integer;
    lmax: array[0..30] of Integer;
    begin
    DataSource.DataSet.DisableControls;
    Canvas.Font := Font;
    for n := 0 to Columns.Count - 1 do
    lmax[n] := Canvas.TextWidth(Fields[n].FieldName) + DEFBORDER;
    DataSource.DataSet.First;
    while not DataSource.DataSet.EOF do
    begin
    for n := 0 to Columns.Count - 1 do
    begin
    temp := Canvas.TextWidth(trim(Columns[n].Field.DisplayText)) + DEFBORDER;
    if temp > lmax[n] then lmax[n] := temp;
    end;
    DataSource.DataSet.Next;
    end;
    DataSource.DataSet.First;
    for n := 0 to Columns.Count - 1 do
    if lmax[n] > 0 then
    Columns[n].Width := lmax[n];
    DataSource.DataSet.EnableControls;
    end;

    Y la llamada puede ser hecha desde cualquier objeto de una clase derivada de la ayudada

    procedure TForm2.Button1Click(Sender: TObject);
    begin
    DBGrid1.AjustaColumnas;
    end;

    Asi es como puedes modificar comportamientos en altos niveles de las jerarquias con la misma técnica. C# planea poder hacer cosas como esta en la versión 3 del lenguaje, habrá que ver si su implementación ofrece las bondades que en Delphi aún no se tienen. Habrá que recordar que hasta Delphi 7 solo se tenian disponibles los Métodos de clase y ahora hay Propiedades y Variables de clase tambien. Creo que a nivel del lenguaje si han habido grandes y muy buenas mejoras en Delphi desde Delphi 7, y no solo en el Lenguaje sino tambien en el IDE. Se pagan algunos precios como el desempeño en algunos casos, pero creo que tambien se obtienen muchos y muy buenos beneficios.

    Un saludo azteca mi estimado Al.

    ResponderBorrar
  3. Estos chicos de Borland no son muy originales que digamos.
    Esto de las clases "helper" ya existía desde los tiempos de Delphi 1, pero jugando un poco con los cast y sabiendo lo que está pasando por debajo.

    Es muy sencillo hacerlo, siguiendo tu ejemplo, defines la clase TMiEdit que herede de TEdit y añada el método Ajustar (o todos los métodos de ayuda que necesites)

    Después, a cualquier TEdit de cualquier aplicación, puedes hacerle un cast a TMiEdit y llamar a los métodos de ayuda:

    TMiEdit(Edit1).Ajustar;

    Desde el código del método Ajustar podrás acceder a todos los métodos y atributos del TEdit estándar, incluso a los protegidos.

    El único inconveniente es que la clase "ayudante" no puede añadir nuevos atributos, ya que modificaría su estructura interna y a la hora de hacer el cast los resultados pueden ser desastrosos.

    Saludos

    JM

    ResponderBorrar