En programación de computadoras , el patrón de diseño de software de peso mosca se refiere a un objeto que minimiza el uso de memoria al compartir algunos de sus datos con otros objetos similares. El patrón de peso mosca es uno de los veintitrés patrones de diseño de GoF más conocidos . [1] Estos patrones promueven un diseño de software flexible orientado a objetos, que es más fácil de implementar, cambiar, probar y reutilizar.
En otros contextos, la idea de compartir estructuras de datos se llama consing hash .
Descripción general
El patrón de peso mosca es útil cuando se trata de una gran cantidad de objetos con elementos simples repetidos que usarían una gran cantidad de memoria si se almacenaran individualmente. Es común mantener datos compartidos en estructuras de datos externas y pasarlos a los objetos temporalmente cuando se utilizan.
Un ejemplo clásico de peso mosca son las estructuras de datos utilizadas para la representación gráfica de caracteres en un procesador de texto . Cada carácter de un documento puede tener un objeto de glifo que contenga su contorno de fuente, métricas de fuente y otros datos de formato. Sin embargo, esto usaría cientos o miles de bytes de memoria para cada carácter. En cambio, para cada carácter puede haber una referencia a un objeto de glifo compartido por cada instancia del mismo carácter en el documento. De esta manera, solo la posición de cada carácter en el documento debería almacenarse internamente.
Problemas que resuelve el patrón de diseño de peso mosca: [2]
- Se debe admitir un gran número de objetos de manera eficiente.
- Debe evitarse la creación de una gran cantidad de objetos.
Al representar documentos de texto grandes, por ejemplo, la creación de un objeto para cada carácter en el documento daría como resultado una gran cantidad de objetos que no podrían procesarse de manera eficiente.
Soluciones que el patrón de diseño Flyweight puede describir:
Definir Flyweight
objetos que
- almacenar el estado intrínseco (invariante) que se puede compartir y
- proporcionan una interfaz a través de la cual se puede pasar el estado extrínseco (variante).
Esto permite a los clientes reutilizar Flyweight
objetos (en lugar de crear un objeto nuevo cada vez) y pasar al estado extrínseco cuando invocan una Flyweight
operación.
Esto reduce en gran medida el número de objetos creados físicamente.
El estado intrínseco es invariante (independiente del contexto) y, por lo tanto, se puede compartir (por ejemplo, el código del carácter 'A' en un conjunto de caracteres determinado).
El estado extrínseco es una variante (depende del contexto) y, por lo tanto, no se puede compartir y debe pasarse (por ejemplo, la posición del carácter 'A' en un documento de texto).
Consulte también el diagrama de secuencia y clase UML a continuación.
Historia
El término "patrón de peso mosca" fue acuñado por primera vez, y la idea ampliamente explorada, por Paul Calder y Mark Linton en 1990 [3] para manejar eficientemente la información de glifos en un editor de documentos WYSIWYG . [4] Sin embargo, ya se utilizaron técnicas similares en otros sistemas, como un marco de aplicación de Weinand et al. (1988). [5] Las celdas reutilizables de iOS en tableViews y collectionViews también usan el patrón flyweight.
Estructura
Diagrama de secuencia y clase UML
En el diagrama de clases de UML anterior , la Client
clase se refiere (1) a la FlyweightFactory
clase para crear / compartir Flyweight
objetos y (2) a la Flyweight
interfaz para realizar una operación pasando en estado extrínseco (variante) ( flyweight.operation(extrinsicState)
). La Flyweight1
clase implementa la Flyweight
interfaz y almacena el estado intrínseco (invariante) que se puede compartir.
El diagrama de secuencia muestra las interacciones en tiempo de ejecución: el Client
objeto llama getFlyweight(key)
al FlyweightFactory
que crea y devuelve un Flyweight1
objeto. Después de llamar operation(extrinsicState)
al Flyweight1
objeto devuelto , Client
vuelve a llamar getFlyweight(key)
al FlyweightFactory
, que ahora comparte y devuelve el Flyweight1
objeto ya existente .
Detalles de implementación y patrones híbridos
Estado extrínseco mutable o inmutable
Una decisión de diseño inicial al implementar un peso mosca es si se deben usar objetos extrínsecos (estado variante) mutables o inmutables. Los objetos inmutables se pueden compartir fácilmente. Pero esto requiere la creación de nuevos objetos extrínsecos cada vez que ocurre un cambio de estado. Si el estado de los objetos solo cambia ocasionalmente, o cambia de manera recurrente, solo será necesario crear una pequeña cantidad de estos objetos extrínsecos inmutables. Si las propiedades de los objetos cambian con frecuencia y de manera única, se prefiere el uso de objetos extrínsecos mutables. Con una implementación mutable, los objetos pueden compartir el estado, pero no se modificarán directamente mientras están en uso. La mutabilidad es para una mejor reutilización de objetos al permitir el almacenamiento en caché y la reinicialización de objetos antiguos no utilizados. Se debe proporcionar una API en tales implementaciones que garantice la devolución de un objeto reinicializado no utilizado. Cuando el estado es muy variable, como cuando la mayoría de los objetos son únicos, se deben abandonar los intentos de compartir. En estos casos, simplemente encapsule el estado altamente variable en un decorador ligero, proporcione un caché para él, inicialice de forma masiva el caché y termine con él.
Partición del objeto de peso mosca
Una forma de implementar el componente extrínseco (variante) de un objeto de peso mosca es con un decorador y usarlo para envolver el objeto intrínseco. Esto se prefiere para el estado que es más variante, ya que los decoradores se pueden aplicar o eliminar fácilmente de los objetos seleccionados y un solo objeto se puede envolver con múltiples decoradores arbitrarios en tiempo de ejecución. El intercambio de decoradores se logra con solo una referencia común. Para el objeto intrínseco, una implementación común es como una clase que consta de nada más que propiedades estáticas. Subclasificando esta clase y agregando más propiedades estáticas, uno puede crear una variedad de objetos invariantes. Este enfoque puede explicar el estado que es mayoritariamente, pero no del todo, invariante.
Considere una aplicación de edición de texto (ejemplo típico). Un usuario puede resaltar un párrafo de texto, poner en negrita algunas palabras dentro de ese texto ... tal vez poner en cursiva solo una palabra y aumentar su tamaño de fuente. Cada modificación es otra capa de decoración que envuelve el objeto intrínseco compartido. Intentar implementar algo como esto con herencia múltiple sería un completo desastre. Tenga en cuenta que estos decoradores tienen referencias entre sí, lo que permite compartir. Los mensajes se propagan a través de las capas decoradas de forma transparente.
Ahora considere otro ejemplo, nuevamente en una aplicación de procesamiento de texto. Puede haber 10,000 letras, pero solo 26 caracteres alfabéticos únicos en un documento. Hay un estado que es mayormente invariante: la letra en particular que se muestra. No es del todo invariante, pero podría tratarse fácilmente como tal. En tales casos de estado mayoritariamente invariante, la subclasificación del objeto intrínseco y la adición de propiedades estáticas a las subclases es la técnica de optimización más eficaz. La mayoría de los compiladores reemplazan objetos comunes como estos (caracteres comunes, números de dígitos bajos) utilizando objetos estáticos optimizados. El enfoque de subclases tiene la ventaja de ser compilado y optimizado, y la interfaz con el objeto intrínseco se mantiene simple mediante polimorfismo.
Estos ejemplos en competencia destacan la diferencia entre el diseño jerárquico, de arriba hacia abajo y el diseño compositivo. En la práctica, los objetos a menudo no se dividen claramente en partes variables e invariantes, hay un área gris donde uno debe decidir la mejor manera de dividir el objeto. Como se mencionó anteriormente, a veces el estado del objeto es tan variable que no es práctico compartir, y ese estado debe colocarse en un decorador y almacenarse en caché. El uso combinado e inteligente de la subclasificación estática y la decoración es lo que se necesita para implementar una partición del objeto verdaderamente eficiente en la memoria.
Descripción general del sistema de recuperación de objetos
Aunque la fábrica para crear o reutilizar estos objetos de peso mosca tiene una interfaz simple, a menudo se trata de una fachada donde el sistema subyacente es bastante complicado. Dado que el objetivo del patrón de peso mosca es preservar la memoria, uno quiere hacer todo lo posible para almacenar en caché, compartir y reutilizar objetos extrínsecos siempre que sea posible. El intercambio de objetos extrínsecos se puede hacer fácilmente haciéndolos inmutables, como se mencionó anteriormente. Una vez más, existen implementaciones mutables en las que es posible compartir, el estado de los objetos compartidos no se puede cambiar mientras se utilizan. En cambio, dicho cambio debe emitir una solicitud para un tipo diferente de objeto compartido, la reinicialización de un objeto antiguo (la ventaja de la mutabilidad) o la creación de un nuevo objeto.
El tipo específico de caché utilizado generalmente depende de la estructura del objeto, si es mutable o inmutable, y si se puede compartir o no. Hay una variedad de algoritmos de almacenamiento en caché y recuperación para elegir. La eficiencia del algoritmo depende de la estructura de datos de la memoria caché, el número de objetos, las características de los objetos y los patrones de uso de memoria de la aplicación.
Para proporcionar acceso global para la creación de estos objetos de peso mosca, es mejor implementar la interfaz de fábrica como Singleton. Esto debe realizarse en aplicaciones de memoria compartida de un solo subproceso y de varios subprocesos. En los sistemas distribuidos, una opción alternativa es crear instancias únicas de todo el sistema flyweight en cada nodo, incluidas las cachés, aunque la utilización de la caché es un problema potencial con este enfoque, y se debe tener cuidado al inicializar objetos de forma masiva. Se podrían desarrollar algoritmos de intercambio más complejos, donde ciertos tipos de objetos se delegaran en ciertos nodos. Incluso se podría crear un administrador que coordine todo esto. Sin embargo, esa es una discusión más allá del alcance de este artículo.
En términos generales, el algoritmo de recuperación comienza con una solicitud de un nuevo objeto a través de la interfaz de fábrica. La solicitud se reenvía a una caché apropiada según el tipo de objeto, cómo se particiona el objeto, cómo se diseñan las cachés, etc. Si la solicitud es cumplida por un objeto en la caché, puede reinicializarse (si es mutable) y devolverse. De lo contrario, será necesario crear una instancia de un nuevo objeto. Si el objeto está dividido en varios subcomponentes extrínsecos, es posible que sea necesario unirlos correctamente antes de devolver el objeto.
Algunos enfoques para almacenar en caché objetos de peso mosca
El almacenamiento en caché de objetos con un estado muy variable (variable hasta cierto punto que no hay razón para intentar compartirlos) se puede lograr fácilmente utilizando una estructura FIFO. Aquí, la estructura FIFO simplemente mantendrá los objetos no utilizados en la caché, sin necesidad de buscar en la caché o lidiar con las tasas de errores. Aun así, se podría preferir una caché sin mantenimiento. Hay menos sobrecarga inicial asociada con una caché no mantenida: los objetos para las cachés se inicializan de forma masiva en el momento de la compilación o el inicio. Por lo tanto, la tasa de error inicial para una caché no mantenida debería ser cero durante bastante tiempo, hasta que no haya más objetos y no será necesario realizar operaciones emergentes inicialmente. Por otro lado, una vez que esto sucede, el algoritmo de recuperación de objetos puede tener más sobrecarga asociada que las operaciones push / pop de una caché mantenida. Qué tipo de caché preferir aquí realmente depende de los patrones de uso de memoria de la aplicación, la estructura de datos real de la caché, la cantidad de objetos que tiene y, sobre todo, los resultados de probar la implementación. Si la aplicación utilizará un número muy constante de estos objetos, se prefiere una caché sin mantenimiento que se inicialice de forma masiva con esa cantidad exacta de objetos. Si el número total de objetos puede variar mucho, entonces puede ser preferible la caché mantenida. En última instancia, solo las pruebas dirán qué implementación elegir.
Cuando se recuperan objetos extrínsecos con estado inmutable, simplemente se debe buscar en la caché un objeto con el estado que se desea, si existe. Esto se hace mejor con una estructura hash, hash del estado del objeto. Si no se encuentra tal objeto, se debe inicializar un solo objeto con ese estado. Esta es otra desventaja del uso de objetos con estado inmutable: la incapacidad de realizar inicializaciones masivas.
Al recuperar objetos extrínsecos con estado mutable, se debe buscar en la caché un objeto con el estado deseado y buscar en la caché un objeto no utilizado para reinicializar si no se encuentra ningún objeto utilizado. Este enfoque requiere más ciclos de CPU que otros enfoques, pero es más eficiente en memoria, ya que minimiza el total de objetos que deben inicializarse al mismo tiempo que los comparte. Generalmente se prefiere. Un error común en la optimización de algoritmos es optimizar las instrucciones de la CPU a expensas de usar más memoria. Cualquier recuperación de datos del disco toma varios órdenes de magnitud más que una instrucción de CPU (SSD toma 8 órdenes de magnitud más que una instrucción de CPU, por ejemplo). Por lo tanto, la máxima prioridad para optimizar el código es, en general, minimizar la huella de memoria. Por supuesto que hay excepciones, pero son menos comunes de lo que cabría esperar.
Para cachés como estos, un algoritmo de búsqueda está optimizado para minimizar los errores. Existe una amplia variedad de algoritmos de búsqueda de caché para elegir. Es útil encapsular estos algoritmos de selección en un patrón de estrategia . Esto permite intercambiar fácilmente diferentes algoritmos, lo que hace que las pruebas y la optimización de la memoria caché sean mucho más prácticas. En el peor de los casos en que no haya ningún objeto no utilizado disponible, después de atravesar el caché, se debe crear una instancia de un nuevo objeto y agregarlo al caché. Es probable que sea preferible una asignación masiva de objetos cuando esto ocurre, agregando todos ellos a la caché, preferiblemente con mapeo de memoria.
Se pueden usar cachés separados para cada subclase única de objeto extrínseco. Se pueden optimizar múltiples cachés por separado, asociando un algoritmo de búsqueda único con cada caché; o igualmente una estructura de datos única. Haga esto según las pruebas: diferentes cachés tienen diferentes patrones de uso de memoria. Los patrones de uso de la memoria también cambian durante la ejecución de las aplicaciones. El algoritmo de selección se puede ajustar para una caché en particular durante la ejecución. Por ejemplo, cuando la aplicación se ejecuta por primera vez, debe haber una tasa de fallas cero, esto cambia a medida que se utilizan esos objetos asignados en forma masiva inicial. El algoritmo de selección debe cambiarse en ese momento. Si el algoritmo de selección está encapsulado en un patrón de estrategia, esto es fácil de hacer.
Restaurar objetos
El algoritmo de restashing suele ser bastante simple: anule la función de desasignación de objetos, busque el caché apropiado y disminuya las variables de recuento de referencia. Si usa una caché de tipo FIFO, el objeto se devuelve a esta dentro de dealloc. Si usa una caché no compartida que no sea una estructura FIFO, asumiendo que se usa una bandera para rastrear el uso, simplemente actualice la bandera en dealloc. Si se comparte la caché, nuevamente los objetos deberían tener una propiedad que actúe como un recuento de referencia (probablemente un int corto), y esto debería reducirse en dealloc. De esta manera, uno sabe cuándo el objeto deja de usarse y está abierto a mutaciones. Si el idioma de origen utiliza el recuento de referencias automático, el puntero se puede establecer en el objeto en cero y el recuento de referencias se reducirá automáticamente. El algoritmo de búsqueda de caché podrá entonces detectar que el objeto no se utiliza comprobando su recuento de referencias.
Encapsulando el sistema de almacenamiento en caché
Este sistema de almacenamiento en caché de objetos (la fábrica que inicia una solicitud, el enrutamiento a cachés, los cachés, los algoritmos de recuperación y las API de objetos) se puede encapsular en un patrón de cadena de responsabilidad. Al hacerlo, se estructura la implementación de una manera que promueve un acoplamiento flexible entre estos diversos componentes. Aunque la mayoría de la lógica de propagación de solicitudes ya debería encapsularse dentro de los objetos de estrategia para los algoritmos de recorrido de caché, el acoplamiento flexible entre componentes puede ser útil al reconfigurar una implementación (como si se necesitara modificarla para que funcione en un entorno diferente, como un entorno multiproceso ; si se desea probar diferentes tipos de cachés; si se desea reutilizar el peso mosca para un conjunto diferente de objetos, etc.).
Realización de operaciones sobre cobros de peso mosca
Las operaciones comunes en estas diversas colecciones incluyen reinicializar objetos extrínsecos reutilizados, operaciones iterativas o únicas con lógica condicional y operaciones realizadas en ciertos subconjuntos de objetos. El patrón de diseño Visitor se combina de manera muy natural con el patrón de peso mosca para implementar tal comportamiento. El patrón encapsula un comportamiento que debe realizar una colección diversa de objetos, como los diversos objetos extrínsecos involucrados en el patrón de peso mosca. El uso de visitantes en el código lo hace mejor encapsulado, más legible y modificable, y más eficiente en memoria que la extensión de clases.
Inmutabilidad e igualdad
Para permitir el uso compartido seguro, entre clientes e hilos, los objetos Flyweight se pueden hacer inmutables . Los objetos de peso mosca son, por definición, objetos de valor. La identidad de la instancia de objeto no tiene importancia; por lo tanto, dos instancias de Flyweight del mismo valor se consideran iguales.
Ejemplo en C # (tenga en cuenta las anulaciones de Equals y GetHashCode, así como las sobrecargas de operadores == y! =):
public class CoffeeFlavour { public CoffeeFlavour ( sabor de cadena ) => this . Sabor = sabor ; Sabor de cadena pública { get ; } public override boolean Equals ( Object ? obj ) => Equals ( this , obj ); public static bool Equals ( CoffeeFlavour ? left , CoffeeFlavour ? right ) => String . Igual ( ¿a la izquierda ? ¿ Sabor , a la derecha ? ¿ Sabor ); public override int GetHashCode () => this . Sabor . GetHashCode (); operador bool estático público == ( CoffeeFlavour a , CoffeeFlavour b ) => Equals ( a , b ); operador public static bool ! = ( CoffeeFlavour a , CoffeeFlavour b ) => ! Igual a ( a , b ); }
Concurrencia
Se debe tener en cuenta una consideración especial en escenarios donde los objetos Flyweight se crean en múltiples subprocesos. Si la lista de valores es finita y se conoce de antemano, los Flyweights se pueden instanciar antes de tiempo y recuperar de un contenedor en varios subprocesos sin contención. Si se crean instancias de Flyweights en varios subprocesos, hay dos opciones:
- Haga que la instanciación de Flyweight sea de un solo subproceso, introduciendo así la contención y asegurando una instancia por valor.
- Permita que los subprocesos concurrentes creen múltiples instancias de Flyweight, eliminando así la contención y permitiendo múltiples instancias por valor. Esta opción solo es viable si se cumple el criterio de igualdad.
Ejemplo en C #
utilizando System.Collections.Concurrent ; utilizando System.Collections.Generic ; usando System.Threading ; interfaz pública ICoffeeFlavourFactory { CoffeeFlavour GetFlavour ( sabor de cadena ); }public class ReducedMemoryFootprint : ICoffeeFlavourFactory { objeto privado de solo lectura _cacheLock = nuevo objeto (); privado de solo lectura IDictionary < string , CoffeeFlavour > _cache = new Dictionary < string , CoffeeFlavour > (); public CoffeeFlavour GetFlavour ( cadena de sabor ) { if ( _caché . TryGetValue ( sabor , fuera CoffeeFlavour cachedCoffeeFlavour )) return cachedCoffeeFlavour ; var coffeeFlavour = new CoffeeFlavour ( sabor ); ThreadPool . QueueUserWorkItem ( AddFlavourToCache , coffeeFlavour ); return coffeeFlavour ; } private void AddFlavourToCache ( estado del objeto ) { var coffeeFlavour = ( CoffeeFlavour ) estado ; if (! _caché . ContainsKey ( sabor café . Sabor )) { bloqueo ( _cachéLock ) { _caché [ sabor café . Sabor ] = sabor a café ; } } } } clase pública MínimoMemoriaFootprint : ICoffeeFlavourFactory { privado de solo lectura ConcurrentDictionary < string , CoffeeFlavour > _caché = nuevo ConcurrentDictionary < string , CoffeeFlavour > (); public CoffeeFlavour GetFlavour ( sabor de cadena ) { return _caché . GetOrAdd ( sabor , flv => nuevo CoffeeFlavour ( flv )); } }
Implementación simple
Flyweight permite a los usuarios compartir datos voluminosos que son comunes a cada objeto. En otras palabras, si uno piensa que los mismos datos se repiten para todos los objetos, pueden usar este patrón para apuntar a un solo objeto y, por lo tanto, pueden ahorrar espacio fácilmente. Aquí, FlyweightPointer crea una empresa miembro estática, que se utiliza para cada objeto de MyObject.
// Define el objeto Flyweight que se repite. clase pública Flyweight { cadena pública CompanyName { get ; establecer ; } cadena pública CompanyLocation { get ; establecer ; } cadena pública CompanyWebsite { get ; establecer ; } // Byte público de datos voluminosos [] CompanyLogo { get ; establecer ; } } public static class FlyweightPointer { public static readonly Flyweight Company = new Flyweight { CompanyName = "Abc" , CompanyLocation = "XYZ" , CompanyWebsite = "www.example.com" // Cargue CompanyLogo aquí }; }public class MyObject { public string Name { get ; establecer ; } cadena pública Compañía => FlyweightPointer . Empresa . CompanyName ; }
Ejemplo en Java
import java.util.ArrayList ; import java.util.WeakHashMap ;class CoffeeFlavour { nombre de cadena final privado ; final estático privado WeakHashMap < String , CoffeeFlavour > CACHE = new WeakHashMap <> (); // solo intern () puede llamar a este constructor private CoffeeFlavour ( String name ) { this . nombre = nombre ; } @Override public String toString () { nombre de retorno ; } public static CoffeeFlavour becario ( String name ) { sincronizado ( CACHE ) { return CACHE . computeIfAbsent ( nombre , CoffeeFlavour :: nuevo ); } } public static int flavoursInCache () { sincronizado ( CACHE ) { devolver CACHE . tamaño (); } } }@FunctionalInterface interface Order { void serve (); Orden estático de ( String flavourName , int tableNumber ) { CoffeeFlavour sabor = CoffeeFlavour . pasante ( flavourName ); return () -> Sistema . fuera . println ( "Sirviendo" + sabor + "a la mesa" + tableNumber ); } }class CoffeeShop { ArrayList < Order > final privado pedidos = new ArrayList <> (); pública vacío takeOrder ( Cadena sabor , int tablenumber ) { pedidos . agregar ( Orden . de ( sabor , número de tabla )); } servicio público vacío () { pedidos . forEach ( Orden :: servir ); } } public class FlyweightExample { public static void main ( String [] args ) { CoffeeShop shop = new CoffeeShop (); tienda . takeOrder ( "Capuchino" , 2 ); tienda . takeOrder ( "Frappe" , 1 ); tienda . takeOrder ( "Espresso" , 1 ); tienda . takeOrder ( "Frappe" , 897 ); tienda . takeOrder ( "Capuchino" , 97 ); tienda . takeOrder ( "Frappe" , 3 ); tienda . takeOrder ( "Espresso" , 3 ); tienda . takeOrder ( "Capuchino" , 3 ); tienda . takeOrder ( "Espresso" , 96 ); tienda . takeOrder ( "Frappe" , 552 ); tienda . takeOrder ( "Capuchino" , 121 ); tienda . takeOrder ( "Espresso" , 121 ); tienda . servicio (); Sistema . fuera . println ( "objetos CoffeeFlavor en cache:" + CoffeeFlavour . flavoursInCache ()); } }
La ejecución de este código dará lo siguiente:
Sirviendo capuchino a la mesa 2Sirviendo Frappe a la mesa 1Sirviendo Espresso en la mesa 1Sirviendo Frappe a la mesa 897Sirviendo capuchino a la mesa 97Sirviendo Frappe a la mesa 3Sirviendo Espresso en la mesa 3Sirviendo capuchino a la mesa 3Sirviendo Espresso en la mesa 96Sirviendo Frappe a la mesa 552Sirviendo capuchino a la mesa 121Sirviendo Espresso en la mesa 121Objetos CoffeeFlavor en caché: 3
Ejemplo en Python
Los atributos se pueden definir a nivel de clase en lugar de solo para instancias en Python porque las clases son objetos de primera clase en el lenguaje, lo que significa que no hay restricciones en su uso ya que son iguales a cualquier otro objeto. Las instancias de clase de nuevo estilo almacenan datos de instancia en un diccionario de atributos especial instance.__dict__
. De forma predeterminada, los atributos a los que se accede primero se buscan en este __dict__
y luego vuelven a los atributos de clase de la instancia a continuación. [7] De esta manera, una clase puede ser efectivamente una especie de contenedor Flyweight para sus instancias.
Aunque las clases de Python son mutables de forma predeterminada, la inmutabilidad se puede emular anulando el __setattr__
método de la clase para que no permita cambios en ningún atributo de Flyweight.
# Las instancias de CheeseBrand serán la clase Flyweights CheeseBrand : def __init__ ( self , brand : str , cost : float ) -> None : self . marca = marca propia . costo = costo propio . _immutable = True # Deshabilita atribuciones futuras def __setattr__ ( self , name , value ): if getattr ( self , "_immutable" , False ): # Permitir la atribución inicial raise RuntimeError ( "Este objeto es inmutable" ) else : super () . __setattr__ ( nombre , valor )class CheeseShop : menu = {} # Contenedor compartido para acceder a Flyweights def __init__ ( self ) -> Ninguno : self . orders = {} # contenedor por instancia con atributos privados def stock_cheese ( self , brand : str , cost : float ) -> None : cheese = CheeseBrand ( marca , costo ) self . menu [ marca ] = queso # Peso mosca compartido def sell_cheese ( self , brand : str , units : int ) -> None : self . pedidos . setdefault ( marca , 0 ) self . pedidos [ marca ] + = unidades # atributo de instancia def total_units_sold ( self ): devuelve la suma ( self . orders . values ()) def total_income ( self ): ingresos = 0 para la marca , unidades en self . pedidos . items (): ingresos + = self . menú [ marca ] . costo * unidades devuelven ingresosshop1 = CheeseShop () shop2 = CheeseShop ()tienda1 . stock_cheese ( "blanco" , 1,25 ) shop1 . stock_cheese ( "blue" , 3.75 ) # Ahora cada CheeseShop tiene 'blanco' y 'azul' en el inventario # La MISMA CheeseBrand 'blanco' y 'azul'tienda1 . sell_cheese ( "azul" , 3 ) # Ambos pueden vender shop2 . sell_cheese ( "blue" , 8 ) # Pero las unidades vendidas se almacenan por instanciaafirmar shop1 . total_units_sold () == 3 afirmar shop1 . ingresos_total () == 3.75 * 3afirmar shop2 . total_units_sold () == 8 afirmar shop2 . ingresos_total () == 3.75 * 8
Ejemplo en Ruby
# Flyweight Object class Lamp attr_reader : color #attr_reader hace que el atributo de color esté disponible fuera # de la clase llamando a .color en una instancia de Lamp def initialize ( color ) @color = color end endclass TreeBranch def initialize ( branch_number ) @branch_number = branch_number end def hang ( lámpara ) pone "Hang # { lamp . color } lamp on branch # { @branch_number } " end end# Flyweight Factory class LampFactory def initialize @lamps = {} end def find_lamp ( color ) si @lamps . has_key? ( color ) # si la lámpara ya existe, haga referencia a ella en lugar de crear una nueva lámpara = @lamps [ color ] else lamp = Lamp . nuevo ( de color ) @lamps [ de color ] = lámpara de extremo de la lámpara final def total_number_of_lamps_made @lamps . tamaño final finalclass ChristmasTree def initialize @lamp_factory = LampFactory . nuevo @lamps_hung = 0 dress_up_the_tree end def hang_lamp ( color , branch_number ) TreeBranch . nuevo (número de sucursal ) . colgar ( @lamp_factory . find_lamp ( color )) @lamps_hung + = 1 extremo def dress_up_the_tree hang_lamp ( 'rojo' , 1 ) hang_lamp ( 'azul' , 1 ) hang_lamp ( 'amarillo' , 1 ) hang_lamp ( 'rojo' , 2 ) hang_lamp ( 'azul' , 2 ) hang_lamp ( 'amarillo' , 2 ) hang_lamp ( 'rojo' , 3 ) hang_lamp ( 'azul' , 3 ) hang_lamp ( 'amarillo' , 3 ) hang_lamp ( 'rojo' , 4 ) hang_lamp ( 'azul' , 4 ) hang_lamp ( 'amarillo' , 4 ) hang_lamp ( 'rojo' , 5 ) hang_lamp ( 'azul' , 5 ) hang_lamp ( 'amarillo' , 5 ) hang_lamp ( 'rojo' , 6 ) hang_lamp ( 'azul' , 6 ) hang_lamp ( 'amarillo' , 6 ) hang_lamp ( 'rojo ' , 7 ) hang_lamp ( 'azul' , 7 ) hang_lamp ( '' amarillo , 7 ) pone "Hecho # { @lamp_factory . total_number_of_lamps_made } total de lámparas" pone "Hung # { @lamps_hung } Lámparas total de" extremo final
Ejemplo en Scala
/ * ejecutar como un script usando `scala flyweight.scala` salida esperada: Sirviendo CoffeeFlavour (Espresso) a la mesa 121 Sirviendo CoffeeFlavour (Cappuccino) a la mesa 121 Sirviendo CoffeeFlavour (Frappe) a la mesa 552 Sirviendo CoffeeFlavour (Espresso) a la mesa 96 Sirviendo CoffeeFlavour (Cappuccino) a la mesa 3 Sirviendo CoffeeFlavour (Espresso) a la mesa 3 Sirviendo CoffeeFlavour (Frappe) a la mesa 3 Sirviendo CoffeeFlavour (Cappuccino) a la mesa 97 Sirviendo CoffeeFlavour (Frappe) a la mesa 897 Sirviendo CoffeeFlavour (Espresso) a la mesa 1 Sirviendo CoffeeFlavour (Frappe) ) a la mesa 1 Sirviendo CoffeeFlavour (Cappuccino) a la mesa 2 Total de objetos CoffeeFlavour hechos: 3 * // * el constructor `privado` asegura que solo se pueden obtener valores internos * del tipo` CoffeeFlavour`. * / class CoffeeFlavour private ( val name : String ) { override def toString = s "CoffeeFlavour ( $ name )" }object CoffeeFlavour { import scala.collection.mutable import scala.ref.WeakReference private val cache = mutable . Mapa . vacío [ String , ref.WeakReference [ CoffeeFlavour ]] def aplicar ( nombre : Cadena ) : CoffeeFlavour = sincronizado { caché . obtener ( nombre ) partido { caso Algunos ( WeakReference ( sabor )) => sabor caso _ => val newFlavour = nueva CoffeeFlavour ( nombre ) de caché . put ( nombre , WeakReference ( newFlavour )) newFlavour } } def totalCoffeeFlavoursMade = cache . tamaño }caso de la clase de pedido ( tablenumber : Int , sabor : CoffeeFlavour ) { def serve : Unit = println ( s "Sirviendo $ sabor a la mesa $ tableNumber " ) }objeto CoffeeShop { var orders = List . vacío [ pedido ] def takeOrder ( flavourName : String , table : Int ) { val flavour = CoffeeFlavour ( flavourName ) val order = Order ( table , flavourName ) orders = order :: orders } def servicio : Unidad = pedidos . foreach ( _ . servir ) def report = s "Total de objetos CoffeeFlavour hechos: $ { CoffeeFlavour . totalCoffeeFlavoursMade } " }CoffeeShop . takeOrder ( "Capuchino" , 2 ) CoffeeShop . takeOrder ( "Frappe" , 1 ) Cafetería . takeOrder ( "Espresso" , 1 ) CoffeeShop . takeOrder ( "Frappe" , 897 ) CoffeeShop . takeOrder ( "Capuchino" , 97 ) CoffeeShop . takeOrder ( "Frappe" , 3 ) Cafetería . takeOrder ( "Espresso" , 3 ) CoffeeShop . takeOrder ( "Capuchino" , 3 ) CoffeeShop . takeOrder ( "Espresso" , 96 ) CoffeeShop . takeOrder ( "Frappe" , 552 ) CoffeeShop . takeOrder ( "Capuchino" , 121 ) CoffeeShop . takeOrder ( "Espresso" , 121 )CoffeeShop . service println ( informe CoffeeShop . )
Ejemplo en Swift
// Las instancias de CoffeeFlavour serán la estructura Flyweights CoffeeFlavor : CustomStringConvertible { var flavour : String var description : String { flavour } }// El menú actúa como una fábrica y un caché para los objetos de peso mosca de CoffeeFlavour. Struct Menu { private var flavors : [ String : CoffeeFlavor ] = [:] mutando func lookup ( sabor : Cadena ) -> CoffeeFlavor { si dejar existente = sabores [ sabor ] { retorno existente } dejar que newFlavor = CoffeeFlavor ( sabor : el sabor ) sabores [ sabor ] = newFlavor volver newFlavor } }struct CoffeeShop { pedidos var privados : [ Int : CoffeeFlavor ] = [:] menú var privado = Menú () función mutante takeOrder ( sabor : Cadena , tabla : Int ) { pedidos [ tabla ] = menú . lookUp ( sabor : sabor ) } func serve () { para ( mesa , sabor ) en pedidos { print ( "Sirviendo \ ( sabor ) a la mesa \ ( mesa ) " ) } } }var coffeeShop = CoffeeShop () coffeeShop . takeOrder ( sabor : "Cappuccino" , tabla : 1 ) coffeeShop . takeOrder ( sabor : "Frappe" , tabla : 3 ) coffeeShop . takeOrder ( sabor : "Espresso" , tabla : 2 ) coffeeShop . takeOrder ( sabor : "Frappe" , tabla : 15 ) coffeeShop . takeOrder ( sabor : "Cappuccino" , tabla : 10 ) coffeeShop . takeOrder ( sabor : "Frappe" , tabla : 8 ) coffeeShop . takeOrder ( sabor : "Espresso" , tabla : 7 ) coffeeShop . takeOrder ( sabor : "Cappuccino" , tabla : 4 ) coffeeShop . takeOrder ( sabor : "Espresso" , tabla : 9 ) coffeeShop . takeOrder ( sabor : "Frappe" , tabla : 12 ) coffeeShop . takeOrder ( sabor : "Cappuccino" , tabla : 13 ) coffeeShop . takeOrder ( sabor : "Espresso" , tabla : 5 ) coffeeShop . servir ()
Ejemplo en Crystal
# Las instancias de CoffeeFlavor serán la clase Flyweights CoffeeFlavor def initialize ( new_flavor : String ) @name = new_flavor end def to_s ( io ) io << @name end end# El menú actúa como una fábrica y caché para la clase de objetos de peso mosca CoffeeFlavor Menu def initialize @flavors = {} of String => CoffeeFlavor end def lookup ( nombre_sabor : Cadena ) @sabores [ nombre_sabor ] || = Sabor a café . nuevo ( nombre_sabor ) final def total_flavors_made @flavors . tamaño final final# El orden es el contexto del peso mosca CoffeeFlavor. class Order getter privado número_tabla : Int32 , sabor : CoffeeFlavor def initialize ( @table_number , @flavor ) end def servir pone "servir # { sabor } a la tabla # { número_tabla } " extremo finalclase CoffeeShop privadas getter órdenes privada captador menú def initialize @orders = [] de Order @menu = Menú . nuevo final def take_order ( nombre_sabor : String , tabla : Int32 ) sabor = menú . lookup ( nombre_sabor ) order = Order . nuevos ( tabla , sabor ) @orders << Para finales def órdenes de servicio . cada uno hace | orden | orden . servir fin fin def report "Total CoffeeFlavor hecho: # { menu . total_flavors_made } " end end# Tienda de programas = CoffeeShop . nueva tienda . take_order ( "Cappuchino" , 2 ) tienda . take_order ( "Frappe" , 1 ) tienda . take_order ( "Espresso" , 1 ) tienda . take_order ( "Frappe" , 897 ) tienda . take_order ( "Cappuccino" , 97 ) tienda . take_order ( "Frappe" , 3 ) tienda . take_order ( "Espresso" , 3 ) tienda . take_order ( "Cappuccino" , 3 ) tienda . take_order ( "Espresso" , 96 ) tienda . take_order ( "Frappe" , 552 ) tienda . take_order ( "Cappuccino" , 121 ) tienda . take_order ( "Espresso" , 121 )tienda . servicio pone tienda . informe
Producción
Sirviendo Cappuchino a la mesa 2Sirviendo Frappe a la mesa 1Sirviendo Espresso en la mesa 1Sirviendo Frappe a la mesa 897Sirviendo capuchino a la mesa 97Sirviendo Frappe a la mesa 3Sirviendo Espresso en la mesa 3Sirviendo capuchino a la mesa 3Sirviendo Espresso en la mesa 96Sirviendo Frappe a la mesa 552Sirviendo capuchino a la mesa 121Sirviendo Espresso en la mesa 121Sabor Total Café: 4
Ejemplo en C ++
La biblioteca de plantillas estándar de C ++ proporciona varios contenedores que permiten asignar objetos únicos a una clave. El uso de contenedores ayuda a reducir aún más el uso de memoria al eliminar la necesidad de crear objetos temporales.
#include #include #include // Las instancias de Tenant serán la clase Flyweights Tenant { public : Tenant ( const std :: string & name = "" ) : m_name ( name ) {} std :: string name () const { return m_name ; } privado : std :: string m_name ; };// El registro actúa como una fábrica y caché para los objetos de peso mosca del inquilino. Clase Registry { public : Registry () : tenants () {} Tenant findByName ( const std :: string & name ) { if ( tenants . Count ( name ) ! = 0 ) return tenants [ name ]; auto newTenant = Inquilino ( nombre ); inquilinos [ nombre ] = newTenant ; return newTenant ; } privado : std :: map < std :: string , Tenant > tenants ; };// El apartamento asigna un inquilino único a su número de habitación. clase Apartamento { public : Apartamento () : m_occupants (), m_registry () {} void addOccupant ( const std :: string & name , int room ) { m_occupants [ room ] = m_registry . findByName ( nombre ); } void tenants () { for ( auto i : m_occupants ) { const int room = i . primero ; const Inquilino inquilino = i . segundo ; std :: cout << inquilino . nombre () << "ocupa la habitación" << habitación << std :: endl ; } } privado : std :: map < int , Tenant > m_occupants ; Registro m_registry ; };int main () { Apartamento apartamento ; apartamento . addOccupant ( "David" , 1 ); apartamento . addOccupant ( "Sarah" , 3 ); apartamento . addOccupant ( "Jorge" , 2 ); apartamento . addOccupant ( "Lisa" , 12 ); apartamento . addOccupant ( "Michael" , 10 ); apartamento . inquilinos (); return 0 ; }
Ejemplo en PHP
phpclass CoffeeFlavour { cadena privada $ nombre ; matriz estática privada $ CACHE = []; función privada __construct ( cadena $ nombre ) { $ esto -> nombre = $ nombre ; } interno de función estática pública ( cadena $ nombre ) : \ WeakReference { if ( ! isset ( self :: $ CACHE [ $ name ])) { self :: $ CACHE [ $ name ] = new self ( $ name ); } return \ WeakReference :: create ( self :: $ CACHE [ $ nombre ]); } función estática pública flavoursInCache () : int { recuento de devoluciones ( self :: $ CACHE ); } función pública __toString () : string { return $ this -> name ; } }class Order { función estática pública de ( string $ flavourName , int $ tableNumber ) : invocable { $ flavour = CoffeeFlavour :: interno ( $ flavourName ) -> get (); return fn () => print ( "Sirviendo $ sabor a la mesa $ tableNumber " . PHP_EOL ); } }class CoffeeShop { arreglo privado $ pedidos = []; función pública takeOrder ( string $ sabor , int $ tableNumber ) { $ this -> orders [] = Order :: of ( $ sabor , $ tableNumber ); } servicio de función pública () { array_walk ( $ esto -> pedidos , fn ( $ v ) => $ v ()); } }$ tienda = nueva CoffeeShop (); $ tienda -> takeOrder ( "Capuchino" , 2 ); $ tienda -> takeOrder ( "Frappe" , 1 ); $ tienda -> takeOrder ( "Espresso" , 1 ); $ tienda -> takeOrder ( "Frappe" , 897 ); $ tienda -> takeOrder ( "Capuchino" , 97 ); $ tienda -> takeOrder ( "Frappe" , 3 ); $ tienda -> takeOrder ( "Espresso" , 3 ); $ tienda -> takeOrder ( "Capuchino" , 3 ); $ tienda -> takeOrder ( "Espresso" , 96 ); $ tienda -> takeOrder ( "Frappe" , 552 ); $ tienda -> takeOrder ( "Capuchino" , 121 ); $ tienda -> takeOrder ( "Espresso" , 121 ); $ tienda -> servicio (); imprimir ( "objetos CoffeeFlavor en caché:" . CoffeeFlavour :: flavoursInCache ());
Ver también
Referencias
- ^ Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994). Patrones de diseño: elementos de software orientado a objetos reutilizable . Addison Wesley. págs. 195ff . ISBN 978-0-201-63361-0.CS1 maint: varios nombres: lista de autores ( enlace )
- ^ "El patrón de diseño Flyweight - Problema, solución y aplicabilidad" . w3sDesign.com . Consultado el 12 de agosto de 2017 .
- ^ Gamma, Erich ; Richard Helm ; Ralph Johnson ; John Vlissides (1995). Patrones de diseño: elementos de software orientado a objetos reutilizable . Addison-Wesley . págs. 205–206 . ISBN 978-0-201-63361-0.
- ^ Calder, Paul R .; Linton, Mark A. (octubre de 1990). Glifos: objetos de peso mosca para interfaces de usuario . El tercer simposio anual ACM SIGGRAPH sobre software y tecnología de interfaz de usuario. Snowbird, Utah, Estados Unidos. págs. 92-101. doi : 10.1145 / 97924.97935 . ISBN 0-89791-410-4.
- ^ Weinand, Andre; Gamma, Erich; Marty, Rudolf (1988). ET ++: un marco de aplicación orientado a objetos en C ++ . OOPSLA (Sistemas de Programación Orientada a Objetos, Lenguajes y Aplicaciones). San Diego, California, Estados Unidos. págs. 46–57. CiteSeerX 10.1.1.471.8796 . doi : 10.1145 / 62083.62089 . ISBN 0-89791-284-5.
- ^ "El patrón de diseño Flyweight - Estructura y colaboración" . w3sDesign.com . Consultado el 12 de agosto de 2017 .
- ^ "Modelo de datos §" . Referencia del lenguaje Python (en línea) . Fundación de software Python . Consultado el 7 de marzo de 2017 .