Articulo de referencia

Patrón de desecho

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 objeto retiene un recurso y lo libera median...

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 objeto retiene un recurso y lo libera mediante la llamada a un método convencional ( generalmente llamado ` dispose` , ` removeclosedisposefreerelease

El patrón Dispose se utiliza principalmente en lenguajes cuyo entorno de ejecución cuenta con recolección automática de basura (véase la motivación a continuación).

Motivación

Encapsular recursos en objetos

Encapsular recursos en objetos es la forma de encapsulación orientada a objetos y es la base del patrón Dispose.

Los recursos suelen representarse mediante identificadores (referencias abstractas), generalmente 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 (específicamente 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 usar directamente, almacenando el valor en una variable y pasándolo como argumento a las funciones que utilizan el recurso. Sin embargo, suele ser útil abstraerse del identificador en sí (por ejemplo, si los distintos sistemas operativos representan los archivos de forma diferente) y almacenar datos auxiliares adicionales junto con el identificador, de modo que los identificadores se pueden almacenar como un campo en un registro , junto con otros datos; si se trata de un tipo de dato opaco , esto proporciona ocultación 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 FILEtipo (llamados confusamente " manejadores de archivo ": estos son una abstracción a nivel de lenguaje), que almacena un manejador (del sistema operativo) al 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 llamando a fopen(en términos orientados a objetos, un constructor ), que adquiere el recurso y devuelve un puntero al mismo; el recurso se libera llamando fclosea sobre un puntero al FILEobjeto. [ 1 ] En código:

#include <stdio.h>ARCHIVO * f = fopen ( nombre_archivo , modo ); // Hacer algo con f. fclose ( f );

Tenga en cuenta que fclosese trata de una función con un FILE*parámetro. En la programación orientada a objetos, esto es en cambio un método de instancia en un objeto de archivo, como en Python:

from typing import TextIOf : TextIO = open ( filename ) # 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 el cierre de archivos tradicionales. Otros recursos pueden gestionarse exactamente de la misma manera: adquiriéndose en un constructor o fábrica, y liberándose mediante un closemétodo explícito dispose.

Liberación inmediata

El problema fundamental que se pretende solucionar al liberar recursos es que estos son costosos (por ejemplo, puede haber un límite en el número de archivos abiertos) y, por lo tanto, deben liberarse rápidamente. Además, en ocasiones se requiere algún trabajo de finalización, sobre todo para las operaciones de entrada/salida, como vaciar los búferes para asegurar que todos los datos se escriban correctamente.

Si un recurso es ilimitado o prácticamente ilimitado, y no es necesaria una finalización explícita, no es importante liberarlo. De hecho, los programas de corta duración a menudo no liberan recursos de forma explícita: debido a su breve tiempo de ejecución, es poco probable que agoten los recursos y confían en que el sistema de tiempo de ejecución o el sistema operativo se encarguen de cualquier finalización.

Sin embargo, en general, los recursos deben gestionarse (especialmente en programas de larga duración, programas que utilizan muchos recursos o por motivos de seguridad, para garantizar que los datos se guarden correctamente). La eliminación explícita implica que la finalización y liberación de los recursos sea determinista e inmediata: el disposemétodo no se completa hasta que se hayan realizado estas acciones.

Una alternativa a la necesidad de una liberación explícita consiste en vincular la gestión de recursos al ciclo de vida 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 patrón RAII ( Resource Acquisition Is Initialization ) 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 al crear el objeto de archivo y, al fsalir del ámbito de la variable, se destruye el objeto de archivo al que fhace referencia y, como parte de este proceso, se libera el recurso.

RAII se basa en que la vida útil de los objetos sea determinista; sin embargo, con la gestión automática de memoria, la vida útil de los objetos no es una preocupación para el programador: los objetos se destruyen en algún momento después de que ya no se utilizan, pero el momento en que se abstrae. De hecho, la vida útil a menudo no es determinista, aunque puede serlo, especialmente si se utiliza el conteo de referencias . 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, permita que el sistema operativo recupere la 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 al ciclo de vida de los objetos, el patrón Dispose permite liberar los recursos rápidamente, a la vez que ofrece flexibilidad en la implementación de la gestión de memoria. El inconveniente es que los recursos deben gestionarse manualmente, lo que puede resultar tedioso y propenso a errores.

Salida anticipada

Un problema clave del patrón Dispose es que, si disposeno se llama al método, se produce una fuga de recursos. Una causa común de esto es la salida prematura de una función, debido a un retorno anticipado o una excepción.

Por ejemplo:

from typing import Any , TextIOdef func ( filename : str ) -> Any : f = open ( filename ) if a : return x f . close () return y

Si la función devuelve un valor en el primer retorno, el archivo nunca se cierra y se produce una fuga de recursos.

from typing import TextIOdef func ( filename : str ) -> None : f : TextIO = open ( filename ) g ( f ) # Hacer algo con f que puede generar una excepción. f . close ()

Si el código intermedio genera una excepción, la función finaliza prematuramente y el archivo nunca se cierra, por lo que se produce una fuga de recursos.

Ambas situaciones pueden ser manejadas por una try...finallyestructura que garantiza que la cláusula finally siempre se ejecute al salir:

from typing import TextIOdef func ( filename : str ) -> None : try : f : TextIO = open ( filename ) # Hacer algo. finally : f . close ()

De forma más genérica:

Resource resource = getResource (); try { // El recurso ha sido adquirido; realizar acciones con el recurso. ... } finally { // Liberar el recurso, incluso si se produjo una excepción. resource.dispose ( ) ; }

Esta try...finallyestructura es necesaria para garantizar la seguridad ante excepciones , ya que el finallybloque permite la ejecución de la lógica de limpieza independientemente de si se produce o no una excepción en dicho trybloque.

Una desventaja de este enfoque es que requiere que el programador agregue explícitamente código de limpieza en un finallybloque. Esto provoca un aumento excesivo del tamaño del código y, de no hacerlo, se producirá una fuga de recursos en el programa.

Construcciones del lenguaje

Para que el uso seguro del patrón Dispose sea menos verboso, varios lenguajes cuentan con algún tipo de soporte incorporado para los recursos que se mantienen y liberan en el mismo bloque de código .

En C++ , la eliminación de recursos se realiza tradicionalmente mediante el patrón de adquisición de recursos mediante inicialización (RAII), por ejemplo, utilizando punteros inteligentes . Una vez que un recurso sale del ámbito, se llama a su destructor .

El lenguaje C# incluye la usinginstrucción [ 2 ] que llama automáticamente al Dispose()método en un objeto que implementa la System.IDisposableinterfaz :

using ( Resource resource = GetResource ()) { // Realizar acciones con el recurso. // ... }

lo cual es igual a:

usando el sistema ;Resource 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 Pythonwith tiene una instrucción que puede usarse con un efecto similar con un objeto gestor de contexto . El protocolo del gestor de contexto requiere la implementación __enter__de __exit__métodos que son llamados automáticamente por la withconstrucción de la instrucción, para evitar la duplicación de código que de otro modo ocurriría con el patrón try/ . [ 3 ]finally

con resource_context_manager () como recurso : # Realizar acciones con el recurso. ... # Realizar otras acciones donde se garantiza que el recurso se desasignará. ...

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 java.lang.AutoCloseableinterfaz (que define el método close()):

import java.io.IOException ; import java.io.OutputStream ;try ( OutputStream config = new OutputStream ( "configs/config.txt" )) { // Hacer algo con 'config' } catch ( IOException e ) { // Manejar la excepción// El recurso 'config' se cierra automáticamente }

En Rust , la semántica de eliminación se realiza mediante un rasgo std::ops::Drop, que destruirá automáticamente el objeto después de salir del ámbito. Se puede llamar manualmente mediante std::mem::drop().

Problemas

Más allá del problema fundamental 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 distinto al de su creación), existen muchas otras complejidades asociadas al patrón de eliminación. Estos problemas se evitan en gran medida con RAII . Sin embargo, en el uso común y sencillo, estas complejidades no se presentan: se adquiere un único recurso, se realiza alguna acción con él y se libera 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 elimina, pero el objeto sigue activo en ese momento), por lo que el recurso puede no estar disponible cuando el objeto intenta usarlo, por ejemplo, al intentar leer un archivo cerrado. Esto significa que todos los métodos del objeto que usan el recurso pueden fallar, concretamente, generalmente devolviendo un error o generando una excepción. En la práctica, esto es menor, ya que el uso de recursos 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 podrían fallar, y no tener un recurso solo añade 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 comprueba mediante una cláusula guard en todos los métodos (que usan el recurso), generando una excepción (como ObjectDisposedExceptionen .NET) si el objeto se ha eliminado. [ 5 ]

Además, es posible llamar disposea un objeto más de una vez. Si bien esto puede indicar un error de programación (cada objeto que contiene un recurso debe ser liberado exactamente una vez), es más simple, más robusto y, por lo tanto, generalmente preferible que disposesea idempotente (lo que significa "llamar varias veces es lo mismo que llamar una vez"). [ 5 ] Esto se implementa fácilmente usando el mismo disposedcampo booleano y comprobándolo en una cláusula guard al comienzo de dispose, en ese caso devolviendo inmediatamente, en lugar de generar una excepción. [ 5 ] Java distingue los tipos desechables (aquellos que implementan AutoCloseable[ 6 ] ) de los tipos desechables donde dispose es idempotente (el subtipo Closeable[ 7 ] ).

La disposición de recursos en presencia de herencia y composición de objetos que contienen recursos presenta problemas análogos a la destrucción/finalización (mediante destructores o finalizadores). Además, dado que el patrón `dispose` generalmente no cuenta con soporte del lenguaje para esto, se requiere código repetitivo . En primer lugar, si una clase derivada sobrescribe un disposemétodo en la clase base, el método sobrescritor en la clase derivada generalmente necesita llamar al disposemétodo en la clase base para liberar correctamente los recursos que contiene la base. En segundo lugar, si un objeto tiene una relación de "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 lo usa directamente), ¿debe el objeto que usa indirectamente ser desechable? Esto corresponde a si la relación es de propiedad ( composición de objetos ), de visualización ( agregación de objetos ) o incluso simplemente de comunicación ( asociación ), y se encuentran ambas convenciones (el usuario indirecto es responsable del recurso o no lo es). Si el uso indirecto es responsable del recurso, debe ser desechable y deshacerse de los objetos de los que es propietario cuando se deshace (análogo a la destrucción o finalización de objetos de propiedad).

La composición (propiedad) proporciona encapsulación (solo es necesario rastrear el objeto que se utiliza), pero a costa de una complejidad considerable cuando existen relaciones adicionales entre objetos, mientras que la agregación (visualización) es considerablemente más simple, a costa de carecer 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 utiliza recursos no administrados directamente". [ 8 ] Consulte la administración de recursos para obtener más detalles y ejemplos.

Véase también

Notas

  1. En la programación basada en clases , los métodos se definen en una clase, utilizando un parámetro implícitothisoself, en lugar de como funciones que toman un parámetro explícito.

Referencias

  1. Referencia de definiciones básicas, Especificación única de UNIX , Versión 5 de The Open Groupstdio.h  
  2. Microsoft MSDN: Instrucción using (Referencia de C#)
  3. Guido van Rossum , Nick Coghlan (13 de junio de 2011). "PEP 343: La declaración "with" . Python Software Foundation.
  4. Tutorial de Oracle Java: La instrucción try-with-resources
  5. 1 2 3 "Patrón de eliminación" .
  6. autocierre
  7. Se puede cerrar
  8. "Interfaz desechable" . Consultado el 9 de diciembre de 2024 .

Lecturas adicionales

  • Red de desarrolladores de Microsoft: Patrón de eliminación