Patrón Visitante en Python
El patrón visitante es un patrón de diseño de comportamiento que te permite agregar nuevas operaciones a clases existentes sin modificar su estructura. En pocas palabras, el patrón visitante separa los algoritmos de la estructura de objetos sobre la que operan.
Idea central
Imagina que vas al hospital para un chequeo médico: tú (el visitado) permaneces quieto, mientras diferentes médicos (visitantes) se acercan a ti para examinarte. Cada médico se especializa en su área, pero todos examinan al mismo tú.
En programación, el patrón visitante funciona de manera similar:
- El elemento visitadoPermanece estable y sin cambios
- El visitantePuede agregar nuevas operaciones de forma flexible
- El elemento acepta la visita del visitante, permitiendo que el visitante realice operaciones sobre sí mismo.
Por qué se necesita el patrón visitante
Limitaciones del enfoque tradicional
Supongamos que tenemos un sistema de gráficos que contiene varias formas:
Ejemplo
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def perimeter(self):
return 2 * 3.14 * self.radius
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
Aquí viene el problema: si queremos agregar nuevas funcionalidades, como calcular el centro de gravedad de las figuras, exportar a formato SVG, etc., tendríamos que modificar cada clase de forma. Esto viola el principio abierto/cerrado (abierto a la extensión, cerrado a la modificación).
Ventajas del patrón visitante
El patrón visitante resuelve este problema de las siguientes maneras:
- Fácil de agregar nuevas operaciones: solo necesitas crear una nueva clase de visitante, sin modificar las clases de elementos existentes.
- Gestión centralizada de operaciones relacionadas: organiza las operaciones relacionadas en un mismo visitante.
- Estructura de elementos estable: las clases de elementos no necesitan modificarse con frecuencia.
Implementación del patrón visitante
Estructura básica
Entendamos la implementación del patrón visitante con un ejemplo concreto:
Ejemplo
# 1. Definir la interfaz de elementos
class ShapeElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 2. Definir la interfaz de visitante
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
# 3. Clases de elementos concretos
class Circle(ShapeElement):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Rectangle(ShapeElement):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
visitor.visit_rectangle(self)
# 4. Clases de visitantes concretos
class AreaCalculator(ShapeVisitor):
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
print(f"El área del círculo: {area:.2f}")
return area
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
print(f"El área del rectángulo: {area:.2f}")
return area
class PerimeterCalculator(ShapeVisitor):
def visit_circle(self, circle):
perimeter = 2 * 3.14 * circle.radius
print(f"El perímetro del círculo: {perimeter:.2f}")
return perimeter
def visit_rectangle(self, rectangle):
perimeter = 2 * (rectangle.width + rectangle.height)
print(f"El perímetro del rectángulo: {perimeter:.2f}")
return perimeter
Ejemplo de uso
Ejemplo
shapes = [
Circle(5),
Rectangle(4, 6),
Circle(3)
]
# Crear visitante
area_calculator = AreaCalculator()
perimeter_calculator = PerimeterCalculator()
print("=== Calcular área ===")
for shape in shapes:
shape.accept(area_calculator)
print("\n"=== Calcular perímetro ===")
for shape in shapes:
shape.accept(perimeter_calculator)
Resultado de salida:
=== 计算面积 === 圆的面积: 78.50 矩形的面积: 24.00 圆的面积: 28.26 === 计算周长 === 圆的周长: 31.40 矩形的周长: 20.00 圆的周长: 18.84
Componentes principales del patrón visitante
1. Visitor (interfaz de visitante)
Define los métodos de acceso para cada clase de elemento concreto.
Ejemplo
def visit_circle(self, circle): pass
def visit_rectangle(self, rectangle): pass
def visit_triangle(self, triangle): pass # Se puede ampliar fácilmente
2. ConcreteVisitor (visitante concreto)
Implementa las operaciones definidas en la interfaz del visitante.
Ejemplo
def visit_circle(self, circle):
return f'<circle r="{circle.radius}" />'
def visit_rectangle(self, rectangle):
return f'<rect width="{rectangle.width}" height="{rectangle.height}" />'
3. Element (interfaz de elemento)
Define el método que acepta al visitante.
Ejemplo
@abstractmethod
def accept(self, visitor):
pass
4. ConcreteElement (elemento concreto)
Implementa la interfaz de elemento, llamando al método correspondiente del visitante en el método accept.
Ejemplo
def accept(self, visitor):
visitor.visit_circle(self) # Aquí ocurre el doble despacho
Ejemplo de aplicación avanzada
Sistema complejo de procesamiento de documentos
Veamos un ejemplo más práctico: manejar diferentes tipos de elementos de documentos.
Ejemplo
# Interfaz de elemento de documento
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Elemento concreto de documento
class Paragraph(DocumentElement):
def __init__(self, text):
self.text = text
def accept(self, visitor):
return visitor.visit_paragraph(self)
class Heading(DocumentElement):
def __init__(self, text, level):
self.text = text
self.level = level
def accept(self, visitor):
return visitor.visit_heading(self)
class List(DocumentElement):
def __init__(self, items):
self.items = items
def accept(self, visitor):
return visitor.visit_list(self)
# Interfaz de visitante
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph): pass
@abstractmethod
def visit_heading(self, heading): pass
@abstractmethod
def visit_list(self, list_element): pass
# Visitante concreto: Exportación HTML
class HTMLExportVisitor(DocumentVisitor):
def visit_paragraph(self, paragraph):
return f"<p>{paragraph.text}</p>"
def visit_heading(self, heading):
return f"<h{heading.level}>{heading.text}</h{heading.level}>"
def visit_list(self, list_element):
items = "".join(f"<li>{item}</li>" for item in list_element.items)
return f"<ul>{items}</ul>"
# Visitante concreto: Conteo de palabras
class WordCountVisitor(DocumentVisitor):
def __init__(self):
self.total_words = 0
def visit_paragraph(self, paragraph):
words = len(paragraph.text.split())
self.total_words += words
return words
def visit_heading(self, heading):
words = len(heading.text.split())
self.total_words += words
return words
def visit_list(self, list_element):
words = sum(len(item.split()) for item in list_element.items)
self.total_words += words
return words
# Ejemplo de uso
document = [
Heading("Tutorial de Python", 1),
Paragraph("Python es un lenguaje de programación potente."),
List(["Elemento de lista 1", "Elemento de lista 2", "Elemento de lista 3"]),
Paragraph("Aprender Python es muy interesante.")
]
# Exportación HTML
html_visitor = HTMLExportVisitor()
print("=== Exportación HTML ===")
for element in document:
print(element.accept(html_visitor))
# Conteo de palabras
word_visitor = WordCountVisitor()
print("\n"=== Conteo de palabras ===")
for element in document:
words = element.accept(word_visitor)
print(f"{element.__class__.__name__}: {words} palabras")
print(f"Total de palabras: {word_visitor.total_words}")
Ventajas y desventajas del patrón visitante
Ventajas
- Principio abierto/cerrado: fácil de agregar nuevas operaciones sin modificar las clases existentes.
- Principio de responsabilidad única: concentra el comportamiento relacionado en una clase de visitante.
- Flexibilidad: puedes elegir diferentes visitantes en tiempo de ejecución.
- Separación de datos: separa los algoritmos de la estructura de datos.
Desventajas
- Difícil de cambiar la interfaz de elementos: agregar una nueva clase de elemento requiere modificar todos los visitantes.
- Puede romper la encapsulación: el visitante puede necesitar acceder a miembros privados del elemento.
- Complejidad: puede resultar demasiado complejo para estructuras de datos simples.
Escenarios de aplicación
Escenarios adecuados para usar el patrón visitante
- Estructura de objetos estable: se necesitan definir múltiples operaciones sobre una estructura de objetos relativamente estable.
- Operaciones que cambian con frecuencia: se necesitan agregar nuevas operaciones o algoritmos con frecuencia.
- Operaciones relacionadas centralizadas: se desea organizar las operaciones relacionadas juntas.
- Estructura de objetos compleja: la estructura de objetos contiene muchos tipos diferentes de objetos.
Casos de aplicación prácticos
- Compiladores: diversos análisis sobre el árbol sintáctico (verificación de tipos, optimización de código, etc.)
- Procesamiento de documentos: diferentes tratamientos de los elementos del documento (exportación, estadísticas, formato, etc.)
- Sistemas GUI: diferentes operaciones sobre componentes de interfaz (renderizado, manejo de eventos, etc.)
- Desarrollo de juegos: diferentes tratamientos de los objetos del juego (detección de colisiones, IA, etc.)
Mejores prácticas y consideraciones
1. Uso de doble despacho
El núcleo del patrón visitante es el doble despacho (double dispatch):
- Primer despacho: el elemento acepta al visitante.
- Segundo despacho: el visitante visita el elemento concreto.
Ejemplo
def accept(self, visitor):
visitor.visit_circle(self) # Aquí se determina el método de acceso específico
2. Manejo del estado del visitante
Si el visitante necesita mantener un estado:
Ejemplo
def __init__(self):
self.total_area = 0
self.shape_count = 0
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
self.total_area += area
self.shape_count += 1
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
self.total_area += area
self.shape_count += 1
def get_statistics(self):
return {
'total_area': self.total_area,
'shape_count': self.shape_count,
'average_area': self.total_area / self.shape_count if self.shape_count > 0 else 0
}
3. Manejo de excepciones durante la visita
Ejemplo
def visit_circle(self, circle):
try:
# Ejecutar la operación
pass
except Exception as e:
print(f"Error al procesar el círculo: {e}")
def visit_rectangle(self, rectangle):
try:
# Ejecutar la operación
pass
except Exception as e:
print(f"Error al procesar el rectángulo: {e}")
Resumen
El patrón visitante es un potente patrón de diseño que proporciona una gran extensibilidad al separar los algoritmos de la estructura de datos. Aunque tiene cierta complejidad, en escenarios adecuados puede mejorar notablemente la mantenibilidad y extensibilidad del código.
Puntos clave:
- El patrón Visitor es adecuado para escenarios donde la estructura de objetos es estable pero las operaciones cambian con frecuencia
- Se implementa el comportamiento polimórfico mediante el mecanismo de doble despacho
- Es fácil añadir nuevas operaciones, pero difícil añadir nuevos tipos de elementos
- En proyectos reales, hay que sopesar su complejidad y los beneficios que aporta