Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Clases interpuestas

Este truco explica una técnica que vienen utilizando algunos programadores de Delphi desde hace mucho tiempo. De hecho, yo mismo leí un artículo que trataba sobre lo que en inglés se denominan interposer classes (o clases interpuestas, más o menos) hace ya bastante tiempo. Pero confieso que no le presté la atención que merecía. Pero nunca es tarde para rectificar...

HISTORIA DE UN COMPROMISO

No es necesario aclarar que Delphi es un lenguaje orientado a objetos. No obstante, igual de cierto es que cuando desarrollamos un formulario en Delphi, no estamos respetando mucho la metodología de objetos... aunque en breve veremos que esta falta de respeto tiene sus ventajas.

Cuando se programa un formulario en Delphi, el programador trabaja sobre una clase descendiente casi siempre de TForm: esta es la única aparición de la herencia de clases en todo el proceso. Supongamos que se añade un botón sobre el formulario. Para realizar alguna acción cuando se pulse sobre él, el programador añade un método ¡al formulario!, y se asigna su dirección a una de las propiedades del botón (al evento OnClick, por supuesto). Como ejercicio mental, supongamos que intentamos hacer eso mismo en un C++ más o menos "puro". Lo que haríamos, con casi total seguridad, sería crear una nueva clase derivada de TButton y "redefinir" un método que se llamaría probablemente Clicked.

Comparemos ambas técnicas: en teoría, el enfoque "puro" debería ser mejor, porque el resultado obtenido es una nueva clase de botón, que en principio debería ser reutilizable. Pero en la práctica, la nueva clase casi nunca lo sería. ¿Qué es lo que debe hacer, en concreto, el dichoso botón al ser pulsado? ¿Mostrar un mensaje "Hola, Mundo"? ¿Merece la pena "reutilizar" esta tontería? Por otra parte, el proceso de heredar de una clase para redefinir un único método es más lento y puede incluso que más peligroso. Por ejemplo, ¿deberíamos llamar a la versión heredada de Clicked? En caso afirmativo, ¿deberíamos llamarla al principio o al final del nuevo método? Como puede ver, la metodología de Delphi gana con facilidad la competencia. En término medio, ofrece mayor velocidad de programación, y si de verdad necesitásemos un botón reutilizable, seguimos teniendo la posibilidad de heredad y redefinir.

Todo esto es posible gracias a que el creador del componente TButton debe haber analizado todos los posibles usos que podía tener un botón. ¿Hace falta saber cuando se pulsa? Pues entonces introducimos un evento OnClick en la clase. ¿Es necesario saber cuándo obtiene el foco del teclado? Allá va entonces un evento OnEnter... El problema surge al ser literalmente imposible prever todas las necesidades del programador que utilizará el componente.

UN EJEMPLO SENCILLO

Supongamos que necesitamos un botón en la ventana principal de una aplicación. Para simplificar, supondremos que se trata de un botón de la clase TSpeedButton, que no puede obtener el foco del teclado. Queremos que el color del texto del botón cambie cuando movamos el ratón sobre su superficie. ¿Hay algún evento a mano? Sorprendentemente, no lo hay. Hay, eso sí, un par de "mensajes de usuario" definidos por Delphi que son recibidos por un control cuando el ratón entra dentro de su área, y cuando la abandona: los mensajes CM_MOUSEENTER y CM_MOUSELEAVE.

Nuestra primera reacción sería crear una clase derivada de TSpeedButton, para que cambie el color del texto del botón en respuesta a los mensajes citados. Sin embargo, los programadores sentimos pánico cuando tenemos que hacer algo semejante. En primer lugar, tenemos que crear, si somos organizados, un paquete de tiempo de ejecución para alojar al nuevo componente, y preferiblemente un paquete independiente, para tiempo de diseño, que registre el componente dentro de la Paleta de Delphi. Ya estos dos pasos adicionales, por sí mismos, nos hacen perder algo de tiempo.

Pero además, cada vez que abramos el proyecto en un ordenador tendremos que asegurarnos de que el paquete esté instalado en ese ordenador. Personalmente, trabajo con varios ordenadores: el de IntSight, el de casa, mi portátil, un ordenador adicional para pruebas de software... En cada uno de ellos, tendría que tener la última versión de mi componente. ¿Y qué sucedería cada vez que tuviese que retocar el código fuente del nuevo botón? Pues que tendría que cargar al menos el paquete de tiempo de ejecución, realizar los cambios y recompilar. En pocas palabras: tanto ajetreo merece la pena sólo si se trata de un componente con gran cantidad de adiciones o modificaciones. No merece la pena si lo único que queremos es cambiar el color de un texto.

Es en este escenario donde las "clases interpuestas" demuestran su utilidad. El meollo de la técnica consiste en crear una clase que tenga el mismo nombre que su ancestro, para engañar a Delphi y forzarle a utilizar nuestra nueva clase. Analice la siguiente declaración:

type
  TSpeedButton = class(Buttons.TSpeedButton)
  private
    procedure CMMouseEnter(var M: TMessage); message CM_MOUSEENTER;
    procedure CMMouseLeave(var M: TMessage); message CM_MOUSELEAVE;
  end;

  TForm1 = class(TForm)
    SpeedButton1: TSpeedButton;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

La clase TSpeedButton (la de verdad) está definida en la unidad Buttons, que Delphi se ha molestado en incluir en la cláusula uses de la unidad donde definimos el formulario. Pero note cómo hemos "atravesado" una clase con el mismo nombre que la predefinida, entre la cláusula uses y la declaración del formulario, que utiliza una instancia de un botón. ¿De qué botón? Si hacemos caso de las reglas de alcance de Delphi, ¡de nuestro nuevo botón! En tiempo de diseño veremos un TSpeedButton común y corriente, pero en tiempo de ejecución, el formulario creará un botón utilizando nuestra propia clase, gracias a nuestro engaño.

La implementación de los manejadores de mensajes declarados en la nueva clase es simple e inmediata:

procedure TSpeedButton.CMMouseEnter(var M: TMessage);
begin
  inherited;
  Font.Color := clNavy;
end;

procedure TSpeedButton.CMMouseLeave(var M: TMessage);
begin
  inherited;
  Font.Color := clBlack;
end;

Quiero que se dé cuenta de otra ventaja, aunque no la hayamos aprovechado en el ejemplo: ¡dentro de la clase interpuesta tendremos acceso a todos los recursos protegidos por sus ancestros! Además, la nueva clase se ha definido por completo dentro de nuestro proyecto. Si cambiamos de ordenador, no hará falta comprobar qué componentes se han instalado en él.

LOS LIMITES DE LA TECNICA

Hay límites, por supuesto, en lo que se puede lograr con esta técnica. La principal pega consiste en que ninguna de las características que añadamos a la nueva clase estará disponible en tiempo de diseño.

No obstante, hay límites que sólo son aparentes. La objeción más frecuente es la dificultad para reutilizar la clase interpuesta. ¿Qué pasaría si necesitáramos los nuevos botones en otro formulario? ¿Tenemos que definir otra vez la clase de interposición? ¡Ni hablar! Podemos encapsularla dentro de una unidad. Para seguir engañando a Delphi, tenemos que incluir la nueva unidad en la cláusula uses del formulario, pero teniendo cuidado de ubicarla después de la unidad donde se encuentra la clase original:

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
  Forms, Dialogs, Buttons, MisBotones;

Como comprenderá, el único límite importante será nuestro conocimiento de la VCL... y nuestra imaginación.