Skip to content

Instantly share code, notes, and snippets.

@leonardopsantos
Created February 13, 2023 21:55
Show Gist options
  • Save leonardopsantos/bc83b163e4afc74621b9220e0e756766 to your computer and use it in GitHub Desktop.
Save leonardopsantos/bc83b163e4afc74621b9220e0e756766 to your computer and use it in GitHub Desktop.
Python example demonstrating the Visitor pattern in an extensible way
import enum
from dataclasses import dataclass, asdict, field
from abc import ABCMeta, abstractmethod
from typing import Tuple
import jinja2
from pprint import pprint
class Gender(enum.Enum):
MALE = enum.auto()
FEMALE = enum.auto()
NONBINARY = enum.auto()
@dataclass
class PersonData:
name: str
age: int
gender: Gender
pronouns: Tuple[str, str] = field(init=False)
def __post_init__(self):
d = {
Gender.MALE : ("He", "Him"),
Gender.FEMALE : ("She", "Her"),
Gender.NONBINARY : ("They", "Them")
}
self.pronouns = d[self.gender]
def accept(self, v:"RendererBasic") -> str:
return v.render_person(self)
@dataclass
class TeacherData(PersonData):
course: str
def accept(self, v:"RendererBasic") -> str:
return v.render_teacher(self)
@dataclass
class StudentData(PersonData):
year: int
def accept(self, v:"RendererBasic") -> str:
return v.render_student(self)
class RendererBase(metaclass=ABCMeta):
@abstractmethod
def render_person(self, person:PersonData) -> str:
pass
@abstractmethod
def render_teacher(self, teacher:TeacherData) -> str:
pass
@abstractmethod
def render_student(self, student:StudentData) -> str:
pass
class RendererBasic(RendererBase):
""" Basic visitor pattern implementation
"""
def __init__(self):
self._person_template = jinja2.Template("""\
Basic Person Rendering:
Name: {{name}},
Age: {{age}}
""")
self._teacher_template = jinja2.Template("""\
Basic Teacher Rendering:
Name: {{name}},
Age: {{age}}
Course: {{course}}
""")
self._student_template = jinja2.Template("""\
Basic Student Rendering:
Name: {{name}},
Age: {{age}}
School Year: {{year}}
""")
def render_person(self, person) -> str:
return self._person_template.render(**asdict(person))
def render_teacher(self, teacher) -> str:
return self._teacher_template.render(**asdict(teacher))
def render_student(self, student) -> str:
return self._student_template.render(**asdict(student))
class RendererModuleBase(metaclass=ABCMeta):
@abstractmethod
def __init__(self):
pass
@abstractmethod
def render(self, data:PersonData) -> str:
pass
# In C++ we'd use templates for the different renderers, which is way cooler.
class RendererPluggable(RendererBase):
""" Modular visitor pattern implementation for extra OOP points.
"""
def __init__(self, person:RendererModuleBase, teacher:RendererModuleBase, student:RendererModuleBase):
self._person = person
self._teacher = teacher
self._student = student
def render_person(self, person):
return self._person.render(person)
def render_teacher(self, teacher):
return self._teacher.render(teacher)
def render_student(self, student):
return self._student.render(student)
class RendererModulePerson(RendererModuleBase):
def __init__(self):
self._template = jinja2.Template("""\
Modular Person Rendering:
Name: {{name}},
Age: {{age}}
""")
def render(self, person:PersonData) -> str:
return self._template.render(**asdict(person))
class RendererModuleTeacher(RendererModulePerson):
def __init__(self):
self._template = jinja2.Template("""\
Modular Teacher Rendering:
Name: {{name}},
Age: {{age}}
Course: {{course}}
""")
class RendererModuleStudent(RendererModulePerson):
def __init__(self):
self._template = jinja2.Template("""\
Modular Student Rendering:
Name: {{name}},
Age: {{age}}
School Year: {{year}}
""")
class RendererModuleTeacherNew(RendererModuleTeacher):
"""
Specialized TeacherData visitor. Dynamically determines the honorific title
to use based on the person's gender.
"""
def __init__(self):
# Shows that we can use templeate features such as conditionals to
# simplify the code.
self._template = jinja2.Template("""\
{{honorific}} {{name}} is a {{course | lower}} teacher. {%- if gender != Gender.FEMALE -%} {{pronouns[0]}} is {{age}} years old.{%- endif -%}
""")
# Need to add so the template knows about the Enum
self._template.globals["Gender"] = Gender
self._honorific = {
Gender.MALE : "Mr.",
Gender.FEMALE : "Ms.",
Gender.NONBINARY : "Mx.",
}
def render(self, teacher:TeacherData) -> str:
return self._template.render(
honorific=self._honorific[teacher.gender],
**asdict(teacher)
)
#
# RendererBasic example
#
def render_basic():
bob = PersonData("Bob", 42, Gender.NONBINARY)
alice = TeacherData("Alice", 31, Gender.FEMALE, "English")
timmy = StudentData("Timmy", 9, Gender.MALE, 3)
r = RendererBasic()
for p in [bob, alice, timmy]:
print(p.accept(r))
#
# RendererPluggable example
#
def render_pluggable():
bob = PersonData("Bob", 42, Gender.NONBINARY)
alice = TeacherData("Alice", 31, Gender.FEMALE, "English")
timmy = StudentData("Timmy", 9, Gender.MALE, 3)
r = RendererPluggable(
RendererModulePerson(),
RendererModuleTeacher(),
RendererModuleStudent()
)
for p in [bob, alice, timmy]:
print(p.accept(r))
#
# RendererPluggable example with specialized renderer
#
def render_pluggable_new():
zaphod = PersonData("Zaphod", 42, Gender.NONBINARY)
bob = TeacherData("Bob", 29, Gender.MALE, "Math")
alice = TeacherData("Alice", 36, Gender.FEMALE, "English")
timmy = StudentData("Timmy", 9, Gender.MALE, 3)
r = RendererPluggable(
RendererModulePerson(),
RendererModuleTeacherNew(),
RendererModuleStudent()
)
for p in [zaphod, bob, alice, timmy]:
print(p.accept(r))
if __name__ == "__main__":
render_basic()
render_pluggable()
render_pluggable_new()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment