Safeguards en Delphi.

Introducción

Los safeguards son el nombre que recibe un ingenioso mecanismo que nos permite, en cierto sentido, olvidarnos de los problemas asociados a liberar memoria en Delphi y que simulan de alguna forma el funcionamiento de un recolector de basura. En C++, para aquellos que han trabajado alguna vez con él, este mecanismo se conoce como Smart Pointers.

El truco

El truco para "olvidarnos" de liberar los objetos cuando terminamos con ellos consiste en el uso de interfaces. En delphi los interfaces incorporan un mecanismo de conteo referencial automático. Esto significa que cuando no se van a usar más (por ejemplo cuando salen de ámbito o cuando se "sobreescriben" y no quedan referencias al objeto que implementa dicho interface) se destruyen automáticamente.

Utilizando esto podemos utilizar el mecanismo de Safeguards para encapsular cualquier objeto dentro de un interface de forma que cuando dicho interface se destruya (porque cualquiera de los motivos expuestos antes) nuestro objeto se libere automáticamente sin que tengamos que preocuparnos nosotros de ello.

El código

Un par de ejemplos

Antes de entrar en el código de los safeguards vamos a ver un par de ejemplos de funcionamiento para entender correctamente como van a funcionar.

procedure EjecutaQuery(qryText : string);
var
  qry : TQuery;
  iGuard : ISafeGuard;
begin
  qry := TQuery.Create; // Crear el query
  Guard(qry, iGuard);   // "Asignarlo" en el safeguard
  qry.SQL.Add(qryText);
  qry.ExecSql;
end;

Si observamos el ejemplo anterior podemos ver claramente que el query creado no se destruye en ningún momento, es decir, no hay ninguna llamada a qry.Free sino que mediante la asignación al safeguard nos olvidamos de su destrucción. En este ejemplo concreto la utilidad es casi nula (conseguimos el mismo efecto poniendo un qry.Free al final) pero vamos a ver un ejemplo un poco más complejo implementado con (izquierda) y sin (derecha) safeguards.

function GuardarTodo(grupo : string) : string;
var
  qry1, qry2 : TQuery;
  tempObj : TMiObjeto;
  iG1, iG2, iG3 : iSafeGuard;
begin
  qry1 := TQuery.Create;
  Guard(iG1);
  qry1.SQL.Add(Format(SQL_SEL_GRUPO,[grupo]));
  try
    qry1.Open;
    // Recorremos todos los grupos
    while not qry.Eof do
    begin
      LeerTempObj(tempObj,
                  qry.Fields[0].AsString);
      Guard(tempObj, iG3);
      try
        tempObj.HazOperacion();
        if (tempObj.Seguir)
        begin
          qry2 := TQuery.Create;
          qry2.SQL.Add(OTRA_QUERY);
          qry.Open;
          result := qry.Fields[0].AsString;
        end;
      except
        // Liberar, logear y salir
        Log('Error xxx al operar');
      end;
      qry.Next;
    end;
  except
    Log('Ocurrió un error al guardar los datos');
  end;  
end;
function GuardarTodo(grupo : string) : string;
var
  qry1, qry2 : TQuery;
  tempObj : TMiObjeto;
begin
  qry1 := TQuery.Create;
  qry1.SQL.Add(Format(SQL_SEL_GRUPO,[grupo]));
  try
    qry1.Open;
    // Recorremos todos los grupos
    while not qry.Eof do
    begin
      LeerTempObj(tempObj,
                  qry.Fields[0].AsString);
      try
        tempObj.HazOperacion();
        if (tempObj.Seguir)
        begin
          qry2 := TQuery.Create;
          qry2.SQL.Add(OTRA_QUERY);
          qry2.Open;
          result := qry.Fields[0].AsString;
        end;
      except
        // Liberar, logear y salir
        Log('Error xxx al operar');
        // Liberamos el query1
        qry1.Free;
        // Liberamos tempObj
        tempObj.Free;
        // ¿Hay que liberar qry2? ...
        if Assigned(qry2)
          qry2.Free;
      end;
      qry.Next;
    end;
  except
    Log('Ocurrió un error al guardar los datos');
    qr1.Free; // Liberar el query
    // ¿Hay que liberar el tempObj?
    if (Assigned(tempObj))
      tempObj.Free;
  end;  
end;

A simple vista podemos ver que el segundo código es algo más complejo... no excesivamente pero podemos observar como las distintos caminos de ejecución (y en especial aquellos poco probables) exigen escribir mucho código solo para saber que elementos debemos liberar y cuales no.

Nota: Este código es un ejemplo, puesto que la implementación del Free de Delphi puede invocarse incluso desde objetos que son nil sin problema y por tanto no haría falta hacer las comprobaciones que se hacen.

Implementación de safeguards

Como ya hemos mencionado la implementación de los safeguards va a estar basada en interfaces. Dichos interfaces, mediante el mecanismo de conteo referencial interno serán los que nos permitan realizar todo el sistema.

Como hemos visto en los ejemplos existe una función Guard que se encarga de "proteger" un objeto dado dentro del safeguard. Además debe existir una forma de volver a obtener el objeto dado el safeguard y alguna forma de desasociar el objeto del safeguard. Vamos a ver la declaración del interfaz del safeguard, el objeto safeguard así como la declaración e implementación de la función Guard.

{ La declaración del interfaz del safeguard }
type ISafeGuard = interface
  function ObtenerObjeto : TObject;
  function Desasociar : TObject;
  function Liberar : TObject;
end;

{ Este es el objeto concreto que implementará el safeguard }
type TSafeGuard = class(TInterfacedObject, ISafeGuard)
private
  // El objeto que almacena el safeguard
  FObj : TObject;
  // Indica si el objeto ya se ha liberado
  FReleased : boolean;
public
  constructor Create(obj : TObject);
  destructor Destroy; override;
  // Devuelve el objeto asociado
  function ObtenerObjeto : TObject;
  // Desasocia el objeto del safeguard
  function Desasociar : TObject;
  // Libera el objeto guardado en el safeguard
  function Liberar : TObject;
end;

{ La función Guard }
procedure Guard(obj : TObject; var iG : ISafeGuard);
begin
  iG := TSafeGuard.Create(obj);
end;

Si examinamos las declaraciones anteriores podemos observar el funcionamiento. Por un lado tenemos un interfaz que describe las operaciones asociadas al safeguard y por otro lado tenemos un objeto concreto que implementa dicho interfaz. La función Guard se encarga de realizar la creación de dicho objeto safeguard (durante la cual se asocia el objeto pasado) y asignarlo al interfaz que le será devuelto al cliente. Por la implementación propia de los interfaces (como ya hemos mencionado) sabemos que el destructor del objeto que implementa el interfaz será llamado cuando no queden referencias a este y en ese momento es cuando deberemos destruir el objeto que almacena el safeguard. Veamos el código del constructor y el destructor:

constructor TSafeGuard.Create(obj : TObject);
begin
  FObj := obj;
  FReleased := false;
end;

constructor TSafeGuard.Destroy;
begin
  if not FReleased then
    FObj.Free;
end;

Otro metodo de conteo referencial

Ya vimos en el artículo sobre conteo referencial en delphi algunos de los problemas asociados a tener varias referencias a un mismo objeto en diferentes secciones de código sin que haya un orden específico en que dichas referencias dejan de ser necesarias y por tanto se vayan a liberar. El problema que se nos planteaba entonces era "de todos los que usan el objeto ¿quien es el responsable de liberarlo?".

Los safeguards nos van a descubrir otro metodo para hacer conteo referencial (seguro que los más avispados ya se habían dado cuenta) precisamente aprovechando las características de los interfaces (es decir que realizan dicho conteo de forma automática).

Para realizar el conteo sencillamente deberemos obtener el safeguard de cada objeto del que deseemos realizar conteo referencia y "jugar" con dicho safeguard. Puesto que los safeguards, al ser interfaces, aumentan y disminuyen el numero de referencias de forma automática. Veamos un ejemplo sencillo en el que dos objetos contienen una lista con objetos compartidos entre ambos.

procedure Operar;
var
  ig : iSafeGuard;
  obj : TMiObjeto;
begin
  for i := 0 to iList.Count - 1 do
  begin
    ig := iList[i];
    obj := TMiObjeto(ig.Objeto);
    // Operar con el objeto
    obj.Operar;
  end;
end;

destructor Destroy;
begin
  iList.Free; // Liberar el interface list
end;

En principio ya no almacenamos los objetos directamente en una lista de objetos sino que almacenamos sus safeguards (de forma que siempre haya una referencia al interface y por tanto el objeto no se destruya). Para acceder a los objetos en si mismos llamamos al metodo Objeto del interfaz que nos devuelve la referencia al objeto en si.

Por la propia naturaleza de los safeguards tenemos garantizado que los objetos se destruirán cuando no queden referencias a los safeguards que los "guardan" de forma que no tenemos que preocuparnos de ello. En el ejemplo anterior, al liberar la lista de interfaces iList los objetos de ella serán liberados si y solo si ya no hay nadie que los referencie, en caso contrario, si queda otra iList por ahí en otra instancia del objeto por ejemplo, no se liberarán.

Referencias

Este artículo esta basado en el artículo "Using SafeGuards" de la comunidad Borland publicado por Matthias Thoma.

0
No votes yet
Your rating: None