Patrón Dispose

Antes de comenzar con el patrón dispose es necesario aclarar ciertos términos, ya que, los vamos a utilizar cuando nos referimos a Dispose. Estos términos son: recursos, recursos administrados, recursos no administrados y recolector de basura (garbage collector o GC)

Recursos

Un recurso, podría definirse como, cualquier componente, físico o virtual, que tiene una disponibilidad limitada en nuestro sistema. Un recurso de software, podría ser la memoria de la máquina virtual de Java o la memoria como tal de la computadora, clases creadas, valores almacenados, conexiones a bases de datos, etc.

Recolector de basura (Garbage Collector o GC)

El GC es un elemento fundamental de cualquier lenguaje de programación, ya que, es el encargado de liberar los recursos utilizados por nuestros sistemas.

Cada vez que creamos una clase, esta se agrega a la memoria de la computadora en donde puede ser accedida por otras clases o recursos. Una vez que esa clase ya no es necesaria, el GC es el encargado de recolectarla para luego ser destruida y liberar espacio en memoria.

Recursos Administrados

En software, cuando hablamos de recursos administrados, nos referimos a todas aquellas clases u objetos que son controlados por el core del lenguaje que estemos usando para codificar.

Por ejemplo, en Java, podríamos decir que un recurso administrado es una instancia de la clase Integer, ya que, la máquina virtual (JVM: Java Virtual Machine) tiene el control total sobre esa instancia por medio del Garbage Collector. Podríamos decir que la JVM conoce quién esta utilizando esa instancia, sabe el valor y todos los detalles de la misma.

En .NET funciona similar. El framework de .NET conoce, de igual manera, cuando una instancia de un objeto del core es creada, qué ubicación en la memoria tiene y qué valor representa. En este caso el Garbage Collector de .NET también tiene el control sobre estas instancias, es decir, están dentro del rango del GC.

Recursos no Administrados

Por otra parte están los recursos no administrados, los cuales son los que no están en el rango del Garbage Collector. Es decir, no conoce ningún detalle de estos recursos. Por ejemplo, cuando utilizamos las clases para leer un archivo del sistema, el GC conoce de la clase utilizada para hacerlo, pero desconoce el estado de las clases internas del sistema operativo para poder abrir el archivo para lectura.

Otro ejemplo sería cuando nos conectamos a una base de datos. De nuevo, el GC conoce las clases del lenguaje utilizadas para realizar la conexión, pero no esta dentro de su rango las clases adicionales utilizadas fuera del lenguaje.

¿Por qué es importante liberar recursos?

Ahora que somos expertos en los diferentes tipos de recursos que utilizan nuestras aplicaciones, es necesario comprender por qué es necesario utilizarlos de manera adecuada.

Como vimos, el GC nos ayuda en gran parte a recolectar esos recursos que ya no están siendo utilizados, ya que, los remueve de la memoria. Al hacer esto, nos permite poder agregar más y más recursos sin necesidad de preocuparnos por administrarlos.

Si no liberamos recursos, dependiendo de nuestra aplicación, podríamos incurrir en errores al momento de ejecución. El error más común que recibiríamos sería que ya no hay espacio en la memoria. Si esto ocurre, nuestra aplicación se vería terminada abruptamente, en el mejor de los casos.

Ahora bien, el GC libera recursos administrados, ¡perfecto! Pero, ¿qué pasa con los no administrados? Es aquí en donde debemos implementar el patrón Dispose.

El patrón dispose es muy peculiar, ya que, prácticamente sólo se puede implementar en lenguajes que realizan una administración automática de los recursos, es decir, aquellos que cuentan con un GC. Inclusive, la mayoría de ejemplos, sólo se encuentran para .NET, ya que Java administra un poco diferente los recursos. Es por esto que, en este post, los ejemplos que voy a utilizar aplican solo a .NET.

Veamos la definición del patrón dispose.

En éste patrón, un recurso no administrado, es utilizado por un objeto y debe ser liberado completamente por medio del llamado de un método (close, dispose, release, free, clean, etc.).

Este sería el diagrama de clases del patrón.

Diagrama de clases sugerido para el patrón dispose

Ahora bien, patrón dispose cuenta con ciertas reglas que deben seguirse, para asegurarnos que lo estamos implementando de manera correcta. Veamos que nos dice la documentación de Microsoft sobre la implementación del método Dispose.

Regla 1: Debe de implementarse sólo si nuestro objeto utiliza recursos no administrados.

Es decir, si nuestras clases no acceden a archivos, configuraciones del registro, punteros, conexiones a redes, etc. No deberíamos de implementar el método Dispose. La razón detrás de esto es que, el GC realiza un excelente trabajo, cuando de liberar recursos se trata, si estos recursos son administrados. Si son no administrados, el GC nunca los podrá reclamar/liberar.

Regla 2: Múltiples llamadas al método Dispose no deberían resultar en una excepción.

Esto nos asegura que los recursos siempre serán liberados de manera correcta, sin importar el estado de nuestra clase.

Existen dos variantes de este patrón, veamos cada una de manera sencilla.

  1. Encapsular, cada recurso no administrado utilizado por nuestra clase, dentro de una clase segura (safe handle). Es decir, dentro de una clase que derive de System.Runtime.InteropServices.SafeHandle. Acá no es necesario sobre escribir el método Finalize, ya que, al derivar de SafeHandle, cubrimos esa parte automáticamente.
  2. Dentro de nuestra clase, sobre escribir el método Finalize, para asegurarnos que los recursos no administrados son liberados.

Cuando decimos sobre escribir el método Finalize, nos referimos a crear el destructor de nuestra clase.

class MyClass {

    /**
    * Constructor
    */
    public MyClass() {
    }

    /**
    * Class destructor
    */
    ~MyClass() {
    }
}

Regla 3: Implementar IDisposable y además, crear el método Dispose(Boolean), el cual debe ser virtual y protegido.

Para ambas variaciones es necesario implementar la interfaz IDisposable y además, crear un método adicional llamado Dispose(Boolean), para manejar la forma en que se liberan los recursos.

Esta sería la definición de la interfaz IDisposable.

public interface IDisposable {
    public void Dispose();
}

La firma del método Dispose(Boolean) sería la siguiente.

protected virtual Dispose(bool disposing)

Regla 4: Siempre invocar al método Dispose(Boolean) con el valor true y llamar a GC.SupressFinalize dentro del método Dispose().

El objetivo del método Dispose() es el de indicar, que deben liberarse los recursos no administrados y evitar el llamado al método Finalize por parte del GC. Para lograr esto, necesitamos llamar a GC.SupressFinalize.

public void Dispose() {
    // we are forcing the clean up of unmanaged resources
    Dispose(true);

    // avoid the call to Finalize
    GC.SupressFinalize(this);
}

El método anterior siempre debe ser utilizado en las clases que implementan IDisposable. Aquí podemos observar que, cuando el método Dispose es invocado, utiliza al método Dispose(Boolean), enviando siempre el valor de TRUE.

Entonces, ¿en qué momento se invoca a Dispose(Boolean) con el valor FALSE?

La respuesta es, desde el método Finalize, o también conocido como el destructor de la clase.

Regla 5: El método Dispose(Boolean) debe contar con una sección que libere los recursos no administrados, sin importar el valor del parámetro. Y una sección para liberar los recursos administrados solo si el parámetro es TRUE.

En la regla anterior, nos indican que los recursos administrados sólo se liberan si el parámetro recibido es verdadero, en esa sección es posible: llamar a objetos que también implementen IDisposable (recuerden que sólo aplica a recursos administrados), a las clases que utilizamos como SafeHandle al invocar su método Dispose y para finalizar, a recursos administrados que consumen grandes cantidades de memoria.

Esto es necesario, ya que, facilitamos el trabajo del GC al invocar explícitamente el método Dispose según corresponda.

Regla 6: Si el patrón es implementado en una clase base, asegúrese de que no sea sellada (sealed o NoInheritable)

Veamos estas reglas en acción utilizando un pequeño código de ejemplo.

Clase Base, utilizando clases SafeHandle.

class MyFileBase : IDisposable {

    // Variable to store current dispose state
    bool disposed = false;

    // wrap our unmanage resources using SafeHandle class
    // Please notice that I'm not using correct constructor to keep the example clean
    // here we are using a pointer, which is an unmanaged resource
    SafeHanlde fileHandle = new SafeFileHandle(IntPtr.Zero, true);

    // this is our standard dispose implementation from Rule 4
    // Also we apply Rule 2, multiple calls with no exceptions
    public void Dispose() {
        Dispose(true);
        GC.SupressFinalize(this);
    }

    // Dispose(Boolean) implementation from Rule 5
    protected virtual void Dispose(bool disposing) {
        if(disposed) {
            return;
        }

        // Rule 5 here, 2 sections, one for managed resources, another one for unmanaged
        if(disposing) {
            // free unmanaged resources using the SafeHandles
            fileHandle().Dispose();
            // free additional managed resources
        }

        // apply current dispose status to our class
        disposed = true;
    }
}

Clase Base, utilizando el destructor, es decir, sin SafeHandle.

class MyFileBase : IDisposable {

    // Variable to store current dispose state
    bool disposed = false;

    // this is our standard dispose implementation from Rule 4
    // Also we apply Rule 2, multiple calls with no exceptions
    public void Dispose() {
        Dispose(true);
        GC.SupressFinalize(this);
    }

    // Dispose(Boolean) implementation from Rule 5
    protected virtual void Dispose(bool disposing) {
        if(disposed) {
            return;
        }

        // Rule 5 here, 2 sections, one for managed resources, another one for unmanaged
        if(disposing) {
            // free additional managed resources
        }

        // free unmanaged resources

        // apply current dispose status to our class
        disposed = true;
    }

    // Finalize method also known as class destructor
    ~MyFileBase() {
        Dispose(false);
    }
}

Clase Derivada, utilizando SafeHandle. En este ejemplo, no debemos implementar Dispose(), ya que lo heredamos de la clase base.

class MyFileA : MyFileBase{

    // Variable to store current dispose state
    bool disposed = false;

    // wrap our unmanage resources using SafeHandle class
    // Please notice that I'm not using correct constructor to keep the example clean
    // here again we use a pointer, which is an unmanaged resource
    SafeHanlde fileHandle = new SafeFileHandle(IntPtr.Zero, true);

    // No method Dispose()

    // Dispose(Boolean) implementation from Rule 5
    protected virtual void Dispose(bool disposing) {
        if(disposed) {
            return;
        }

        // Rule 5 here, 2 sections, one for managed resources, another one for unmanaged
        if(disposing) {
            // free unmanaged resources using the SafeHandles
            fileHandle().Dispose();
            // free additional managed resources
        }

        // free any other unmanaged resources

        // apply current dispose status to our class
        disposed = true;

        // call base class Dispose(Boolean) method
        base.Dispose(disposing);
    }
}

Clase Derivada, utilizando el desctructor.

class MyFileA : MyFileBase{

    // Variable to store current dispose state
    bool disposed = false;

    // No method Dispose()

    // Dispose(Boolean) implementation from Rule 5
    protected virtual void Dispose(bool disposing) {
        if(disposed) {
            return;
        }

        // Rule 5 here, 2 sections, one for managed resources, another one for unmanaged
        if(disposing) {
            // free additional managed resources
        }

        // free any other unmanaged resources

        // apply current dispose status to our class
        disposed = true;

        // call base class Dispose(Boolean) method
        base.Dispose(disposing);
    }

    // Finalize method
    ~MyFileA() {
        Dispose(false);
    }
}

Regla 7: Siempre utilice SafeHandle y evite utilizar los destructores.

La razón es muy sencilla, implementar correctamente un destructor puede tomar mucho tiempo y además, es muy riesgoso. Las clases dentro de SafeHandle ya cuentan con los procedimientos correctos para liberar los recursos que utilizan.

Pueden encontrar un ejemplo muy completo en la documentación de Microsoft aquí.

¿Cuáles son algunos beneficios de implementar IDisposable?

Aparte de garantizarnos que estamos liberando correctamente todos los recursos utilizados por nuestras clases, esta interfaz, nos permite simplificar nuestro código cuando utilizamos cualquier clase que la implemente.

Veamos el ejemplo utilizando código extra y tedioso.

class MyClass : IDisposable {
    // variables and methods using resources
 
    public void Dispose() {
        Dispose(true);
        GC.SupressFinalize(this);
    }
     
    protected virtual void Dispose(bool disposing) {
        // logic used to release resources 
    }
}

class MyApp {
    // method to create resources
    public void workMyClass() {
        // create the instance and get resources
        MyClass myClass = new MyClass();

        //
        try {
            // work with the class resources
        }
        // catch exceptions
        catch(;) {
        }
        finally {
            // check if the class is still valid and resources are not yet freed
            if(myClass != null) {
                // make sure resources are released by calling Dispose
                ((IDisposable)myClass).Dispose();
            }
        }
    }
}

Ahora, veamos el código simplificado.

class MyApp {
    // method to create resources
    public void workMyClass() {
        // create the instance and get resources
        using (MyClass myClass = new MyClass()) {
            // work with the class resources
        }
    }
}

Como podemos ver en el código anterior, la segunda opción es más sencilla y más clara. Al utilizar using, nos garantizamos que siempre se va a llamar al método Dispose implementado por nuestra clase.

En el próximo post vamos a analizar el patrón Observador.

Recordá suscribirte aquí para recibir las últimas actualizaciones todas las semanas.

Leave a Reply

Your email address will not be published. Required fields are marked *