3.3. Patrones estructurales#
Los patrones estructurales son un conjunto de patrones de diseño en ingeniería de software que se centran en la composición de clases y objetos para formar estructuras más grandes y complejas. Estos patrones de diseño ayudan a definir relaciones entre objetos y clases de manera más flexible, permitiendo cambios en la estructura del sistema sin tener que cambiar sus componentes internos.
3.3.1. Adapter#
El patrón Adapter es un patrón de diseño estructural que permite que interfaces incompatibles trabajen juntas. Este patrón convierte la interfaz de una clase en otra interfaz que un cliente espera, permitiendo que objetos con interfaces incompatibles puedan colaborar entre sí.
Estructura del Patrón Adapter:
Cliente: Es la clase que interactúa con el Adaptador para utilizar los servicios proporcionados por el Adaptee.
Target: Define la interfaz específica que utiliza el Cliente.
Adaptee: Es la clase existente que tiene una interfaz incompatible con la que el Cliente espera.
Adapter: Es la clase que conecta el Cliente con el Adaptee. Implementa la interfaz del Target y mantiene una referencia al Adaptee, delegando las llamadas del Cliente al Adaptee.
En este ejemplo, supongamos que tenemos un sistema que necesita mostrar información de libros, pero la clase Book
proporciona una interfaz incompatibles con la interfaz requerida por el cliente. Utilizaremos el patrón Adapter para adaptar la interfaz de Book
a la interfaz requerida por el cliente.
from abc import ABC, abstractmethod
# Target
class BookViewer(ABC):
@abstractmethod
def display_info(self):
pass
# Adaptee
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def get_title(self):
return self.title
def get_author(self):
return self.author
# Adapter
class BookAdapter(BookViewer):
def __init__(self, book):
self.book = book
def display_info(self):
print(f"Title: {self.book.get_title()}")
print(f"Author: {self.book.get_author()}")
# Cliente
class App:
def __init__(self, book_viewer):
self.book_viewer = book_viewer
def display_book_info(self):
self.book_viewer.display_info()
# Uso del patrón Adapter
book = Book("The Hobbit", "J.R.R. Tolkien")
adapter = BookAdapter(book)
app = App(adapter)
app.display_book_info()
Title: The Hobbit
Author: J.R.R. Tolkien
En este ejemplo:
Book
es la clase existente con una interfaz incompatible.BookViewer
define la interfaz requerida por el cliente.BookAdapter
es el adaptador que conecta la interfaz deBook
con la interfaz deBookViewer
.App
es el cliente que utiliza la interfaz deBookViewer
para mostrar la información del libro.
El adaptador BookAdapter
permite al cliente utilizar la clase Book
a través de la interfaz de BookViewer
, a pesar de que sus interfaces son incompatibles. Esto permite una mayor flexibilidad y reutilización de código en el sistema.
3.3.2. Bridge#
El patrón Bridge es un patrón de diseño estructural que separa la abstracción de su implementación, permitiendo que ambas puedan variar de forma independiente. Este patrón se utiliza cuando se necesita evitar una unión fija entre la abstracción y su implementación, lo que permite que ambos puedan cambiar y evolucionar de forma independiente sin necesidad de modificar el otro.
Estructura del Patrón Bridge:
Abstracción: Define la interfaz de alto nivel que el cliente utiliza para interactuar con el sistema. Mantiene una referencia a un objeto de Implementación.
Implementación: Define la interfaz de bajo nivel que define las operaciones concretas que se pueden realizar. Puede haber múltiples implementaciones, y la abstracción se refiere a una de ellas.
Implementador Concreto: Es una implementación concreta de la interfaz Implementación.
Refinamiento Abstracción: Extiende la interfaz de Abstracción proporcionando una funcionalidad más detallada.
Supongamos que tenemos un sistema de formas geométricas que necesitan ser dibujadas en diferentes sistemas operativos (Windows y Linux). Utilizaremos el patrón Bridge para separar las clases de formas de su implementación de dibujo.
from abc import ABC, abstractmethod
# Implementación
class DrawingAPI(ABC):
@abstractmethod
def draw_circle(self, x, y, radius):
pass
# Implementación Concreta
class DrawingAPI1(DrawingAPI):
def draw_circle(self, x, y, radius):
print(f"API1 drawing a circle at ({x}, {y}) with radius {radius}")
# Implementación Concreta
class DrawingAPI2(DrawingAPI):
def draw_circle(self, x, y, radius):
print(f"API2 drawing a circle at ({x}, {y}) with radius {radius}")
# Abstracción
class Shape(ABC):
def __init__(self, drawing_api):
self._drawing_api = drawing_api
@abstractmethod
def draw(self):
pass
# Refinamiento de Abstracción
class CircleShape(Shape):
def __init__(self, x, y, radius, drawing_api):
super().__init__(drawing_api)
self._x = x
self._y = y
self._radius = radius
def draw(self):
self._drawing_api.draw_circle(self._x, self._y, self._radius)
# Uso del patrón Bridge
shapes = [
CircleShape(1, 2, 3, DrawingAPI1()),
CircleShape(5, 7, 11, DrawingAPI2())
]
for shape in shapes:
shape.draw()
API1 drawing a circle at (1, 2) with radius 3
API2 drawing a circle at (5, 7) with radius 11
En este ejemplo:
DrawingAPI
define la interfaz para la implementación de dibujo.DrawingAPI1
yDrawingAPI2
son implementaciones concretas de la interfaz de dibujo.Shape
es la abstracción que define la interfaz de alto nivel para las formas geométricas.CircleShape
es una forma geométrica concreta que extiende la abstracciónShape
.Al ejecutar el código, cada forma geométrica se dibuja utilizando la implementación de dibujo adecuada, independientemente del sistema operativo o la tecnología de dibujo utilizada.
Este patrón permite que las clases de abstracción y las clases de implementación puedan variar y evolucionar de forma independiente, lo que facilita la extensión y el mantenimiento del sistema.
3.3.3. Composite#
El patrón Composite es un patrón de diseño estructural que permite tratar objetos individuales y composiciones de objetos de manera uniforme. Este patrón compone objetos en estructuras de árbol para representar jerarquías de parte-todo. Esto significa que los clientes pueden tratar tanto a los objetos compuestos como a los individuales de manera uniforme.
Estructura del Patrón Composite:
Componente: Define la interfaz común para todos los componentes, ya sean objetos individuales o compuestos.
Hoja (Leaf): Representa los objetos individuales en la composición. Implementa la interfaz del componente.
Composite: Es un objeto que contiene una colección de componentes. Implementa la interfaz del componente y define operaciones específicas para trabajar con sus hijos.
Supongamos que queremos modelar una estructura de árbol para representar una empresa. Utilizaremos el patrón Composite para modelar departamentos y empleados de manera uniforme.
from abc import ABC, abstractmethod
# Componente
class Component(ABC):
@abstractmethod
def show_name(self):
pass
# Hoja (Leaf)
class Employee(Component):
def __init__(self, name):
self.name = name
def show_name(self):
return self.name
# Composite
class Department(Component):
def __init__(self, name):
self.name = name
self.children = []
def add(self, component):
self.children.append(component)
def remove(self, component):
self.children.remove(component)
def show_name(self):
names = [self.name]
for child in self.children:
names.append(child.show_name())
return ' -> '.join(names)
# Uso del patrón Composite
marketing = Department("Marketing")
sales = Department("Sales")
marketing.add(Employee("John"))
marketing.add(Employee("Alice"))
sales.add(Employee("Bob"))
sales.add(Employee("Carol"))
company = Department("Company")
company.add(marketing)
company.add(sales)
print(company.show_name())
Company -> Marketing -> John -> Alice -> Sales -> Bob -> Carol
En este ejemplo:
Component
define la interfaz común para todos los componentes.Employee
es una hoja que representa a un empleado individual.Department
es un composite que representa un departamento que puede contener empleados individuales o subdepartamentos.Al ejecutar el código, se imprime la estructura jerárquica de la empresa, mostrando los nombres de los departamentos y los empleados.
Este patrón permite tratar tanto a los objetos compuestos como a los individuales de manera uniforme, lo que facilita la manipulación de estructuras jerárquicas de parte-todo.
3.3.4. Decorator#
El patrón Decorator es un patrón de diseño estructural que permite agregar funcionalidad adicional a objetos dinámicamente sin modificar su estructura básica. Este patrón se basa en la composición en lugar de la herencia, lo que significa que permite a los objetos agregar nuevas características al envolverlos con objetos decoradores.
Estructura del Patrón Decorator:
Componente: Define la interfaz común para los objetos que pueden tener responsabilidades adicionales agregadas dinámicamente.
Componente Concreto: Implementa la interfaz del componente y define el comportamiento básico.
Decorador: Es una clase abstracta que contiene una referencia a un objeto del mismo tipo que el componente. También implementa la interfaz del componente y redirige las llamadas al objeto envuelto.
Decorador Concreto: Agrega funcionalidad adicional al componente. Puede envolver otros decoradores para agregar múltiples capas de funcionalidad.
Supongamos que tenemos una interfaz de Coffee
y una implementación básica SimpleCoffee
. Queremos agregar funcionalidades adicionales, como leche o crema, a nuestro café sin modificar la clase SimpleCoffee
. Utilizaremos el patrón Decorator para lograr esto.
from abc import ABC, abstractmethod
# Componente
class Coffee(ABC):
@abstractmethod
def get_cost(self):
pass
@abstractmethod
def get_description(self):
pass
# Componente Concreto
class SimpleCoffee(Coffee):
def get_cost(self):
return 10
def get_description(self):
return "Simple Coffee"
# Decorador
class CoffeeDecorator(Coffee):
def __init__(self, decorated_coffee):
self.decorated_coffee = decorated_coffee
def get_cost(self):
return self.decorated_coffee.get_cost()
def get_description(self):
return self.decorated_coffee.get_description()
# Decorador Concreto
class Milk(CoffeeDecorator):
def __init__(self, decorated_coffee):
super().__init__(decorated_coffee)
def get_cost(self):
return self.decorated_coffee.get_cost() + 2
def get_description(self):
return self.decorated_coffee.get_description() + ", Milk"
# Decorador Concreto
class Cream(CoffeeDecorator):
def __init__(self, decorated_coffee):
super().__init__(decorated_coffee)
def get_cost(self):
return self.decorated_coffee.get_cost() + 5
def get_description(self):
return self.decorated_coffee.get_description() + ", Cream"
# Uso del patrón Decorator
coffee = SimpleCoffee()
print("Description:", coffee.get_description(), "Cost:", coffee.get_cost())
coffee_with_milk = Milk(coffee)
print("Description:", coffee_with_milk.get_description(), "Cost:", coffee_with_milk.get_cost())
coffee_with_cream = Cream(coffee)
print("Description:", coffee_with_cream.get_description(), "Cost:", coffee_with_cream.get_cost())
Description: Simple Coffee Cost: 10
Description: Simple Coffee, Milk Cost: 12
Description: Simple Coffee, Cream Cost: 15
En este ejemplo:
Coffee
es la interfaz común para los diferentes tipos de café.SimpleCoffee
es una implementación básica deCoffee
.CoffeeDecorator
es la clase abstracta que define la estructura de los decoradores.Milk
yCream
son decoradores concretos que agregan funcionalidad adicional a un objeto de café.Al ejecutar el código, se muestra cómo se puede agregar leche o crema a un café básico sin modificar la clase
SimpleCoffee
.
El patrón Decorator permite agregar funcionalidades adicionales a los objetos existentes de forma dinámica y flexible, lo que hace que el código sea más modular y fácil de extender.
3.3.5. Facade#
El patrón Facade es un patrón de diseño estructural que proporciona una interfaz unificada para un conjunto de interfaces en un subsistema. Este patrón define una interfaz de nivel superior que facilita el uso de un sistema más grande o complejo, ocultando la complejidad del sistema y proporcionando una interfaz simplificada para interactuar con él.
Estructura del Patrón Facade:
Facade: Proporciona una interfaz unificada para un conjunto de interfaces en un subsistema. Conoce qué clases del subsistema son responsables de una solicitud específica y delega las solicitudes del cliente a estas clases.
Subsistema: Consiste en un conjunto de clases que implementan funcionalidades específicas del sistema.
Supongamos que tenemos un sistema de computación que consta de varias partes, como el procesador, la memoria y la unidad de almacenamiento. Utilizaremos el patrón Facade para proporcionar una interfaz simplificada para encender y apagar la computadora.
# Subsistema
class CPU:
def boot(self):
print("CPU is booting")
def shutdown(self):
print("CPU is shutting down")
# Subsistema
class Memory:
def load(self):
print("Memory is loading")
def free(self):
print("Memory is free")
# Subsistema
class Storage:
def read(self):
print("Storage is reading")
def write(self):
print("Storage is writing")
# Facade
class ComputerFacade:
def __init__(self):
self.cpu = CPU()
self.memory = Memory()
self.storage = Storage()
def start(self):
self.cpu.boot()
self.memory.load()
self.storage.read()
def shutdown(self):
self.storage.write()
self.memory.free()
self.cpu.shutdown()
# Uso del patrón Facade
computer = ComputerFacade()
computer.start()
print("\nComputer is running...\n")
computer.shutdown()
CPU is booting
Memory is loading
Storage is reading
Computer is running...
Storage is writing
Memory is free
CPU is shutting down
En este ejemplo:
CPU
,Memory
yStorage
son subsistemas que representan diferentes partes de la computadora.ComputerFacade
es la fachada que proporciona una interfaz unificada para encender y apagar la computadora.Al ejecutar el código, la fachada
ComputerFacade
oculta la complejidad del encendido y apagado de la computadora al cliente, proporcionando una interfaz simple para interactuar con el sistema.
El patrón Facade promueve la simplicidad y el desacoplamiento al proporcionar una interfaz simplificada para un sistema más grande o complejo. Esto facilita el uso y la comprensión del sistema por parte del cliente.
3.3.6. Flyweight#
El patrón Flyweight es un patrón de diseño estructural que se utiliza para minimizar el uso de memoria o el costo de la creación de objetos mediante el uso compartido de objetos similares. Este patrón se basa en el concepto de reutilización de objetos existentes en lugar de crear nuevos objetos, lo que ayuda a reducir el consumo de recursos y mejorar el rendimiento de la aplicación.
Estructura del Patrón Flyweight:
Flyweight: Define una interfaz mediante la cual los objetos flyweight pueden recibir y actuar sobre los datos compartidos. También almacena el estado intrínseco (compartido) de los objetos flyweight.
ConcreteFlyweight: Implementa la interfaz Flyweight y almacena el estado extrínseco (único) del objeto flyweight. El estado extrínseco no es compartido y debe ser proporcionado externamente.
FlyweightFactory: Crea y administra objetos flyweight. Almacena una pool de objetos flyweight y los proporciona a los clientes cuando se solicitan. Si un objeto flyweight ya existe en el pool, lo devuelve en lugar de crear uno nuevo.
Supongamos que tenemos un sistema de procesamiento de texto donde necesitamos representar cada carácter del texto como un objeto flyweight para minimizar el uso de memoria. Utilizaremos el patrón Flyweight para lograr esto.
class Flyweight:
def __init__(self, intrinsic_state):
self.intrinsic_state = intrinsic_state
def operation(self, extrinsic_state):
pass
class ConcreteFlyweight(Flyweight):
def operation(self, extrinsic_state):
print(f"Operation with intrinsic state '{self.intrinsic_state}' and extrinsic state '{extrinsic_state}'")
class FlyweightFactory:
def __init__(self):
self.flyweights = {}
def get_flyweight(self, intrinsic_state):
if intrinsic_state not in self.flyweights:
self.flyweights[intrinsic_state] = ConcreteFlyweight(intrinsic_state)
return self.flyweights[intrinsic_state]
# Uso del patrón Flyweight
factory = FlyweightFactory()
flyweight1 = factory.get_flyweight("A")
flyweight2 = factory.get_flyweight("A")
flyweight3 = factory.get_flyweight("B")
flyweight1.operation("State 1")
flyweight2.operation("State 2")
flyweight3.operation("State 3")
Operation with intrinsic state 'A' and extrinsic state 'State 1'
Operation with intrinsic state 'A' and extrinsic state 'State 2'
Operation with intrinsic state 'B' and extrinsic state 'State 3'
En este ejemplo:
Flyweight
define la interfaz para los objetos flyweight.ConcreteFlyweight
implementa la interfazFlyweight
y almacena el estado intrínseco.FlyweightFactory
crea y administra objetos flyweight. Almacena una pool de objetos flyweight y los proporciona a los clientes cuando se solicitan.
Al ejecutar el código, se puede ver que el objeto flyweight “A” se comparte entre flyweight1
y flyweight2
, mientras que el objeto flyweight “B” es único para flyweight3
. Esto demuestra cómo el patrón Flyweight puede minimizar el uso de memoria mediante el uso compartido de objetos similares.
3.3.7. Proxy#
El patrón Proxy es un patrón de diseño estructural que proporciona un substituto o marcador de posición para otro objeto para controlar el acceso a ese objeto. El Proxy actúa como intermediario entre el cliente y el objeto real, permitiendo agregar funcionalidades adicionales como el control de acceso, la creación diferida, el almacenamiento en caché, etc.
Estructura del Patrón Proxy:
Sujeto (Subject): Define la interfaz común para el objeto real y el Proxy, de modo que el Proxy pueda ser utilizado en lugar del objeto real.
Proxy: Mantiene una referencia al objeto real y controla el acceso a él. Implementa la misma interfaz que el objeto real para que el cliente no sea consciente de que está utilizando un Proxy en lugar del objeto real.
Sujeto Real (Real Subject): Es el objeto real que el Proxy representa. El Proxy delega las solicitudes del cliente al Sujeto Real cuando sea necesario.
Supongamos que tenemos una interfaz Image
para cargar y mostrar imágenes. Queremos crear un Proxy para controlar el acceso a la carga de imágenes y mostrar un mensaje cuando se carga una imagen.
from abc import ABC, abstractmethod
# Sujeto
class Image(ABC):
@abstractmethod
def display(self):
pass
# Sujeto Real
class RealImage(Image):
def __init__(self, filename):
self.filename = filename
self.load_from_disk()
def load_from_disk(self):
print("Loading", self.filename)
def display(self):
print("Displaying", self.filename)
# Proxy
class ProxyImage(Image):
def __init__(self, filename):
self.filename = filename
self.real_image = None
def display(self):
if self.real_image is None:
self.real_image = RealImage(self.filename)
self.real_image.display()
# Uso del patrón Proxy
image1 = ProxyImage("image1.jpg")
image1.display() # RealImage será creado y cargado desde el disco
image2 = ProxyImage("image2.jpg")
image2.display() # RealImage será creado y cargado desde el disco
image2.display() # No se volverá a cargar RealImage, ya que está en caché
Loading image1.jpg
Displaying image1.jpg
Loading image2.jpg
Displaying image2.jpg
Displaying image2.jpg
En este ejemplo:
Image
es la interfaz común para la carga y visualización de imágenes.RealImage
es el Sujeto Real que carga la imagen desde el disco.ProxyImage
es el Proxy que controla el acceso a la carga de imágenes. Cuando se llama al métododisplay()
, el Proxy carga la imagen solo cuando sea necesario.
Al ejecutar el código, se puede observar que el Proxy carga la imagen real solo cuando se llama al método display()
, lo que demuestra el control de acceso proporcionado por el patrón Proxy.
3.3.8. Otros patrones#
Módulo:
El patrón Módulo organiza un conjunto de clases y objetos relacionados en una estructura unificada y modular.
Composite-View:
El patrón Composite-View proporciona una forma de tratar las estructuras jerárquicas de objetos como objetos individuales.
Private Class Data:
El patrón Private Class Data controla el acceso a los datos de clase mediante la encapsulación de los datos dentro de un objeto de datos privado.