Design Patterns - Flyweight

Propósito

El patrón Flyweight (Peso ligero) se basa en la idea de compartir objetos que se repiten, en vez de crear una nueva instancia de cada uno de ellos.

Se construyen objetos ligeros a partir de la información redundante (compartida por varios objetos) y se referencian por los nuevos objetos.

Esto optimiza el uso de memoria ya que elimina la redundancia de objetos con propiedades idénticas.

Problema

El problema que resuelve es el uso excesivo y sobrecarga de memoria debido a la creación de un gran número de objetos de una clase, que tienen casi el mismo estado o datos (redundancia).

Solución

La solución que propone el Flyweight es:

  • Separación de Datos Intrínsecos y Extrínsecos: Almacena los datos comunes (intrínsecos) en objetos flyweight y pasa los datos específicos del contexto (extrínsecos) a través de métodos.
  • Reutilización de Objetos: Permite la reutilización de objetos flyweight en diferentes contextos, evitando crear nuevos objetos.

Se comparte el estado intrínseco común entre muchos objetos, esto significa que se crea un número menor de objetos, y cada objeto almacena solo su estado extrínseco.

Estructura

flyweight

Participantes

  • Flyweight: Define una interfaz a través de la cual los flyweights pueden recibir un estado extrínseco y actuar él.
  • ConcreteFlyweight: Implementa la interfaz Flyweight y almacena el estado intrínseco. Un objeto ConcreteFlyweight debe poder ser compartido, por lo que cualquier estado que almacene debe ser intrínseco.
  • UnsharedConcreteFlyweight: Implementa la interfaz Flyweight y almacena el estado extrínseco, no es compartido. Suelen tener objetos ConcreteFlyweight como hijos en algún nivel de la estructura de objetos.
  • FlyweightFactory: Crea y gestiona los objetos Flyweight y asegura que se compartan correctamente (cuando un cliente solicita un Flyweight, el objeto FlyweightFactory proporciona una instancia concreta o crea una nueva, en caso de que no exista ninguno).
  • Client: Mantiene una referencia a los flyweights y calcula o guarda los estados extrínsecos.

Cuándo Usarlo

Este patrón es recomendable cuando:

  • hay muchos objetos con estados similares que pueden ser extraídos y compartidos.
  • hay que reducir la carga de memoria.

Ventajas

Eficiencia en el Uso de la Memoria: Reduce significativamente el uso de memoria cuando hay muchas instancias de objetos.

Rendimiento Mejorado: Puede mejorar el rendimiento en sistemas con restricciones de memoria.

Desventajas

Complejidad Adicional: Introduce una mayor complejidad en el diseño.

Compromiso entre Tiempo y Espacio: Puede aumentar el tiempo de ejecución debido a la necesidad de manejar los estados extrínsecos.

Dificultad en la Implementación: Requiere una cuidadosa planificación y ejecución para ser efectivo.

Ejemplo: Juego de Ajedrez

Debemos desarrollar un juego de ajedrez. Cada pieza de ajedrez (como peones, torres, caballos) tiene características comunes, como color y posición, pero también comportamientos específicos (posición en el tablero).

Problema

El desafío principal es gestionar eficientemente la memoria y los recursos del sistema, especialmente cuando representamos un gran número de piezas en el tablero.

Crear un objeto separado para cada pieza en el tablero puede ser muy costoso en términos de recursos, especialmente cuando muchas piezas comparten propiedades similares, como el color.

Solución planteada

Vamos a usar el patrón Flyweight para optimizar el uso de memoria y mejorar el rendimiento del juego. 

Separamos el estado intrínseco común (tipo y color), que se puede compartir entre muchas piezas, del estado extrínseco (posición en el tablero), que es único para cada pieza.

Definimos la interfaz común para todas las piezas ChessPiece con el método dibujar. Creamos una pieza concreta Pawn, que mantiene su estado intrínseco (color). Creamos la clase ChessPiecePosition para manejar el estado extrínseco (la posición). Mediante el ChessPieceFactory vamos a poder gestionar la creación de instancias.

flyweight

Código Java

Codificamos en Java lo que preparamos en el diagrama.

Definimos la Interfaz Flyweight:


              
  interface ChessPiece {
      void draw(ChessPiecePosition position);
  }
              
              

Implementamos un ConcreteFlyweight:


              
  class Pawn implements ChessPiece {
      private String color; // Estado intrínseco
  
      public Pawn(String color) {
          this.color = color;
      }
  
      public void draw(ChessPiecePosition position) {
          System.out.println("Dibujando peón " + color + " en posición " + position.getX() + "," + position.getY());
      }
  }
              
              

Creamos la clase UnsharedConcreteFlyweight:


              
  class ChessPiecePosition {
      private int x, y;
  
      public ChessPiecePosition(int x, int y) {
          this.x = x;
          this.y = y;
      }
  
      // Getters y setters para x e y
  }
              
              

Creamos la clase FlyweightFactory:


              
  class ChessPieceFactory {
      private static final Map PIECES = new HashMap<>();
  
      public static ChessPiece getChessPiece(String color) {
          PIECES.computeIfAbsent(color, c -> new Pawn(c));
          return PIECES.get(color);
      }
  }
              
              

Usamos el Flyweight en el cliente:


              
  public class Client {
      public static void main(String[] args) {
          ChessPiece whitePawn = ChessPieceFactory.getChessPiece("Blanco");
          ChessPiecePosition position1 = new ChessPiecePosition(0, 1);
          whitePawn.draw(position1);
  
          ChessPiece blackPawn = ChessPieceFactory.getChessPiece("Negro");
          ChessPiecePosition position2 = new ChessPiecePosition(0, 6);
          blackPawn.draw(position2);
      }
  }
  
              

Mapeo (del ejemplo a Participantes)

Los participantes que vimos antes son: Flyweight, ConcreteFlyweight, UnsharedConcreteFlyweight, FlyweightFactory, Client:

  • ChessPiece (Flyweight): Una interfaz común para todas las piezas de ajedrez, que permite dibujar una pieza en el tablero en una posición dada.
  • Pawn (ConcreteFlyweight): Representa una pieza específica de ajedrez (en este caso, un peón) y almacena el estado intrínseco, el color en este ejemplo.
  • ChessPiecePosition (UnsharedConcreteFlyweight): Mantiene el estado extrínseco, que en este caso son las coordenadas ‘x’ e ‘y’ de una pieza en el tablero de ajedrez.
  • ChessPieceFactory (FlyweightFactory): Es responsable de crear y gestionar los objetos Flyweight y asegura que se reutilicen las instancias apropiadas. Crea un Pawn solo si no existe uno con el color deseado.
  • Client: Usa la FlyweightFactory para obtener instancias de las piezas de ajedrez y las coloca en el tablero usando estados extrínsecos.

Conclusiones

El patrón Flyweight en nuestro juego de ajedrez digital permite un uso eficiente de la memoria y mejora el rendimiento al compartir piezas comunes, como peones de un mismo color, entre múltiples posiciones en el tablero, mientras se mantiene el estado específico de cada pieza por separado.

Patrones relacionados

  • Composite

Se puede combinar con Flyweight para implementar estructuras lógicas jerárquicas.

  • State y Strategy

Suele ser mejor implementar los objetos State y Strategy como pesos ligeros.