No es dinámico como en "dinámico de Bajo nivel" sino llamar métodos por su nombre. La Run-Time Type Information (RTTI) es la forma en que Delphi se vuelve mucho mas poderoso al combinarse con herencia y polimorfismo, sin embargo es el punto ahora, el problema es distinto.
Digamos que queremos hacer un pequeño motor de script (básico para empezar). Podemos guardar las instrucciones del script en un archivo de texto, en un campo de una tabla, etc. Nuestro programa lee esas instrucciones y las va ejecutando una por una, haciendo diversas cosas. ¿Dinámico, no?
Bueno es fácil con Delphi. Los métodos tienen una dirección de memoria donde esta el código (las instrucciones en lenguaje máquina) y una dirección de memoria donde esta los datos (digamos el ambiente del método), en este caso son los datos de la clase a la que el método pertenece.
Ahora, ¿Como guardamos una dirección de memoria en Delphi? Un puntero es un tipo de variable que guarda una dirección de memoria, en dicha dirección puede haber cualquier cosa: una cadena, un numero, un arreglo, o... un método. Como toda clase deriva de la Clase TObject, y dicha clase TObject tiene un método llamado MethodAddress con el que obtenemos el puntero hacia un método que sepamos su nombre, entonces si tenemos una clase llamada MiClase que tiene un método llamado Alerta, obtenemos la dirección de memoria donde esta el código que ejecuta ese método de la siguiente manera:
MiClase.MethodAddress("Alerta");
Para representar un método, Delphi tiene una estructura llamada TMethod. Dicha estructura tiene dos componentes: Code y Data (Código y Datos como lo mencionamos antes). En Code va la dirección de memoria de el método, y en Data guardamos la dirección de memoria de la instancia de la clase (es decir, el objeto) a la cual pertenece el método.
Entonces para llamar ejecutar nuestro Script básico, necesitamos una clase que contenga métodos con el nombre de las instrucciones de nuestro script, y después crear una instancia de esa clase (crear un objeto de esa clase), leer nuestro script y por cada linea del script (una instrucción) crear un TMethod obteniendo el puntero hacia esa función. Lo codifíco aquí en un solo procedimiento por facilidad:
// EjecutaScript con el parámetro del nombre
// del archivo "script"
procedure TMiClase.EjecutaScript(ArchScript: string);
var
p: procedure of Object; // Un puntero a un metodo
Script: TStringList; // Aqui cargaremos el script
instrucc: string; // para iterar por
// las instrucciones
begin
if FileExists(ArchScript) then
begin
Script := TStringList.Create();
try
// Si existe el archivo del script
// Cargamos el script a una lista de strings
Script.LoadFromFile(NombreArchivoScript);
// Por cada instrucción (una linea
// en el archivo):
for instrucc in Script do
begin
// Si la linea no es vacía
if trim(instrucc) <> '' then
begin
// Obtenemos la dirección del método y la
// asignamos al elemento Code de
// p como TMethod
TMethod(p).Code := Self.MethodAddress(instrucc);
// Asignamos el objeto mismo
// como Data del TMethod
TMethod(p).Data := Self;
// Si resulto asignado p (no apunta a nil)
if Assigned(p) then
begin
p(); // Ejecutamos p!!
end;
end;
end;
finally
FreeAndNil(Script); // liberamos el TStringList
// Como debe de ser :P
end;
end;
end;
Ejemplo: Si tenemos un método llamado Alerta, un BorraCache y un Procesa en la clase TMiClase:
// Actualizado 20/nov/2007 - ver nota al pie
TMiClase = class
...
public
procedure EjecutaScript(ArchScript: string);
...
published
procedure Alerta;
procedure Procesa;
procedure BorraCache;
...
end;
y nuestro script consiste en un archivo llamado por ejemplo 'haznuevo.txt' que contiene:
alerta
borracache
procesaal invocar:
var
MiObjeto: TMiClase;
begin
MiObjeto := TMiClase.Create;
try
MiObjeto.EjecutaScript('haznuevo.txt');
finally
FreeAndNil(MiObjeto);
end;
end;
Entonces se ejecutaran los métodos Alerta, BorraCache y Procesa del Objeto MiObjeto (de clase MiClase :)
También hay forma de pasar parámetros, lo mostrare en un post mas adelante.
Actualización 20/nov/2007: Como bien lo señala Al Gonzalez (¡gracias Al!), el método
MethodAddress solo funciona para obtener la dirección de métodos declarados como
published!! por lo que declararlos en la sección
private no funciona.