En la programación orientada a objetos , el patrón dispose es un patrón de diseño para la gestión de recursos . En este patrón, un recurso es retenido por un objeto y liberado mediante una llamada a un método convencional (normalmente llamado close
, dispose
, free
, release
según el lenguaje) que libera cualquier recurso que el objeto esté reteniendo. Muchos lenguajes de programación ofrecen construcciones de lenguaje para evitar tener que llamar al método dispose explícitamente en situaciones comunes.
El patrón dispose se utiliza principalmente en lenguajes cuyo entorno de ejecución tiene recolección automática de basura (ver la motivación a continuación).
Envolver recursos en objetos es la forma de encapsulación orientada a objetos y es la base del patrón de disposición.
Los recursos se representan normalmente mediante identificadores (referencias abstractas), concretamente por lo general números enteros, que se utilizan para comunicarse con un sistema externo que proporciona el recurso. Por ejemplo, los archivos son proporcionados por el sistema operativo (en concreto, el sistema de archivos ), que en muchos sistemas representa los archivos abiertos con un descriptor de archivo (un número entero que representa el archivo).
Estos identificadores se pueden utilizar directamente, almacenando el valor en una variable y pasándolo como argumento a funciones que utilizan el recurso. Sin embargo, con frecuencia es útil abstraerse del identificador en sí (por ejemplo, si diferentes sistemas operativos representan archivos de manera diferente) y almacenar datos auxiliares adicionales con el identificador, de modo que los identificadores se puedan almacenar como un campo en un registro , junto con otros datos; si se trata de un tipo de datos opaco , esto proporciona ocultamiento de información y el usuario se abstrae de la representación real.
Por ejemplo, en la entrada/salida de archivos de C , los archivos se representan mediante objetos del FILE
tipo (confusamente llamados " identificadores de archivo ": son una abstracción a nivel de lenguaje), que almacena un identificador (del sistema operativo) para el archivo (como un descriptor de archivo ), junto con información auxiliar como el modo de E/S (lectura, escritura) y la posición en el flujo. Estos objetos se crean mediante una llamada fopen
(en términos orientados a objetos, un constructor ), que adquiere el recurso y devuelve un puntero al mismo; el recurso se libera mediante una llamada fclose
a un puntero al FILE
objeto. [1] En código:
ARCHIVO * f = fopen ( nombre_archivo , modo ); // Hacer algo con f. fclose ( f );
Tenga en cuenta que fclose
es una función con un FILE *
parámetro. En la programación orientada a objetos, se trata de un método de instancia en un objeto de archivo, como en Python:
f = open ( nombre_archivo ) # Hacer algo con f. f . close ()
Este es precisamente el patrón de disposición, y solo difiere en la sintaxis y la estructura del código [a] de la apertura y cierre de archivos tradicionales. Otros recursos se pueden gestionar exactamente de la misma manera: se adquieren en un constructor o fábrica y se liberan mediante un método close
o explícito.dispose
El problema fundamental que se pretende resolver con la liberación de recursos es que los recursos son costosos (por ejemplo, puede haber un límite en la cantidad de archivos abiertos) y, por lo tanto, se deben liberar rápidamente. Además, a veces se necesita algún trabajo de finalización, en particular para la E/S, como vaciar los búferes para garantizar que se escriban todos los datos.
Si un recurso es ilimitado o efectivamente ilimitado, y no es necesaria una finalización explícita, no es importante liberarlo y, de hecho, los programas de corta duración a menudo no liberan recursos explícitamente: debido al corto tiempo de ejecución, es poco probable que agoten los recursos y dependen del sistema de ejecución o del sistema operativo para realizar cualquier finalización.
Sin embargo, en general, los recursos deben administrarse (en particular, en el caso de programas de larga duración, programas que utilizan muchos recursos o por razones de seguridad, para garantizar que se escriban los datos). La eliminación explícita significa que la finalización y liberación de los recursos es determinista y rápida: el dispose
método no se completa hasta que se realizan estas tareas.
Una alternativa a exigir la eliminación explícita es vincular la gestión de recursos a la vida útil del objeto : los recursos se adquieren durante la creación del objeto y se liberan durante su destrucción . Este enfoque se conoce como el modismo Adquisición de recursos es inicialización (RAII) y se utiliza en lenguajes con gestión de memoria determinista (por ejemplo, C++ ). En este caso, en el ejemplo anterior, el recurso se adquiere cuando se crea el objeto de archivo y, cuando se sale del ámbito de la variable , se destruye f
el objeto de archivo al que hace referencia y, como parte de esto, se libera el recurso.f
RAII se basa en que la duración de vida de los objetos sea determinista; sin embargo, con la gestión automática de la memoria, la duración de vida de los objetos no es una preocupación del programador: los objetos se destruyen en algún momento después de que ya no se utilizan, sino cuando se abstrae. De hecho, la duración de vida a menudo no es determinista, aunque puede serlo, en particular si se utiliza el recuento de referencias . De hecho, en algunos casos no hay garantía de que los objetos se finalicen alguna vez : cuando el programa termina, puede que no finalice los objetos y, en su lugar, simplemente deje que el sistema operativo recupere memoria; si se requiere la finalización (por ejemplo, para vaciar los búferes), puede producirse una pérdida de datos.
De este modo, al no vincular la gestión de recursos con la vida útil de los objetos, el patrón de eliminación permite liberar recursos rápidamente, al tiempo que brinda flexibilidad de implementación para la gestión de memoria. El costo de esto es que los recursos deben administrarse manualmente, lo que puede ser tedioso y propenso a errores.
Un problema clave con el patrón dispose es que si dispose
no se llama al método, se pierde el recurso. Una causa común de esto es la salida anticipada de una función, debido a un retorno anticipado o una excepción.
Por ejemplo:
def func ( nombre_archivo ) : f = open ( nombre_archivo ) if a : return x f.close ( ) return y
Si la función retorna en el primer retorno, el archivo nunca se cierra y se pierde el recurso.
def func ( filename ): f = open ( filename ) g ( f ) # Hacer algo con f que pueda generar una excepción. f . close ()
Si el código intermedio genera una excepción, la función sale antes y el archivo nunca se cierra, por lo que se pierde el recurso.
Ambos pueden manejarse mediante una try...finally
construcción que garantiza que la cláusula finally siempre se ejecute al salir:
def func ( nombre_archivo ): try : f = open ( nombre_archivo ) # Hacer algo. finally : f.close ( )
De manera más genérica:
Recurso resource = getResource (); try { // Se ha adquirido el recurso; realice acciones con el recurso. ... } finally { // Libere el recurso, incluso si se lanzó una excepción. resource . dispose (); }
La try...finally
construcción es necesaria para la seguridad adecuada de las excepciones , ya que el finally
bloque permite la ejecución de la lógica de limpieza independientemente de si se lanza o no una excepción en el try
bloque.
Una desventaja de este enfoque es que requiere que el programador agregue explícitamente código de limpieza en un finally
bloque. Esto genera un aumento del tamaño del código y, si no se hace, se producirá una pérdida de recursos en el programa.
Para que el uso seguro del patrón dispose sea menos detallado, varios lenguajes tienen algún tipo de soporte incorporado para los recursos almacenados y liberados en el mismo bloque de código .
El lenguaje C# presenta la using
declaración [2] que llama automáticamente al Dispose
método en un objeto que implementa la IDisposable
interfaz :
usando ( Recurso recurso = GetResource ( )) { // Realizar acciones con el recurso. ... }
que es igual a:
Recurso resource = GetResource () try { // Realizar acciones con el recurso. ... } finally { // Es posible que el recurso no se haya adquirido o que ya se haya liberado if ( resource != null ) (( IDisposable ) resource ). Dispose (); }
De manera similar, el lenguaje Python tiene una with
declaración que se puede utilizar con un efecto similar con un objeto de administrador de contexto . El protocolo de administrador de contexto requiere implementar métodos __enter__
y __exit__
que sean llamados automáticamente por la with
construcción de la declaración, para evitar la duplicación de código que de otra manera ocurriría con el patrón try
/ . [3]finally
con resource_context_manager () como recurso : # Realizar acciones con el recurso. ... # Realizar otras acciones en las que se garantiza que el recurso será desasignado. ...
El lenguaje Java introdujo una nueva sintaxis llamada try
-with-resources en la versión 7 de Java. [4] Se puede utilizar en objetos que implementan la interfaz AutoCloseable (que define el método close()):
try ( OutputStream x = new OutputStream (...)) { // Hacer algo con x } catch ( IOException ex ) { // Manejar excepción // El recurso x se cierra automáticamente } // intentar
Más allá del problema clave de la correcta gestión de recursos en presencia de devoluciones y excepciones, y la gestión de recursos basada en el montón (eliminar objetos en un ámbito diferente de donde se crean), existen muchas otras complejidades asociadas con el patrón dispose. Estos problemas se evitan en gran medida con RAII . Sin embargo, en el uso común y simple, estas complejidades no surgen: adquirir un solo recurso, hacer algo con él, liberarlo automáticamente.
Un problema fundamental es que tener un recurso ya no es una invariante de clase (el recurso se mantiene desde la creación del objeto hasta que se desecha, pero el objeto sigue activo en este punto), por lo que el recurso puede no estar disponible cuando el objeto intenta usarlo, por ejemplo, al intentar leer desde un archivo cerrado. Esto significa que todos los métodos del objeto que utilizan el recurso pueden fallar, concretamente, normalmente devolviendo un error o lanzando una excepción. En la práctica, esto es menor, ya que el uso de recursos normalmente también puede fallar por otras razones (por ejemplo, al intentar leer más allá del final de un archivo), por lo que estos métodos ya pueden fallar, y no tener un recurso solo agrega otro posible fallo. Una forma estándar de implementar esto es agregar un campo booleano al objeto, llamado disposed
, que se establece en verdadero mediante dispose
, y se verifica mediante una cláusula de protección para todos los métodos (que utilizan el recurso), lanzando una excepción (como ObjectDisposedException
en .NET) si el objeto ha sido desechado. [5]
Además, es posible llamar dispose
a un objeto más de una vez. Si bien esto puede indicar un error de programación (cada objeto que contiene un recurso debe desecharse exactamente una vez), es más simple, más robusto y, por lo tanto, generalmente preferible que dispose
sea idempotente (lo que significa "llamar varias veces es lo mismo que llamar una vez"). [5] Esto se implementa fácilmente utilizando el mismo disposed
campo booleano y comprobándolo en una cláusula de protección al comienzo de dispose
, en ese caso retornando inmediatamente, en lugar de lanzar una excepción. [5] Java distingue los tipos desechables (aquellos que implementan AutoCloseable) de los tipos desechables donde dispose es idempotente (el subtipo Closeable).
La eliminación en presencia de herencia y composición de objetos que contienen recursos tiene problemas análogos a la destrucción/finalización (a través de destructores o finalizadores). Además, dado que el patrón dispose generalmente no tiene soporte de lenguaje para esto, es necesario un código repetitivo . En primer lugar, si una clase derivada anula un dispose
método en la clase base, el método anulador en la clase derivada generalmente necesita llamar al dispose
método en la clase base, para liberar adecuadamente los recursos almacenados en la base. En segundo lugar, si un objeto tiene una relación "tiene un" con otro objeto que contiene un recurso (es decir, si un objeto usa indirectamente un recurso a través de otro objeto que usa directamente un recurso), ¿debería descartarse el objeto que usa indirectamente? Esto corresponde a si la relación es de propiedad ( composición de objetos ) o de visualización ( agregación de objetos ), o incluso solo de comunicación ( asociación ), y se encuentran ambas convenciones (el usuario indirecto es responsable del recurso o no es responsable). Si el uso indirecto es responsable del recurso, éste debe ser desechable, y disponer de los objetos propios cuando se desecha (de forma análoga a destruir o finalizar los objetos propios).
La composición (posesión) proporciona encapsulación (solo se debe rastrear el objeto que se usa), pero a costa de una complejidad considerable cuando hay más relaciones entre los objetos, mientras que la agregación (visualización) es considerablemente más simple, a costa de la falta de encapsulación. En .NET , la convención es que solo el usuario directo de los recursos sea responsable: "Debe implementar IDisposable solo si su tipo usa recursos no administrados directamente". [6] Consulte la administración de recursos para obtener detalles y más ejemplos.
this
, self
en lugar de como funciones que toman un parámetro explícito.