El patrón de grupo de objetos es un patrón de diseño de creación de software que utiliza un conjunto de objetos inicializados que se mantienen listos para usar, un " grupo ", en lugar de asignarlos y destruirlos a pedido. Un cliente del grupo solicitará un objeto del grupo y realizará operaciones en el objeto devuelto. Cuando el cliente ha terminado, devuelve el objeto al grupo en lugar de destruirlo ; esto se puede hacer de forma manual o automática.
Los grupos de objetos se utilizan principalmente para el rendimiento: en algunas circunstancias, los grupos de objetos mejoran significativamente el rendimiento. Las agrupaciones de objetos complican la vida útil de los objetos , ya que los objetos obtenidos de una agrupación y devueltos a ella no se crean ni se destruyen realmente en este momento y, por lo tanto, requieren cuidado en la implementación.
Descripción
Cuando es necesario trabajar con una gran cantidad de objetos cuya instanciación es particularmente costosa y cada objeto solo se necesita durante un breve período de tiempo, el rendimiento de una aplicación completa puede verse afectado negativamente. Un patrón de diseño de grupo de objetos puede considerarse deseable en casos como estos.
El patrón de diseño del grupo de objetos crea un conjunto de objetos que se pueden reutilizar. Cuando se necesita un nuevo objeto, se solicita al grupo. Si un objeto preparado previamente está disponible, se devuelve inmediatamente, evitando el costo de creación de instancias. Si no hay objetos presentes en el grupo, se crea y se devuelve un nuevo elemento. Cuando el objeto se ha utilizado y ya no se necesita, se devuelve al grupo, lo que permite volver a utilizarlo en el futuro sin repetir el costoso proceso de creación de instancias computacionalmente. Es importante tener en cuenta que una vez que se ha utilizado y devuelto un objeto, las referencias existentes dejarán de ser válidas.
En algunos grupos de objetos, los recursos son limitados, por lo que se especifica un número máximo de objetos. Si se alcanza este número y se solicita un nuevo artículo, se puede lanzar una excepción o el hilo se bloqueará hasta que un objeto se libere de nuevo en el grupo.
El patrón de diseño del grupo de objetos se utiliza en varios lugares de las clases estándar de .NET Framework. Un ejemplo es el proveedor de datos de .NET Framework para SQL Server. Como las conexiones de la base de datos de SQL Server pueden ser lentas de crear, se mantiene un grupo de conexiones. Cerrar una conexión en realidad no renuncia al vínculo a SQL Server. En cambio, la conexión se mantiene en un grupo del que se puede recuperar cuando se solicita una nueva conexión. Esto aumenta sustancialmente la velocidad de realizar las conexiones.
Beneficios
La agrupación de objetos puede ofrecer un aumento significativo del rendimiento en situaciones en las que el costo de inicializar una instancia de clase es alto y la tasa de instanciación y destrucción de una clase es alta; en este caso, los objetos se pueden reutilizar con frecuencia y cada reutilización ahorra una cantidad significativa de hora. La agrupación de objetos requiere recursos: memoria y posiblemente otros recursos, como sockets de red, por lo que es preferible que el número de instancias en uso en cualquier momento sea bajo, pero esto no es necesario.
El objeto agrupado se obtiene en un tiempo predecible cuando la creación de los nuevos objetos (especialmente a través de la red) puede llevar un tiempo variable. Estos beneficios se aplican principalmente a objetos que son costosos con respecto al tiempo, como conexiones de bases de datos, conexiones de socket, subprocesos y objetos gráficos grandes como fuentes o mapas de bits.
En otras situaciones, la agrupación de objetos simples (que no contienen recursos externos, pero solo ocupan memoria) puede no ser eficiente y podría disminuir el rendimiento. [1] En caso de agrupación de memoria simple, la técnica de administración de memoria de asignación de bloques es más adecuada, ya que el único objetivo es minimizar el costo de asignación y desasignación de memoria al reducir la fragmentación.
Implementación
Los grupos de objetos se pueden implementar de forma automatizada en lenguajes como C ++ mediante punteros inteligentes . En el constructor del puntero inteligente, se puede solicitar un objeto del grupo, y en el destructor del puntero inteligente, el objeto se puede devolver al grupo. En los lenguajes de recolección de basura, donde no hay destructores (que se garantiza que se llamarán como parte de un desenrollado de pila), los grupos de objetos deben implementarse manualmente, solicitando explícitamente un objeto de la fábrica y devolviendo el objeto llamando a un método de disposición. (como en el patrón de eliminación ). Usar un finalizador para hacer esto no es una buena idea, ya que generalmente no hay garantías sobre cuándo (o si) se ejecutará el finalizador. En su lugar, se debe usar "probar ... finalmente" para garantizar que la obtención y liberación del objeto sea neutral para las excepciones.
Los grupos de objetos manuales son fáciles de implementar, pero más difíciles de usar, ya que requieren la administración manual de la memoria de los objetos del grupo.
Manejo de piscinas vacías
Los grupos de objetos emplean una de las tres estrategias para manejar una solicitud cuando no hay objetos de repuesto en el grupo.
- No proporcionar un objeto (y devolver un error al cliente).
- Asignar un nuevo objeto, aumentando así el tamaño de la piscina. Las piscinas que hacen esto generalmente le permiten establecer la marca de agua alta (la cantidad máxima de objetos que se han usado).
- En un entorno multiproceso , un grupo puede bloquear al cliente hasta que otro hilo devuelva un objeto al grupo.
Trampas
Se debe tener cuidado para garantizar que el estado de los objetos devueltos al grupo se restablezca a un estado sensible para el próximo uso del objeto, de lo contrario, el objeto puede estar en un estado inesperado para el cliente, lo que puede provocar que falle. El grupo es responsable de restablecer los objetos, no los clientes. Los grupos de objetos llenos de objetos con un estado peligrosamente obsoleto a veces se denominan pozos negros de objetos y se consideran un antipatrón .
Es posible que el estado obsoleto no siempre sea un problema; se vuelve peligroso cuando hace que el objeto se comporte inesperadamente. Por ejemplo, un objeto que representa detalles de autenticación puede fallar si el indicador "autenticado exitosamente" no se restablece antes de ser reutilizado, ya que indica que un usuario está autenticado (posiblemente como otra persona) cuando no lo está. Sin embargo, no restablecer un valor utilizado solo para la depuración, como la identidad del último servidor de autenticación utilizado, puede no plantear problemas.
Un restablecimiento inadecuado de los objetos puede provocar fugas de información. Los objetos que contienen datos confidenciales (por ejemplo, los números de la tarjeta de crédito de un usuario) deben borrarse antes de pasarlos a nuevos clientes; de lo contrario, los datos pueden divulgarse a una parte no autorizada.
Si el grupo es utilizado por varios subprocesos, es posible que necesite los medios para evitar que subprocesos paralelos intenten reutilizar el mismo objeto en paralelo. Esto no es necesario si los objetos agrupados son inmutables o seguros para subprocesos.
Crítica
Algunas publicaciones no recomiendan el uso de la agrupación de objetos con ciertos lenguajes, como Java , especialmente para objetos que solo usan memoria y no contienen recursos externos (como conexiones a la base de datos). Los oponentes suelen decir que la asignación de objetos es relativamente rápida en los lenguajes modernos con recolectores de basura ; mientras que el operador new
necesita sólo instrucciones diez, el clásico new
- delete
par que se encuentra en los diseños compartidos requiere cientos de ellos como lo hace el trabajo más complejo. Además, la mayoría de los recolectores de basura escanean referencias de objetos "en vivo", y no la memoria que estos objetos usan para su contenido. Esto significa que cualquier número de objetos "muertos" sin referencias se puede descartar con un coste reducido. Por el contrario, mantener una gran cantidad de objetos "activos" pero no utilizados aumenta la duración de la recolección de basura. [1]
Ejemplos de
Ir
El siguiente código de Go inicializa un grupo de recursos de un tamaño específico (inicialización simultánea) para evitar problemas de carrera de recursos a través de los canales y, en el caso de un grupo vacío, establece el procesamiento del tiempo de espera para evitar que los clientes esperen demasiado.
// grupo de paquetes grupo de paquetesimportar ( "errores" "registro" "matemáticas / rand" "sincronización" "tiempo" )const getResMaxTime = 3 * tiempo . Segundovar ( ErrPoolNotExist = errores . Nuevo ( "el grupo no existe" ) ErrGetResTimeout = errores . Nuevo ( "tiempo de espera de recurso agotado" ) )// Recursos Tipo de Recurso struct { resid int }// NewResource Simula la creación lenta de inicialización de recursos // (p. Ej., Conexión TCP, adquisición de clave simétrica SSL, autenticación auth consume mucho tiempo) func NewResource ( id int ) * Resource { time . Dormir ( 500 * tiempo . Milisegundos ) retorno y recurso { resId : id } }// Do Los recursos de simulación consumen mucho tiempo y el consumo aleatorio es de 0 ~ 400ms func ( r * Resource ) Do ( workId int ) { time . Sueño ( tiempo . Duración ( rand . Intn ( 5 )) * 100 * tiempo . Milisegundos ) registro . Printf ( "usando el recurso #% d trabajo terminado% d fin \ n" , r . ResId , workId ) }// Pool basado en la implementación del canal Go, para evitar el tipo de problema de estado de carrera de recursos Pool chan * Resource// Se crea un nuevo grupo de recursos del tamaño especificado // Los recursos se crean al mismo tiempo para ahorrar tiempo de inicialización de recursos. Func New ( size int ) Pool { p : = make ( Pool , size ) wg : = new ( sync . WaitGroup ) wg . Agregue ( tamaño ) para i : = 0 ; i < tamaño ; i ++ { go func ( resId int ) { p <- NewResource ( resId ) wg . Hecho () } ( i ) } wg . Espera () devuelve p }// GetResource basado en el canal, se evita el estado de carrera de recursos y se establece el tiempo de espera de adquisición de recursos para la función de grupo vacía ( p Pool ) GetResource () ( r * Resource , err error ) { select { case r : = <- p : return r , caso nulo <- tiempo . Después de ( getResMaxTime ): return nil , ErrGetResTimeout } } // GiveBackResource devuelve recursos al grupo de recursos func ( p Pool ) GiveBackResource ( r * Resource ) error { if p == nil { return ErrPoolNotExist } p <- r return nil }// paquete principal paquete principalimportar ( "github.com/tkstorm/go-design/creational/object-pool/pool" "log" "sincronizar" )func main () { // Inicializa un grupo de cinco recursos, // que se puede ajustar a 1 o 10 para ver el tamaño de la diferencia : = 5 p : = grupo . Nuevo ( tamaño )// Invoca un recurso para realizar el trabajo de identificación doWork : = func ( workId int , wg * sync . WaitGroup ) { diferir wg . Done () // Obtiene el recurso del grupo de recursos res , err : = p . GetResource () if err ! = Nil { log . Println ( err ) return } // Recursos para devolver diferir p . GiveBackResource ( res ) // Usa recursos para manejar el trabajo res . Do ( workId ) }// Simular 100 procesos concurrentes para obtener recursos del grupo de activos num : = 100 wg : = new ( sync . WaitGroup ) wg . Sumar ( num ) para i : = 0 ; i < num ; i ++ { go doWork ( i , wg ) } wg . Espera () }
C#
En la biblioteca de clases base de .NET hay algunos objetos que implementan este patrón. System.Threading.ThreadPool
está configurado para tener un número predefinido de subprocesos para asignar. Cuando se devuelven los subprocesos, están disponibles para otro cálculo. Por lo tanto, uno puede usar hilos sin pagar el costo de creación y eliminación de hilos.
A continuación, se muestra el código básico del patrón de diseño del grupo de objetos implementado con C #. Por brevedad, las propiedades de las clases se declaran utilizando la sintaxis de propiedad implementada automáticamente en C # 3.0. Estos podrían reemplazarse con definiciones de propiedades completas para versiones anteriores del lenguaje. El grupo se muestra como una clase estática, ya que es inusual que se requieran varios grupos. Sin embargo, es igualmente aceptable usar clases de instancia para grupos de objetos.
namespace DesignPattern.Objectpool { // La clase PooledObject es el tipo que es costoso o lento de instanciar, // o que tiene disponibilidad limitada, por lo que debe mantenerse en el grupo de objetos. público de clase PooledObject { privado DateTime _createdAt = DateTime . Ahora ; pública DateTime CreatedAt { conseguir { volver _createdAt ; } } cadena pública TempData { obtener ; establecer ; } } // La clase Pool controla el acceso a los objetos agrupados. Mantiene una lista de objetos disponibles y una // colección de objetos que se han obtenido del grupo y están en uso. El grupo asegura que los objetos liberados // se devuelvan a un estado adecuado, listos para su reutilización. pool de clases estáticas públicas { Lista estática privada < PooledObject > _available = new List < PooledObject > (); Lista estática privada < PooledObject > _inUse = new List < PooledObject > (); pública estática PooledObject GetObject () { bloqueo ( _available ) { si ( _available . Count =! 0 ) { PooledObject po = _available [ 0 ]; _inUse . Agregar ( po ); _disponible . RemoveAt ( 0 ); return po ; } else { PooledObject po = new PooledObject (); _inUse . Agregar ( po ); return po ; } } } ReleaseObject vacío estático público ( PooledObject po ) { CleanUp ( po ); bloqueo ( _disponible ) { _disponible . Agregar ( po ); _inUse . Eliminar ( po ); } } Private static void CleanUp ( PooledObject po ) { po . TempData = nulo ; } } }
En el código anterior, el PooledObject tiene propiedades para el momento en que se creó, y otra, que el cliente puede modificar, que se restablece cuando el PooledObject se libera de nuevo al grupo. Se muestra el proceso de limpieza, en el momento de la liberación de un objeto, asegurando que esté en un estado válido antes de que pueda ser solicitado al grupo nuevamente.
Java
Java admite la agrupación de subprocesos a través de java.util.concurrent.ExecutorService
y otras clases relacionadas. El servicio ejecutor tiene un cierto número de subprocesos "básicos" que nunca se descartan. Si todos los subprocesos están ocupados, el servicio asigna la cantidad permitida de subprocesos adicionales que luego se descartan si no se utilizan durante el tiempo de vencimiento determinado. Si no se permiten más subprocesos, las tareas se pueden colocar en la cola. Finalmente, si esta cola puede ser demasiado larga, se puede configurar para suspender el subproceso solicitante.
clase pública PooledObject { public String temp1 ; public String temp2 ; public String temp3 ;public String getTemp1 () { return temp1 ; } public void setTemp1 ( String temp1 ) { this . temp1 = temp1 ; } public String getTemp2 () { return temp2 ; } public void setTemp2 ( String temp2 ) { this . temp2 = temp2 ; } public String getTemp3 () { return temp3 ; } public void setTemp3 ( String temp3 ) { this . temp3 = temp3 ; } }
clase pública PooledObjectPool { tiempo expTime largo estático privado = 6000 ; // 6 segundos public static HashMap < PooledObject , Long > disponible = new HashMap < PooledObject , Long > (); public static HashMap < PooledObject , Long > inUse = new HashMap < PooledObject , Long > (); pública sincronizado estático PooledObject getObject () { larga ahora = Sistema . currentTimeMillis (); if ( ! available . isEmpty ()) { for ( Map . Entry < PooledObject , Long > entry : available . entrySet ()) { if ( ahora - entrada . getValue () > expTime ) { // el objeto ha expirado popElement ( disponible ); } else { PooledObject po = popElement ( disponible , entrada . getKey ()); empujar ( inUse , po , ahora ); return po ; } } }// o no hay PooledObject disponible o cada uno ha expirado, así que devuelve uno nuevo return createPooledObject ( ahora ); } objeto agrupado estático sincronizado privado createPooledObject ( largo ahora ) { PooledObject po = new PooledObject (); empujar ( inUse , po , ahora ); return po ; } push vacío estático sincronizado privado ( HashMap < PooledObject , Long > map , PooledObject po , long ahora ) { map . poner ( po , ahora ); } releaseObject vacío estático público ( pooledObject po ) { cleanUp ( po ); disponible . poner ( po , System . currentTimeMillis ()); inUse . eliminar ( po ); } popElement de PooledObject estático privado ( HashMap < PooledObject , Long > map ) { Map . Entrada < PooledObject , Long > entrada = mapa . entrySet (). iterador (). siguiente (); PooledObject clave = entrada . getKey (); // Valor largo = entry.getValue (); mapear . eliminar ( entrada . getKey ()); tecla de retorno ; } popElement de PooledObject estático privado ( HashMap < PooledObject , Long > map , clave PooledObject ) { map . quitar ( clave ); tecla de retorno ; } limpieza de vacío estático público ( PooledObject po ) { po . setTemp1 ( nulo ); po . setTemp2 ( nulo ); po . setTemp3 ( nulo ); } }
Ver también
Notas
- ↑ a b Goetz, Brian (27 de septiembre de 2005). "Teoría y práctica de Java: leyendas del rendimiento urbano, revisitado" . IBM . IBM developerWorks. Archivado desde el original el 14 de febrero de 2012 . Consultado el 15 de marzo de 2021 .
Referencias
- Kircher, Michael; Prashant Jain (4 de julio de 2002). "Patrón de agrupación" (PDF) . EuroPLoP 2002 . Alemania . Consultado el 9 de junio de 2007 .
- Goldshtein, Sasha; Zurbalev, Dima; Flatow, Ido (2012). Rendimiento Pro .NET: Optimice sus aplicaciones C # . Presione. ISBN 978-1-4302-4458-5.
enlaces externos
- Artículo de OODesign
- Mejora del rendimiento con la agrupación de objetos (Microsoft Developer Network)
- Artículo de Developer.com
- Entrada del repositorio de patrones de Portland
- Apache Commons Pool: un mini marco para implementar correctamente la agrupación de objetos en Java
- Patrones de programación de juegos: grupo de objetos