Created
February 13, 2023 21:55
-
-
Save leonardopsantos/bc83b163e4afc74621b9220e0e756766 to your computer and use it in GitHub Desktop.
Python example demonstrating the Visitor pattern in an extensible way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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