stringtranslate.com

Patrón de grupo de objetos

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 su uso (un " grupo "), en lugar de asignarlos y destruirlos según demanda. 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 mejorar el rendimiento: en algunas circunstancias, los grupos de objetos mejoran significativamente el rendimiento. Los grupos de objetos complican la vida útil de los objetos , ya que los objetos obtenidos y devueltos a un grupo en realidad no se crean ni se destruyen en este momento y, por lo tanto, requieren cuidado en la implementación.

Descripción

Cuando es necesario trabajar con numerosos objetos cuya creación de instancias es particularmente costosa y cada objeto sólo se necesita durante un corto período de tiempo, el rendimiento de una aplicación completa puede verse afectado negativamente. En casos como estos, puede considerarse deseable un patrón de diseño de grupo de objetos.

El patrón de diseño del grupo de objetos crea un conjunto de objetos que pueden reutilizarse. Cuando se necesita un nuevo objeto, se solicita del grupo. Si un objeto previamente preparado está disponible, se devuelve inmediatamente, evitando el costo de instanciación. 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 es necesario, 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 elemento, se puede generar una excepción o el subproceso se bloqueará hasta que se libere un objeto nuevamente al 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 .NET Framework para SQL Server. Como las conexiones de bases de datos de SQL Server pueden tardar en crearse, se mantiene un grupo de conexiones. Cerrar una conexión en realidad no renuncia al vínculo con SQL Server. En cambio, la conexión se mantiene en un grupo, del cual se puede recuperar cuando se solicita una nueva conexión. Esto aumenta sustancialmente la velocidad de realización de 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 creación de instancias 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 tiempo. La agrupación de objetos requiere recursos: memoria y posiblemente otros recursos, como sockets de red, y por lo tanto es preferible que la cantidad de instancias en uso en un momento dado sea baja, pero esto no es necesario.

El objeto agrupado se obtiene en un tiempo predecible cuando la creación de nuevos objetos (especialmente a través de la red) puede llevar un tiempo variable. Estos beneficios son válidos principalmente para objetos que son costosos en términos de 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, sino que solo ocupan memoria) puede no ser eficiente y podría disminuir el rendimiento. [1] En el caso de una 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 la asignación y desasignación de memoria reduciendo 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 desenredado de la 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 eliminació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 utilizar "intentar... finalmente" para garantizar que obtener y liberar el objeto sea neutral en cuanto a excepciones.

Los grupos de objetos manuales son fáciles de implementar, pero más difíciles de usar, ya que requieren una gestión manual de la memoria de los objetos del grupo.

Manejo de piscinas vacías

Los grupos de objetos emplean una de tres estrategias para manejar una solicitud cuando no hay objetos de repuesto en el grupo.

  1. No se puede proporcionar un objeto (y devolver un error al cliente).
  2. Asigne un nuevo objeto, aumentando así el tamaño del grupo. Las piscinas que hacen esto generalmente le permiten establecer la marca de agua máxima (la cantidad máxima de objetos jamás utilizados).
  3. En un entorno multiproceso , un grupo puede bloquear al cliente hasta que otro subproceso devuelva un objeto al grupo.

Escollos

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 estancado 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 reutilizarlo, ya que indica que un usuario está autenticado (posiblemente como otra persona) cuando no lo está. Sin embargo, no restablecer un valor utilizado sólo para la depuración, como la identidad del último servidor de autenticación utilizado, puede no plantear problemas.

El restablecimiento inadecuado de objetos puede provocar fugas de información. Los objetos que contienen datos confidenciales (por ejemplo, los números de tarjetas de crédito de un usuario) deben borrarse antes de pasarse a nuevos clientes; de lo contrario, los datos pueden revelarse a una parte no autorizada.

Si el grupo es utilizado por varios subprocesos, es posible que necesite medios para evitar que subprocesos paralelos intenten reutilizar el mismo objeto en paralelo. Esto no es necesario si los objetos agrupados son inmutables o son seguros para subprocesos.

Crítica

Algunas publicaciones no recomiendan el uso de agrupaciones de objetos con ciertos lenguajes, como Java , especialmente para objetos que solo usan memoria y no contienen recursos externos (como conexiones a bases de datos). Los opositores suelen decir que la asignación de objetos es relativamente rápida en los lenguajes modernos con recolectores de basura ; Mientras que el operador newsólo necesita diez instrucciones, el par clásico newque deletese encuentra en los diseños de agrupación requiere cientos de ellas, ya que realiza un trabajo más complejo. Además, la mayoría de los recolectores de basura escanean referencias de objetos "vivos" y no la memoria que estos objetos utilizan 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

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 canales y, en el caso de un grupo vacío, establece el tiempo de espera del procesamiento para evitar que los clientes esperen demasiado.

// grupo de paquetes grupo de paquetes importar ( "errores" "registro" "matemáticas/rand" "sincronización" "hora" ) const getResMaxTime = 3 * tiempo . Segundo     var ( ErrPoolNotExist = errores . Nuevo ( "el grupo no existe" ) ErrGetResTimeout = errores . Nuevo ( "obtener tiempo de espera de recurso" ) )     // Tipo de recurso Estructura de recurso { resId int }    //NewResource Simula la creación lenta de inicialización de recursos // (por ejemplo, la conexión TCP, la adquisición de clave simétrica SSL y la autenticación de autenticación consumen 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 ~ 400 ms func ( r * Resource ) Do ( workId int ) { time . Dormir ( tiempo . Duración ( rand . Intn ( 5 )) * 100 * tiempo . Milisegundos ) registro . Printf ( "usando el recurso #%d trabajo terminado %d acabado\n" , r . resId , workId ) }           // Grupo basado en la implementación del canal Go, para evitar el problema del estado de carrera de recursos tipo Pool chan * Recurso   // Nuevo grupo de recursos del tamaño especificado // Los recursos se crean simultáneamente 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 ; yo < tamaño ; i ++ { go func ( resId int ) { p <- NewResource ( resId ) wg . Hecho () }( i ) } wg . Esperar () devolver p }                       //GetResource según el canal, se evita el estado de carrera de recursos y se establece el tiempo de espera de adquisición de recursos para el grupo vacío func ( p Pool ) GetResource () ( r * Resource , err error ) { select { case r := <- p : return r , caso nulo <- tiempo . Después ( getResMaxTime ): devuelve 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 principal importar ( "github.com/tkstorm/go-design/creational/object-pool/pool" "log" "sync" ) func main () { // Inicializa un grupo de cinco recursos, // que se puede ajustar a 1 o 10 para ver la diferencia tamaño := 5 p := pool . Nuevo ( tamaño )      // Invoca un recurso para realizar el trabajo de identificación doWork := func ( workId int , wg * sync . WaitGroup ) { defer wg . Listo () // Obtenga el recurso del grupo de recursos res , err : = p . GetResource () si err ! = nil { log . Println ( err ) return } // Recursos para devolver diferir p . GiveBackResource ( res ) // Usa recursos para manejar la resolución del trabajo . Hacer ( Id. De trabajo ) }               // Simule 100 procesos simultáneos para obtener recursos del grupo de activos num := 100 wg := new ( sync . WaitGroup ) wg . Agregue ( núm ) para i := 0 ; yo < número ; i ++ { ir a hacerTrabajo ( i , wg ) } wg . Esperar () }              

C#

En la biblioteca de clases base de .NET hay algunos objetos que implementan este patrón. System.Threading.ThreadPoolestá configurado para tener un número predefinido de subprocesos para asignar. Cuando se devuelven los subprocesos, están disponibles para otro cálculo. Por tanto, se pueden utilizar subprocesos sin pagar el coste de creación y eliminación de subprocesos.

A continuación se muestra el código básico del patrón de diseño del grupo de objetos implementado con C#. Para mayor brevedad, las propiedades de las clases se declaran utilizando la sintaxis de propiedades 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 utilizar clases de instancia para grupos de objetos.

espacio de nombres DesignPattern.Objectpool ; // La clase PooledObject es el tipo cuya creación de instancias es costosa o lenta, // o que tiene disponibilidad limitada, por lo que debe mantenerse en el grupo de objetos. clase pública PooledObject { Fecha y hora privada _createdAt = Fecha y hora . Ahora ;        Fecha y hora pública Creado en => _creado en ;     cadena pública TempData { obtener ; colocar ; } }      // 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 garantiza que los objetos liberados // vuelvan a un estado adecuado, listos para su reutilización. piscina de clase pública estática { Lista estática privada < PooledObject > _disponible = nueva Lista < PooledObject > (); Lista estática privada < PooledObject > _inUse = nueva Lista < PooledObject > ();                  public static PooledObject GetObject () { lock ( _available ) { if ( _available . Count != 0 ) { PooledObject po = _available [ 0 ]; _en uso . Agregar ( po ); _disponible . Eliminar en ( 0 ); volver po ; } else { PooledObject po = nuevo PooledObject (); _en uso . Agregar ( po ); volver po ; } } }                                   ReleaseObject vacío estático público ( PooledObject po ) { Limpieza ( po );       bloquear ( _disponible ) { _disponible . Agregar ( po ); _en uso . Quitar ( po ); } }       Limpieza de vacío estático privado ( PooledObject po ) { po . Datos temporales = nulo ; } }         

En el código anterior, PooledObject tiene propiedades para el momento en que se creó y otra, que el cliente puede modificar, que se restablece cuando PooledObject se devuelve al grupo. Se muestra el proceso de limpieza, al liberar un objeto, asegurando que esté en un estado válido antes de que pueda solicitarse nuevamente del grupo.

Java

Java admite la agrupación de subprocesos a través java.util.concurrent.ExecutorServicede otras clases relacionadas. El servicio ejecutor tiene una cierta cantidad 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 un 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 hilo solicitante.

clase pública PooledObject { cadena pública temp1 ; cadena pública temp2 ; cadena pública temp3 ; cadena pública getTemp1 () { return temp1 ; } public void setTemp1 ( String temp1 ) { this . temp1 = temp1 ; } cadena pública getTemp2 () { return temp2 ; } public void setTemp2 ( String temp2 ) { this . temp2 = temp2 ; } cadena pública getTemp3 () { return temp3 ; } public void setTemp3 ( String temp3 ) { this . temp3 = temp3 ; } }                                       
clase pública PooledObjectPool { tiempo de espera largo estático privado = 6000 ; // 6 segundos HashMap estático público < PooledObject , Long > disponible = nuevo HashMap < PooledObject , Long > (); HashMap estático público < PooledObject , Long > inUse = new HashMap < PooledObject , Long > (); público sincronizado estático PooledObject getObject () { largo ahora = Sistema . horaactualMillis (); if ( ! disponible . isEmpty ()) { for ( Map . Entry < PooledObject , Long > entrada : disponible . EntrySet ()) { if ( now - Entry . getValue () > expTime ) { // el objeto ha caducado popElement ( disponible ); } else { PooledObject po = popElement ( disponible , entrada . getKey ()); empujar ( en uso , po , ahora ); volver po ; } } }                                                         // o no hay ningún PooledObject disponible o cada uno ha caducado, así que devuelve uno nuevo return createPooledObject ( now ); } PooledObject estático sincronizado privado createPooledObject ( hace mucho tiempo ) { PooledObject po = new PooledObject (); empujar ( en uso , po , ahora ); volver po ; }               push vacío estático sincronizado privado ( HashMap < PooledObject , Long > map , PooledObject po , long now ) { mapa . poner ( po , ahora ); }           objeto de liberación vacío estático público ( PooledObject po ) { limpieza ( po ); disponible . put ( po , Sistema . currentTimeMillis ()); en uso . eliminar ( po ); } popElement estático privado PooledObject ( HashMap < PooledObject , Long > mapa ) { Mapa . Entrada < PooledObject , Long > entrada = mapa . conjunto de entrada (). iterador (). próximo (); Clave PooledObject = entrada . obtener la clave (); //Valor largo=entry.getValue(); mapa . eliminar ( entrada . getKey ()); tecla de retorno ; } popElement estático privado PooledObject ( HashMap < PooledObject , Long > mapa , clave PooledObject ) { mapa . eliminar ( tecla ); 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

  1. ^ ab Goetz, Brian (27 de septiembre de 2005). "Teoría y práctica de Java: leyendas del rendimiento urbano, revisadas". IBM . IBM DeveloperWorks. Archivado desde el original el 14 de febrero de 2012 . Consultado el 15 de marzo de 2021 .

Referencias

enlaces externos