Patrón de peso mosca - Flyweight pattern

Una captura de pantalla del paquete Writer de LibreOffice.
Los editores de texto, como LibreOffice Writer , a menudo usan el patrón de peso mosca.

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 . 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 .

El término fue acuñado por primera vez, y la idea fue ampliamente explorada por Paul Calder y Mark Linton en 1990 para manejar de manera eficiente la información de glifos en un editor de documentos WYSIWYG . Sin embargo, ya se utilizaron técnicas similares en otros sistemas ya en 1988.

Visió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 son las estructuras de datos utilizadas para representar caracteres en un procesador de textos . Ingenuamente, 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, cada carácter puede tener 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 debe almacenarse internamente.

Como resultado, los objetos de peso mosca pueden:

  • almacenar un estado intrínseco que es invariante, independiente del contexto y que se puede compartir (por ejemplo, el código del carácter 'A' en un conjunto de caracteres determinado)
  • proporcionar una interfaz para pasar en estado extrínseco que es variante, dependiente del contexto y no se puede compartir (por ejemplo, la posición del carácter 'A' en un documento de texto)

Los clientes pueden reutilizar Flyweightobjetos y pasarlos en estado extrínseco según sea necesario, reduciendo el número de objetos creados físicamente.

Estructura

Un ejemplo de diagrama de secuencia y clase UML para el patrón de diseño Flyweight.

El diagrama de clases de UML anterior muestra:

  • la Clientclase, que usa el patrón de peso mosca
  • la FlyweightFactoryclase, que crea y comparte Flyweightobjetos
  • la Flyweight interfaz , que toma en estado extrínseco y realiza una operación
  • la Flyweight1clase, que implementa Flyweighty almacena el estado intrínseco

El diagrama de secuencia muestra las siguientes interacciones en tiempo de ejecución :

  1. El Clientobjeto llama getFlyweight(key)al FlyweightFactory, que devuelve un Flyweight1objeto.
  2. Después de llamar operation(extrinsicState)al Flyweight1objeto devuelto , de Clientnuevo llama getFlyweight(key)al FlyweightFactory.
  3. El FlyweightFactorydevuelve el ya existente Flyweight1objeto.

Detalles de implementacion

Hay varias formas de implementar el patrón de peso mosca. Un ejemplo es la mutabilidad: si los objetos que almacenan el estado de peso mosca extrínseco pueden cambiar.

Los objetos inmutables se comparten fácilmente, pero requieren la creación de nuevos objetos extrínsecos cada vez que se produce un cambio de estado. Por el contrario, los objetos mutables pueden compartir estado. La mutabilidad permite una mejor reutilización de objetos mediante el almacenamiento en caché y la reinicialización de objetos antiguos que no se utilizan. Compartir no suele ser viable cuando el estado es muy variable.

Otras preocupaciones principales incluyen la recuperación (cómo el cliente final accede al peso mosca), el almacenamiento en caché y la simultaneidad .

Recuperación

La interfaz de fábrica para crear o reutilizar objetos de peso mosca es a menudo una fachada para un sistema subyacente complejo. Por ejemplo, la interfaz de fábrica se implementa comúnmente como singleton para proporcionar acceso global para crear pesos mosca.

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.

Por lo general, la solicitud se reenvía a una caché adecuada según el tipo de objeto que sea. Si la solicitud la cumple un objeto en la caché, puede reinicializarse y devolverse. De lo contrario, se crea una instancia de un nuevo objeto. Si el objeto se divide en varios subcomponentes extrínsecos, se unirán antes de devolver el objeto.

Almacenamiento en caché

Hay dos formas de almacenar en caché objetos flyweight: cachés mantenidos y no mantenidos.

Los objetos con un estado muy variable se pueden almacenar en caché con una estructura FIFO . Esta estructura mantiene los objetos no utilizados en la caché, sin necesidad de buscar en la caché.

Por el contrario, los cachés no mantenidos tienen menos sobrecarga inicial: los objetos para los cachés se inicializan de forma masiva en el momento de la compilación o el inicio. Una vez que los objetos llenan la caché, el algoritmo de recuperación de objetos puede tener más sobrecarga asociada que las operaciones push / pop de una caché mantenida.

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 no se encuentra tal objeto, se debe inicializar uno con ese estado. Al recuperar objetos extrínsecos con estado mutable, se debe buscar en la caché un objeto no utilizado para reinicializar si no se encuentra ningún objeto utilizado. Si no hay ningún objeto no utilizado disponible, se debe crear una instancia de un nuevo objeto y agregarlo a la caché.

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é. Este sistema de almacenamiento en caché de objetos se puede encapsular con el patrón de cadena de responsabilidad , que promueve un acoplamiento flexible entre los componentes.

Concurrencia

Se debe tener en cuenta una consideración especial cuando se crean objetos de peso mosca en varios subprocesos. Si la lista de valores es finita y se conoce de antemano, los pesos mosca se pueden instanciar antes de tiempo y recuperar de un contenedor en múltiples subprocesos sin contención. Si se crean instancias de flyweights en varios subprocesos, hay dos opciones:

  1. Haga que la instanciación de flyweight sea de un solo subproceso, lo que introduce la contención y garantiza una instancia por valor.
  2. Permita que los subprocesos concurrentes creen múltiples instancias de peso ligero, eliminando así la contención y permitiendo múltiples instancias por valor.

Para permitir el uso compartido seguro entre clientes y subprocesos, los objetos de peso ligero se pueden convertir en objetos de valor inmutable , donde dos instancias se consideran iguales si sus valores son iguales.

Este ejemplo de C # 9 usa registros para crear un objeto de valor que represente sabores de café:

public record CoffeeFlavours(string flavour);

Ejemplo en C #

En este ejemplo, FlyweightPointercrea un miembro estático que se utiliza para cada instancia de la MyObjectclase.

// Defines Flyweight object that repeats itself.
public class Flyweight
{
    public string CompanyName { get; set; }
    public string CompanyLocation { get; set; }
    public string CompanyWebsite { get; set; }
    // Bulky data
    public byte[] CompanyLogo { get; set; }
}

public static class FlyweightPointer
{
    public static readonly Flyweight Company = new Flyweight
    {
        CompanyName = "Abc",
        CompanyLocation = "XYZ",
        CompanyWebsite = "www.example.com"
        // Load CompanyLogo here
    };
}

public class MyObject
{
    public string Name { get; set; }
    public string Company => FlyweightPointer.Company.CompanyName;
}

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. De esta forma, 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.

# Instances of CheeseBrand will be the Flyweights
class CheeseBrand:
    def __init__(self, brand: str, cost: float) -> None:
        self.brand = brand
        self.cost = cost
        self._immutable = True  # Disables future attributions

    def __setattr__(self, name, value):
        if getattr(self, "_immutable", False):  # Allow initial attribution
            raise RuntimeError("This object is immutable")
        else:
            super().__setattr__(name, value)

class CheeseShop:
    menu = {}  # Shared container to access the Flyweights

    def __init__(self) -> None:
        self.orders = {}  # per-instance container with private attributes

    def stock_cheese(self, brand: str, cost: float) -> None:
        cheese = CheeseBrand(brand, cost)
        self.menu[brand] = cheese  # Shared Flyweight

    def sell_cheese(self, brand: str, units: int) -> None:
        self.orders.setdefault(brand, 0)
        self.orders[brand] += units  # Instance attribute

    def total_units_sold(self):
        return sum(self.orders.values())

    def total_income(self):
        income = 0
        for brand, units in self.orders.items():
            income += self.menu[brand].cost * units
        return income

shop1 = CheeseShop()
shop2 = CheeseShop()

shop1.stock_cheese("white", 1.25)
shop1.stock_cheese("blue", 3.75)
# Now every CheeseShop have 'white' and 'blue' on the inventory
# The SAME 'white' and 'blue' CheeseBrand

shop1.sell_cheese("blue", 3)  # Both can sell
shop2.sell_cheese("blue", 8)  # But the units sold are stored per-instance

assert shop1.total_units_sold() == 3
assert shop1.total_income() == 3.75 * 3

assert shop2.total_units_sold() == 8
assert shop2.total_income() == 3.75 * 8

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 <iostream>
#include <map>
#include <string>

// Instances of Tenant will be the Flyweights
class Tenant {
public:
    Tenant(const std::string& name = "") : m_name(name) {}

    std::string name() const {
        return m_name;
    }
private:
    std::string m_name;
};

// Registry acts as a factory and cache for Tenant flyweight objects
class Registry {
public:
    Registry() : tenants() {}

    Tenant& findByName(const std::string& name) {
        if (tenants.count(name) != 0) return tenants[name];
        Tenant newTenant{name};
        tenants[name] = newTenant;
        return tenants[name];
    }
private:
    std::map<std::string,Tenant> tenants;
};

// Apartment maps a unique tenant to their room number.
class Apartment {
public:
    Apartment() : m_occupants(), m_registry() {}

    void addOccupant(const std::string& name, int room) {
        m_occupants[room] = &m_registry.findByName(name);
    }

    void tenants() {
        for (auto i : m_occupants) {
            const int room = i.first;
            const auto tenant = i.second;
            std::cout << tenant->name() << " occupies room " << room << std::endl;
        }
    }
private:
    std::map<int,Tenant*> m_occupants;
    Registry m_registry;
};

int main() {
    Apartment apartment;
    apartment.addOccupant("David", 1);
    apartment.addOccupant("Sarah", 3);
    apartment.addOccupant("George", 2);
    apartment.addOccupant("Lisa", 12);
    apartment.addOccupant("Michael", 10);
    apartment.tenants();

    return 0;
}

Ejemplo en PHP

<?php

class CoffeeFlavour  {

    private string $name;
    private static array $CACHE = [];
    
    private function __construct(string $name){
        $this->name = $name;
    }
    
    public static function intern(string $name) : \WeakReference {
        if(!isset(self::$CACHE[$name])){
            self::$CACHE[$name] =  new self($name);
        }
        return \WeakReference::create(self::$CACHE[$name]);
    }
    
    public static function flavoursInCache() : int {
        return count(self::$CACHE);
    }
    
    public function __toString() : string {
        return $this->name;
    }
    
}

class Order {
    
    public static function of(string $flavourName, int $tableNumber) : callable {
        $flavour = CoffeeFlavour::intern($flavourName)->get();
        return fn() => print("Serving $flavour to table $tableNumber ".PHP_EOL);
    }
    
}

class CoffeeShop {
    
    private array $orders = [];

    public function takeOrder(string $flavour, int $tableNumber) {
        $this->orders[] = Order::of($flavour, $tableNumber);
    }

    public function service() {
        array_walk($this->orders, fn($v) => $v());
    }
}

$shop = new CoffeeShop();
$shop->takeOrder("Cappuccino", 2);
$shop->takeOrder("Frappe", 1);
$shop->takeOrder("Espresso", 1);
$shop->takeOrder("Frappe", 897);
$shop->takeOrder("Cappuccino", 97);
$shop->takeOrder("Frappe", 3);
$shop->takeOrder("Espresso", 3);
$shop->takeOrder("Cappuccino", 3);
$shop->takeOrder("Espresso", 96);
$shop->takeOrder("Frappe", 552);
$shop->takeOrder("Cappuccino", 121);
$shop->takeOrder("Espresso", 121);
$shop->service();
print("CoffeeFlavor objects in cache: ". CoffeeFlavour::flavoursInCache());

Ver también

Referencias