Patrón Builder en Python

El patrón Builder nos permite crear diferentes representaciones de objetos utilizando el mismo proceso de construcción. Imagina el proceso de construir una casa: ya sea una villa o un apartamento, ambos requieren pasar por pasos como los cimientos, levantar muros, instalar fontanería y electricidad, pero el resultado final es completamente diferente.

Idea central

El patrón Builder separa el proceso de construcción de un objeto complejo de su representación, de modo que el mismo proceso de construcción pueda crear diferentes representaciones. Este patrón es especialmente adecuado para crear objetos complejos que constan de múltiples componentes.


¿Por qué se necesita el patrón Builder?

Problemas del método de construcción tradicional

Veamos primero un ejemplo sin usar el patrón Builder:

Ejemplo

class Computer:
    def __init__(self, cpu, memory, storage, graphics_card, monitor):
        self.cpu = cpu
        self.memory = memory
        self.storage = storage
        self.graphics_card = graphics_card
        self.monitor = monitor
   
    def __str__(self):
        return f"Computer: CPU={self.cpu}, Memory={self.memory}GB, Storage={self.storage}GB, Graphics={self.graphics_card}, Monitor={self.monitor}"

# Crear objeto de computadora
computer = Computer("Intel i7", 16, 512, "NVIDIA RTX 3060", "27 pulgadas 4K")
print(computer)

Desventajas de este método:

  • Demasiados parámetros en el constructor, difíciles de mantener
  • Hay que recordar el orden de todos los parámetros
  • El proceso de creación no es lo suficientemente flexible
  • Mala legibilidad del código

Componentes del patrón Builder

El patrón Builder incluye cuatro roles principales:

1. Product (Producto)

El objeto complejo que se va a crear

2. Builder (Constructor abstracto)

Define la interfaz abstracta para crear las diferentes partes del producto

3. ConcreteBuilder (Constructor concreto)

Implementa la interfaz Builder, construye y ensambla los componentes

4. Director (Director)

Construye un objeto que utiliza la interfaz Builder.


Implementación completa del código

Implementemos completamente el patrón Builder mediante un ejemplo de configuración de una computadora:

1. Clase de producto (Product)

Ejemplo

class Computer:
    """Clase de producto de computadora"""
    def __init__(self):
        self.cpu = None
        self.memory = None
        self.storage = None
        self.graphics_card = None
        self.monitor = None
   
    def __str__(self):
        specs = []
        if self.cpu:
            specs.append(f"CPU: {self.cpu}")
        if self.memory:
            specs.append(f"Memoria: {self.memory}GB")
        if self.storage:
            specs.append(f"Almacenamiento: {self.storage}GB")
        if self.graphics_card:
            specs.append(f"Tarjeta gráfica: {self.graphics_card}")
        if self.monitor:
            specs.append(f"Monitor: {self.monitor}")
       
        return "Configuración de la computadora:\n" + "\n".join(f"  - {spec}" for spec in specs)

2. Constructor abstracto (Builder)

Ejemplo

from abc import ABC, abstractmethod

class ComputerBuilder(ABC):
    """Clase abstracta de constructor de computadoras"""
   
    def __init__(self):
        self.computer = Computer()
   
    @abstractmethod
    def build_cpu(self):
        pass
   
    @abstractmethod
    def build_memory(self):
        pass
   
    @abstractmethod
    def build_storage(self):
        pass
   
    @abstractmethod
    def build_graphics_card(self):
        pass
   
    @abstractmethod
    def build_monitor(self):
        pass
   
    def get_computer(self):
        return self.computer

3. Constructor concreto (ConcreteBuilder)

Ejemplo

class GamingComputerBuilder(ComputerBuilder):
    """Constructor de computadoras para juegos"""
   
    def build_cpu(self):
        self.computer.cpu = "Intel i9-13900K"
   
    def build_memory(self):
        self.computer.memory = 32
   
    def build_storage(self):
        self.computer.storage = 2000  # 2TB
   
    def build_graphics_card(self):
        self.computer.graphics_card = "NVIDIA RTX 4090"
   
    def build_monitor(self):
        self.computer.monitor = "32 pulgadas 4K 144Hz"

class OfficeComputerBuilder(ComputerBuilder):
    """Constructor de computadoras de oficina"""
   
    def build_cpu(self):
        self.computer.cpu = "Intel i5-13400"
   
    def build_memory(self):
        self.computer.memory = 16
   
    def build_storage(self):
        self.computer.storage = 512
   
    def build_graphics_card(self):
        self.computer.graphics_card = "Tarjeta gráfica integrada"
   
    def build_monitor(self):
        self.computer.monitor = "24 pulgadas 1080P"

4. Director

Ejemplo

class ComputerDirector:
    """Director de construcción de computadoras"""
   
    def __init__(self, builder):
        self.builder = builder
   
    def construct_computer(self):
        """Proceso completo de construcción de una computadora"""
        self.builder.build_cpu()
        self.builder.build_memory()
        self.builder.build_storage()
        self.builder.build_graphics_card()
        self.builder.build_monitor()
   
    def get_computer(self):
        return self.builder.get_computer()

Ejemplo de uso

Ahora veamos cómo usar el patrón Builder para crear diferentes tipos de computadoras:

Ejemplo

def main():
    print("=== Demostración del patrón Builder ==="\n")
   
    # Crear computadora para juegos
    print("1. Construcción de computadora para juegos:")
    gaming_builder = GamingComputerBuilder()
    director = ComputerDirector(gaming_builder)
    director.construct_computer()
    gaming_computer = director.get_computer()
    print(gaming_computer)
   
    print("\n" + "="*50 + "\n")
   
    # Crear computadora de oficina
    print("2. Construcción de computadora de oficina:")
    office_builder = OfficeComputerBuilder()
    director = ComputerDirector(office_builder)
    director.construct_computer()
    office_computer = director.get_computer()
    print(office_computer)
   
    print("\n" + "="*50 + "\n")
   
    # Crear computadora personalizada
    print("3. Construcción de computadora personalizada:")
    custom_builder = CustomComputerBuilder()
    custom_builder.build_cpu("AMD Ryzen 7 7800X3D")
    custom_builder.build_memory(64)
    custom_builder.build_storage(4000)
    custom_builder.build_graphics_card("AMD RX 7900 XTX")
    custom_builder.build_monitor("34 pulgadas ultraancho curvo")
    custom_computer = custom_builder.get_computer()
    print(custom_computer)

# Constructor personalizado (construcción de pasos opcionales)
class CustomComputerBuilder:
    """Constructor de computadoras personalizado"""
   
    def __init__(self):
        self.computer = Computer()
   
    def build_cpu(self, cpu):
        self.computer.cpu = cpu
        return self  # Devuelve a sí mismo, admite llamadas encadenadas
   
    def build_memory(self, memory):
        self.computer.memory = memory
        return self
   
    def build_storage(self, storage):
        self.computer.storage = storage
        return self
   
    def build_graphics_card(self, graphics_card):
        self.computer.graphics_card = graphics_card
        return self
   
    def build_monitor(self, monitor):
        self.computer.monitor = monitor
        return self
   
    def get_computer(self):
        return self.computer

if __name__ == "__main__":
    main()

Variantes del patrón Builder

1. Builder con interfaz fluida

Ejemplo

class Pizza:
    def __init__(self):
        self.size = None
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False
   
    def __str__(self):
        ingredients = []
        if self.cheese: ingredients.append("queso")
        if self.pepperoni: ingredients.append("salchicha italiana")
        if self.mushrooms: ingredients.append("champiñón")
        return f"{self.size} pulgadas de pizza, ingredientes: {', '.join(ingredients)}"

class PizzaBuilder:
    def __init__(self, size):
        self.pizza = Pizza()
        self.pizza.size = size
   
    def add_cheese(self):
        self.pizza.cheese = True
        return self
   
    def add_pepperoni(self):
        self.pizza.pepperoni = True
        return self
   
    def add_mushrooms(self):
        self.pizza.mushrooms = True
        return self
   
    def build(self):
        return self.pizza

# Usar interfaz fluida
pizza = (PizzaBuilder(12)
         .add_cheese()
         .add_pepperoni()
         .add_mushrooms()
         .build())
print(pizza)

Ventajas y desventajas del patrón Builder

Ventajas

Ventajas Descripción
Buen encapsulamiento El proceso de construcción está separado de la representación; el cliente no necesita conocer los detalles internos
Alta extensibilidad Se pueden añadir fácilmente nuevos constructores concretos
Mejor control Permite un control más preciso del proceso de construcción
Legibilidad del código El código es más claro cuando se utiliza el encadenamiento de llamadas

Desventajas

Desventajas Descripción
Aumenta la complejidad Requiere definir múltiples clases, lo que aumenta la complejidad del sistema
No es adecuado cuando los productos difieren mucho Si las diferencias entre productos son muy grandes, el patrón Builder no es muy adecuado
El constructor depende del producto El constructor necesita conocer los detalles específicos del producto

Casos de uso

Situaciones adecuadas para usar el patrón Builder:

  1. Crear objetos complejos: se necesita crear objetos complejos con múltiples componentes
  2. Proceso de construcción estable: el proceso de construcción del objeto es relativamente estable, pero los componentes del objeto cambian con frecuencia
  3. Múltiples representaciones: los objetos que se necesitan crear tienen varias representaciones diferentes
  4. Evitar constructores telescópicos: evitar usar constructores con muchos parámetros

Ejemplos de aplicación práctica:

  • Conversor de documentos: convertir documentos a diferentes formatos como PDF, Word, HTML, etc.
  • Preparación de comidas: preparar comidas de diferentes menús (combinaciones de hamburguesas, papas fritas, bebidas)
  • Construcción de componentes de UI: construir componentes de interfaz de usuario complejos
  • Generación de informes: generar informes que incluyen diferentes secciones (encabezados, datos, gráficos)

Ejercicios prácticos

Ejercicio 1: Implementar un constructor de automóviles

Intenta implementar un patrón Builder para un automóvil, con los siguientes componentes:

  • Motor (gasolina, eléctrico, híbrido)
  • Color de la carrocería (rojo, azul, negro, blanco)
  • Tamaño de las ruedas (16 pulgadas, 18 pulgadas, 20 pulgadas)
  • Interior (cuero, tela)

Ejercicio 2: Mejorar el constructor de computadoras

Agrega las siguientes funcionalidades al constructor de computadoras:

  • Componentes opcionales (por ejemplo, si se instala una unidad de disco)
  • Lógica de validación (por ejemplo, la memoria no puede ser inferior a 4 GB)
  • Función de cálculo de precio

Resumen

El patrón Builder es un potente patrón de diseño creacional que descompone el proceso de construcción de objetos complejos en varios pasos simples, haciendo que el proceso de creación sea más flexible y controlable. Aunque aumenta la complejidad del sistema, en escenarios donde se necesitan crear objetos complejos y el proceso de construcción es relativamente fijo, el patrón Builder puede mejorar significativamente la mantenibilidad y extensibilidad del código.

Recuerda la idea central de los patrones de diseño:No usar el patrón solo por usarlo, sino para resolver problemas de diseño específicos.. En el desarrollo real, debes decidir si usar el patrón Builder según los requisitos concretos.

Otras extensiones