Операции над объектами без изменения классов. Double dispatch через singledispatch. Pattern matching Python 3.10+.
Visitor добавляет операции без изменения классов. Double Dispatch выбирает метод по типу получателя и аргумента.
Visitor — поведенческий паттерн, позволяющий добавлять операции к объектам без изменения их классов через двойную диспетчеризацию.
# ❌ ПЛОХО: Нужно менять каждый класс для новой операции
class Circle:
def __init__(self, radius: float):
self.radius = radius
def draw(self):
print(f"Drawing circle r={self.radius}")
class Square:
def __init__(self, side: float):
self.side = side
def draw(self):
print(f"Drawing square side={self.side}")
# Нужно добавить export to XML — меняем все классы!
class Circle:
# ... предыдущий код ...
def to_xml(self) -> str:
return f"<circle radius='{self.radius}'/>"
class Square:
# ... предыдущий код ...
def to_xml(self) -> str:
return f"<square side='{self.side}'/>"
# Теперь нужно добавить area() — снова меняем все классы!# ✅ ХОРОШО: Операции в отдельных классах-посетителях
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def accept(self, visitor: "ShapeVisitor"):
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def accept(self, visitor: "ShapeVisitor"):
visitor.visit_circle(self)
def draw(self):
print(f"Drawing circle r={self.radius}")
class Square(Shape):
def __init__(self, side: float):
self.side = side
def accept(self, visitor: "ShapeVisitor"):
visitor.visit_square(self)
def draw(self):
print(f"Drawing square side={self.side}")
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle: Circle):
pass
@abstractmethod
def visit_square(self, square: Square):
pass
class DrawVisitor(ShapeVisitor):
def visit_circle(self, circle: Circle):
circle.draw()
def visit_square(self, square: Square):
square.draw()
class XMLExportVisitor(ShapeVisitor):
def __init__(self):
self.xml_parts = []
def visit_circle(self, circle: Circle):
self.xml_parts.append(f"<circle radius='{circle.radius}'/>")
def visit_square(self, square: Square):
self.xml_parts.append(f"<square side='{square.side}'/>")
def get_xml(self) -> str:
return "".join(self.xml_parts)
class AreaVisitor(ShapeVisitor):
def __init__(self):
self.total_area = 0
def visit_circle(self, circle: Circle):
area = 3.14159 * circle.radius ** 2
print(f"Circle area: {area:.2f}")
self.total_area += area
def visit_square(self, square: Square):
area = square.side ** 2
print(f"Square area: {area:.2f}")
self.total_area += area
# Использование
shapes = [Circle(5), Square(4), Circle(3)]
# Рисование
draw_visitor = DrawVisitor()
for shape in shapes:
shape.accept(draw_visitor)
# Экспорт в XML
xml_visitor = XMLExportVisitor()
for shape in shapes:
shape.accept(xml_visitor)
print(xml_visitor.get_xml())
# <circle radius='5'/><square side='4'/><circle radius='3'/>
# Площадь
area_visitor = AreaVisitor()
for shape in shapes:
shape.accept(area_visitor)
print(f"Total area: {area_visitor.total_area:.2f}")# ❌ ПЛОХО: Выбор метода только по типу получателя
class Collision:
def collide(self, other: "GameObject"):
# Не знаем тип other statically
pass
class Player(Collision):
def collide(self, other: "GameObject"):
# Приходится проверять тип в runtime
if isinstance(other, Enemy):
self._collide_with_enemy(other)
elif isinstance(other, Wall):
self._collide_with_wall(other)
def _collide_with_enemy(self, enemy):
print("Player hits Enemy")
def _collide_with_wall(self, wall):
print("Player hits Wall")
# Проблема: логика столкновений размазана# ✅ ХОРОШО: Выбор по типу обоих объектов
from abc import ABC, abstractmethod
class GameObject(ABC):
@abstractmethod
def collide(self, other: "GameObject"):
pass
@abstractmethod
def collide_with_player(self, player: "Player"):
pass
@abstractmethod
def collide_with_enemy(self, enemy: "Enemy"):
pass
@abstractmethod
def collide_with_wall(self, wall: "Wall"):
pass
class Player(GameObject):
def collide(self, other: GameObject):
# Первый dispatch: по типу other
other.collide_with_player(self)
def collide_with_player(self, other: "Player"):
print("Player hits Player")
def collide_with_enemy(self, enemy: "Enemy"):
print("Player hits Enemy")
def collide_with_wall(self, wall: "Wall"):
print("Player hits Wall")
class Enemy(GameObject):
def collide(self, other: GameObject):
other.collide_with_enemy(self)
def collide_with_player(self, player: "Player"):
print("Enemy hits Player")
def collide_with_enemy(self, other: "Enemy"):
print("Enemy hits Enemy")
def collide_with_wall(self, wall: "Wall"):
print("Enemy hits Wall")
class Wall(GameObject):
def collide(self, other: GameObject):
other.collide_with_wall(self)
def collide_with_player(self, player: "Player"):
print("Wall hits Player")
def collide_with_enemy(self, enemy: "Enemy"):
print("Wall hits Enemy")
def collide_with_wall(self, other: "Wall"):
print("Wall hits Wall")
# Использование
player = Player()
enemy = Enemy()
wall = Wall()
player.collide(enemy) # Player hits Enemy
enemy.collide(player) # Enemy hits Player
player.collide(wall) # Player hits Wall# ✅ singledispatch для функций
from functools import singledispatch
@singledispatch
def process(value):
"""Базовая реализация"""
print(f"Processing generic: {value}")
@process.register(int)
def _(value: int):
print(f"Processing int: {value * 2}")
@process.register(str)
def _(value: str):
print(f"Processing str: {value.upper()}")
@process.register(list)
def _(value: list):
print(f"Processing list: {len(value)} items")
# Использование
process(42) # Processing int: 84
process("hello") # Processing str: HELLO
process([1, 2, 3]) # Processing list: 3 items
process(3.14) # Processing generic: 3.14# ✅ singledispatchmethod для методов
from functools import singledispatchmethod
class Processor:
@singledispatchmethod
def process(self, value):
return f"Generic: {value}"
@process.register
def _(self, value: int):
return f"Integer: {value * 2}"
@process.register
def _(self, value: str):
return f"String: {value.upper()}"
# Использование
p = Processor()
print(p.process(42)) # Integer: 84
print(p.process("hi")) # String: HI
print(p.process(3.14)) # Generic: 3.14# ✅ Pattern matching как альтернатива Visitor
from abc import ABC
class Shape(ABC):
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
class Square(Shape):
def __init__(self, side: float):
self.side = side
def draw(shape: Shape):
match shape:
case Circle(radius=r):
print(f"Drawing circle r={r}")
case Square(side=s):
print(f"Drawing square side={s}")
case _:
print("Unknown shape")
def area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r ** 2
case Square(side=s):
return s ** 2
case _:
return 0
# Использование
shapes = [Circle(5), Square(4)]
for shape in shapes:
draw(shape)
total = sum(area(s) for s in shapes)
print(f"Total area: {total:.2f}")# ✅ Visitor для AST
from abc import ABC, abstractmethod
class ASTNode(ABC):
@abstractmethod
def accept(self, visitor: "ASTVisitor"):
pass
class NumberNode(ASTNode):
def __init__(self, value: float):
self.value = value
def accept(self, visitor: "ASTVisitor"):
visitor.visit_number(self)
class AddNode(ASTNode):
def __init__(self, left: ASTNode, right: ASTNode):
self.left = left
self.right = right
def accept(self, visitor: "ASTVisitor"):
visitor.visit_add(self)
class ASTVisitor(ABC):
@abstractmethod
def visit_number(self, node: NumberNode):
pass
@abstractmethod
def visit_add(self, node: AddNode):
pass
class EvalVisitor(ASTVisitor):
def visit_number(self, node: NumberNode):
return node.value
def visit_add(self, node: AddNode):
node.left.accept(self)
node.right.accept(self)
return self.left_result + self.right_result
# Упрощённая версия
def evaluate(self, node: ASTNode) -> float:
if isinstance(node, NumberNode):
return node.value
elif isinstance(node, AddNode):
return self.evaluate(node.left) + self.evaluate(node.right)
# Использование: 2 + 3
ast = AddNode(NumberNode(2), NumberNode(3))
visitor = EvalVisitor()
print(visitor.evaluate(ast)) # 5# ✅ Visitor для отчётов
from abc import ABC, abstractmethod
class Employee(ABC):
@abstractmethod
def accept(self, visitor: "EmployeeVisitor"):
pass
class Developer(Employee):
def __init__(self, name: str, lines: int):
self.name = name
self.lines = lines
def accept(self, visitor: "EmployeeVisitor"):
visitor.visit_developer(self)
class Manager(Employee):
def __init__(self, name: str, team_size: int):
self.name = name
self.team_size = team_size
def accept(self, visitor: "EmployeeVisitor"):
visitor.visit_manager(self)
class EmployeeVisitor(ABC):
@abstractmethod
def visit_developer(self, dev: Developer):
pass
@abstractmethod
def visit_manager(self, mgr: Manager):
pass
class SalaryVisitor(EmployeeVisitor):
def __init__(self):
self.total = 0
def visit_developer(self, dev: Developer):
salary = dev.lines * 0.1
print(f"{dev.name}: ${salary}")
self.total += salary
def visit_manager(self, mgr: Manager):
salary = mgr.team_size * 1000
print(f"{mgr.name}: ${salary}")
self.total += salary
# Использование
employees = [
Developer("Alice", 5000),
Manager("Bob", 5),
Developer("Charlie", 7000),
]
visitor = SalaryVisitor()
for emp in employees:
emp.accept(visitor)
print(f"Total salary: ${visitor.total}")| Паттерн | Когда использовать | Pythonic-реализация |
|---|---|---|
| Visitor | Много операций над объектами без изменения классов | accept() + visit_*() |
| Double Dispatch | Столкновения, взаимодействия двух объектов | Методы collide_with_*() |
| singledispatch | Функции с поведением по типу | @singledispatch |
| Pattern matching | Python 3.10+, альтернатива Visitor | match/case |
Главный принцип: Visitor добавляет операции через отдельные классы, не меняя объекты.
Изучите тему Repository и Unit of Work для абстракции доступа к данным.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.