Great Resources
- Game Programming Patterns β Event Queue β one of the clearest real-world pattern walkthroughs out there; the whole site is worth reading
- Fintech Engineering Handbook
Gang of Four Design Patterns
LLMs were used to create this summary
A Python-centric, expert-level guide to the 23 Gang of Four (GoF) design patterns. This document explains the patterns, gives short analyses, and provides compact Python examples. Code examples are illustrative, idiomatic, and formatted as fenced code blocks for readability.
Introduction
The sections below present each pattern with:
- Purpose / When to use
- Key components (concise)
- A short Python example
Part I β Creational Patterns
Creational patterns control object creation to promote loose coupling and flexibility.
1. Singleton
Purpose: Ensure a class has only one instance and provide a global access point.
Key points:
- Use for centralized services (config, logging).
- Be cautious: can become a hidden global and hurt testability.
Example (Python):
class Logger:
_instance = None
def __init__(self):
"""
The constructor is private. It is not intended for external use.
"""
raise RuntimeError("Call get_instance() instead")
@classmethod
def get_instance(cls):
if cls._instance is None:
# Lazy initialization for the first time it's accessed
cls._instance = cls.__new__(cls)
print("Logger instance created.")
return cls._instance
def log(self, message):
print(f"Log: {message}")
# Usage
logger1 = Logger.get_instance()
logger2 = Logger.get_instance()
print(f"Are logger1 and logger2 the same instance? {logger1 is logger2}")
logger1.log("This is the first log message.")
logger2.log("This is the second log message.")Pythonβs module system naturally provides singleton-like behavior. Explicit Singleton classes are almost never needed. https://www.pixelstech.net/article/1754987478-please-stop-asking-about-singleton-in-python
2. Factory Method
Purpose: Define an interface for creating an object, letting subclasses decide which class to instantiate.
Example:
from abc import ABC, abstractmethod
# Product Interface
class Vehicle(ABC):
@abstractmethod
def get_type(self):
pass
# Concrete Products
class Car(Vehicle):
def get_type(self):
return "Car"
class Truck(Vehicle):
def get_type(self):
return "Truck"
# Creator Interface
class VehicleFactory(ABC):
@abstractmethod
def create_vehicle(self) -> Vehicle:
pass
def get_vehicle_info(self):
vehicle = self.create_vehicle()
return f"A new {vehicle.get_type()} has been created."
# Concrete Creators
class CarFactory(VehicleFactory):
def create_vehicle(self):
return Car()
class TruckFactory(VehicleFactory):
def create_vehicle(self):
return Truck()
# Usage
car_factory = CarFactory()
print(car_factory.get_vehicle_info())
truck_factory = TruckFactory()
print(truck_factory.get_vehicle_info())3. Abstract Factory
Purpose: Create families of related objects without specifying concrete classes.
Example:
from abc import ABC, abstractmethod
# Abstract Products
class Button(ABC):
@abstractmethod
def get_style(self):
pass
class Checkbox(ABC):
@abstractmethod
def get_style(self):
pass
# Concrete Products
class WindowsButton(Button):
def get_style(self):
return "Windows-style button"
class MacOSButton(Button):
def get_style(self):
return "MacOS-style button"
class WindowsCheckbox(Checkbox):
def get_style(self):
return "Windows-style checkbox"
class MacOSCheckbox(Checkbox):
def get_style(self):
return "MacOS-style checkbox"
# Abstract Factory
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete Factories
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_checkbox(self):
return WindowsCheckbox()
class MacOSFactory(GUIFactory):
def create_button(self):
return MacOSButton()
def create_checkbox(self):
return MacOSCheckbox()
# Client Code
def create_ui(factory: GUIFactory):
button = factory.create_button()
checkbox = factory.create_checkbox()
print(f"Created UI with a {button.get_style()} and a {checkbox.get_style()}.")
# Usage
windows_ui = WindowsFactory()
create_ui(windows_ui)
macos_ui = MacOSFactory()
create_ui(macos_ui)4. Builder
Purpose: Separate the construction of a complex object from its representation.
Example:
from abc import ABC, abstractmethod
# Product
class Report:
def __init__(self):
self.sections = []
def add_section(self, section):
self.sections.append(section)
def display(self):
print("--- Report ---")
for section in self.sections:
print(section)
print("--------------")
# Builder Interface
class ReportBuilder(ABC):
@abstractmethod
def build_header(self):
pass
@abstractmethod
def build_body(self):
pass
@abstractmethod
def build_footer(self):
pass
@abstractmethod
def get_report(self) -> Report:
pass
# Concrete Builders
class HTMLReportBuilder(ReportBuilder):
def __init__(self):
self.report = Report()
def build_header(self):
self.report.add_section("<h1>HTML Report Header</h1>")
def build_body(self):
self.report.add_section("<p>HTML Report Body</p>")
def build_footer(self):
self.report.add_section("<footer>HTML Report Footer</footer>")
def get_report(self):
return self.report
class PDFReportBuilder(ReportBuilder):
def __init__(self):
self.report = Report()
def build_header(self):
self.report.add_section("PDF Report Header")
def build_body(self):
self.report.add_section("PDF Report Body")
def build_footer(self):
self.report.add_section("PDF Report Footer")
def get_report(self):
return self.report
# Director
class ReportDirector:
def __init__(self, builder: ReportBuilder):
self.builder = builder
def construct_report(self):
self.builder.build_header()
self.builder.build_body()
self.builder.build_footer()
# Usage
html_builder = HTMLReportBuilder()
director = ReportDirector(html_builder)
director.construct_report()
html_report = html_builder.get_report()
html_report.display()
pdf_builder = PDFReportBuilder()
director = ReportDirector(pdf_builder)
director.construct_report()
pdf_report = pdf_builder.get_report()
pdf_report.display()5. Prototype
Purpose: Create objects by copying an existing prototype.
Example:
import copy
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
def clone(self):
return copy.deepcopy(self)
class Circle(Shape):
def __init__(self, x, y, radius):
super().__init__(x, y)
self.radius = radius
def __str__(self):
return f"Circle at ({self.x}, {self.y}) with radius {self.radius}"
# Client Code
if __name__ == "__main__":
prototype_circle = Circle(10, 20, 5)
print(f"Original: {prototype_circle}")
cloned_circle = prototype_circle.clone()
cloned_circle.x = 30
cloned_circle.y = 40
print(f"Cloned and modified: {cloned_circle}")
print(f"Original after clone: {prototype_circle}")
print(f"Are they the same object? {prototype_circle is cloned_circle}")Part II β Structural Patterns
Structural patterns describe object composition and simplify interfaces.
6. Adapter
Purpose: Let incompatible interfaces work together by adapting one to the other.
Example:
# Target Interface
class NewPaymentProcessor:
def charge(self, amount):
raise NotImplementedError
# Adaptee (Legacy System)
class LegacyPaymentGateway:
def process_payment(self, amount):
print(f"Processing payment of ${amount} through legacy gateway.")
# Adapter
class PaymentAdapter(NewPaymentProcessor):
def __init__(self, legacy_gateway):
self.legacy_gateway = legacy_gateway
def charge(self, amount):
print("Adapter: Converting charge to process_payment...")
self.legacy_gateway.process_payment(amount)
# Client Code
def process_client_payment(processor: NewPaymentProcessor, amount):
print("Client: Sending request to processor.")
processor.charge(amount)
# Usage
legacy_gateway = LegacyPaymentGateway()
payment_adapter = PaymentAdapter(legacy_gateway)
process_client_payment(payment_adapter, 100)7. Bridge
Purpose: Decouple an abstraction from its implementation so they can vary independently.
Example:
from abc import ABC, abstractmethod
# Implementer Interface
class Workshop(ABC):
@abstractmethod
def work(self):
pass
# Concrete Implementers
class Produce(Workshop):
def work(self):
print("Produced", end="")
class Assemble(Workshop):
def work(self):
print(" And Assembled.")
# Abstraction
class Vehicle(ABC):
def __init__(self, workshop1: Workshop, workshop2: Workshop):
self.workshop1 = workshop1
self.workshop2 = workshop2
@abstractmethod
def manufacture(self):
pass
# Refined Abstractions
class Car(Vehicle):
def __init__(self, workshop1: Workshop, workshop2: Workshop):
super().__init__(workshop1, workshop2)
def manufacture(self):
print("Car ", end="")
self.workshop1.work()
self.workshop2.work()
class Bike(Vehicle):
def __init__(self, workshop1: Workshop, workshop2: Workshop):
super().__init__(workshop1, workshop2)
def manufacture(self):
print("Bike ", end="")
self.workshop1.work()
self.workshop2.work()
# Usage
car = Car(Produce(), Assemble())
car.manufacture()
bike = Bike(Produce(), Assemble())
bike.manufacture()8. Composite
Purpose: Compose objects into tree structures and treat them uniformly.
Example:
from abc import ABC, abstractmethod
# Component Interface
class Task(ABC):
@abstractmethod
def display(self):
pass
# Leaf
class SimpleTask(Task):
def __init__(self, title):
self.title = title
def display(self):
print(f"Simple Task: {self.title}")
# Composite
class TaskList(Task):
def __init__(self, title):
self.title = title
self.tasks = []
def add_task(self, task):
self.tasks.append(task)
def remove_task(self, task):
self.tasks.remove(task)
def display(self):
print(f"Task List: {self.title}")
for task in self.tasks:
task.display()
# Client Code
if __name__ == "__main__":
# Creating simple tasks
simple_task1 = SimpleTask("Complete Coding")
simple_task2 = SimpleTask("Write Documentation")
# Creating a task list
project_tasks = TaskList("Project Tasks")
project_tasks.add_task(simple_task1)
project_tasks.add_task(simple_task2)
# Nested task list
phase1_tasks = TaskList("Phase 1 Tasks")
phase1_tasks.add_task(SimpleTask("Design"))
phase1_tasks.add_task(SimpleTask("Implementation"))
project_tasks.add_task(phase1_tasks)
# Displaying tasks
project_tasks.display()9. Decorator
Purpose: Add responsibilities to objects dynamically.
Example:
from abc import ABC, abstractmethod
# Component Interface
class Coffee(ABC):
@abstractmethod
def get_description(self):
pass
@abstractmethod
def get_cost(self):
pass
# Concrete Component
class PlainCoffee(Coffee):
def get_description(self):
return "Plain Coffee"
def get_cost(self):
return 2.0
# Decorator
class CoffeeDecorator(Coffee):
def __init__(self, decorated_coffee: Coffee):
self.decorated_coffee = decorated_coffee
def get_description(self):
return self.decorated_coffee.get_description()
def get_cost(self):
return self.decorated_coffee.get_cost()
# Concrete Decorators
class MilkDecorator(CoffeeDecorator):
def get_description(self):
return self.decorated_coffee.get_description() + ", Milk"
def get_cost(self):
return self.decorated_coffee.get_cost() + 0.5
class SugarDecorator(CoffeeDecorator):
def get_description(self):
return self.decorated_coffee.get_description() + ", Sugar"
def get_cost(self):
return self.decorated_coffee.get_cost() + 0.2
# Usage
plain_coffee = PlainCoffee()
print(f"Description: {plain_coffee.get_description()}, Cost: ${plain_coffee.get_cost()}")
coffee_with_milk = MilkDecorator(PlainCoffee())
print(f"Description: {coffee_with_milk.get_description()}, Cost: ${coffee_with_milk.get_cost()}")
coffee_with_sugar_and_milk = SugarDecorator(MilkDecorator(PlainCoffee()))
print(f"Description: {coffee_with_sugar_and_milk.get_description()}, Cost: ${coffee_with_sugar_and_milk.get_cost()}")10. Facade
Purpose: Provide a unified, simple interface to a complex subsystem.
Example:
class CPU:
def process_data(self):
print("CPU: Processing data...")
class Memory:
def load(self):
print("Memory: Loading data...")
class HardDrive:
def read_data(self):
print("HardDrive: Reading boot data...")
# The Facade
class ComputerFacade:
def __init__(self):
self.cpu = CPU()
self.memory = Memory()
self.hard_drive = HardDrive()
def start_computer(self):
print("Starting computer...")
self.hard_drive.read_data()
self.memory.load()
self.cpu.process_data()
print("Computer started.")
# Client Code
if __name__ == "__main__":
computer = ComputerFacade()
computer.start_computer()11. Flyweight
Purpose: Share intrinsic state to reduce memory usage when many similar objects exist.
Example:
class CharacterFlyweight:
def __init__(self, font_name, font_size):
self.font_name = font_name
self.font_size = font_size
def display(self, x, y, color):
print(f"Displaying character with {self.font_name}, "
f"size {self.font_size}, at ({x}, {y}) in color {color}.")
class CharacterFactory:
_character_pool = {}
def get_character(self, font_name, font_size):
key = (font_name, font_size)
if key not in self._character_pool:
self._character_pool[key] = CharacterFlyweight(font_name, font_size)
print(f"Created new flyweight for font: {key}")
return self._character_pool[key]
# Client Code
if __name__ == "__main__":
factory = CharacterFactory()
# Create two characters that share the same intrinsic state
char_A = factory.get_character("Arial", 12)
char_B = factory.get_character("Arial", 12)
char_A.display(10, 10, "black")
char_B.display(20, 10, "red")
print(f"Are char_A and char_B the same object? {char_A is char_B}")12. Proxy
Purpose: Provide a surrogate that controls access to another object.
Example:
from abc import ABC, abstractmethod
# Subject Interface
class Image(ABC):
@abstractmethod
def display(self):
pass
# Real Subject
class RealImage(Image):
def __init__(self, filename):
self.filename = filename
self._load_image_from_disk()
def _load_image_from_disk(self):
print(f"Loading image from disk: {self.filename}")
def display(self):
print(f"Displaying image: {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:
print("Proxy: Real image not loaded. Loading now...")
self.real_image = RealImage(self.filename)
self.real_image.display()
# Client Code
if __name__ == "__main__":
image = ProxyImage("large_image.jpg")
print("Client: Initialized proxy. The image is not yet loaded.")
# First call will load the image
image.display()
print("\n")
# Subsequent calls will use the cached image
image.display()Part III β Behavioral Patterns
Behavioral patterns define object interaction and responsibility assignment.
13. Chain of Responsibility
Purpose: Pass a request along a chain of handlers until one handles it.
Example:
from abc import ABC, abstractmethod
class Handler(ABC):
def __init__(self, next_handler=None):
self._next_handler = next_handler
def set_next(self, handler):
self._next_handler = handler
return handler
@abstractmethod
def handle(self, request):
if self._next_handler:
return self._next_handler.handle(request)
return None
class InfoHandler(Handler):
def handle(self, request):
if request == "info":
return "Info handled by InfoHandler"
else:
return super().handle(request)
class WarningHandler(Handler):
def handle(self, request):
if request == "warning":
return "Warning handled by WarningHandler"
else:
return super().handle(request)
class ErrorHandler(Handler):
def handle(self, request):
if request == "error":
return "Error handled by ErrorHandler"
else:
return super().handle(request)
# Usage
info_handler = InfoHandler()
warning_handler = WarningHandler()
error_handler = ErrorHandler()
info_handler.set_next(warning_handler).set_next(error_handler)
print(info_handler.handle("warning"))
print(info_handler.handle("error"))
print(info_handler.handle("info"))
print(info_handler.handle("unknown"))14. Command
Purpose: Encapsulate a request as an object; enable undo/redo and queuing.
Example:
from abc import ABC, abstractmethod
# Receiver
class Light:
def turn_on(self):
print("Light is ON")
def turn_off(self):
print("Light is OFF")
# Command Interface
class Command(ABC):
@abstractmethod
def execute(self):
pass
# Concrete Commands
class TurnOnLightCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_on()
class TurnOffLightCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_off()
# Invoker
class RemoteControl:
def __init__(self):
self.command = None
def set_command(self, command):
self.command = command
def press_button(self):
if self.command:
self.command.execute()
# Usage
light = Light()
turn_on = TurnOnLightCommand(light)
turn_off = TurnOffLightCommand(light)
remote = RemoteControl()
remote.set_command(turn_on)
remote.press_button()
remote.set_command(turn_off)
remote.press_button()15. Interpreter
Purpose: Define a representation for a language and interpret sentences in that language.
Example:
class Context:
def __init__(self, expression):
self.expression = expression
self.result = None
# Abstract Expression
class Expression:
def interpret(self, context):
pass
# Terminal Expressions
class NumberExpression(Expression):
def __init__(self, value):
self.value = value
def interpret(self, context):
return self.value
# Non-terminal Expressions
class AdditionExpression(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def interpret(self, context):
return self.left.interpret(context) + self.right.interpret(context)
class MultiplicationExpression(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def interpret(self, context):
return self.left.interpret(context) * self.right.interpret(context)
# Client Code (simplified)
if __name__ == "__main__":
# Represents "2 + 3 * 4"
expression = AdditionExpression(
NumberExpression(2),
MultiplicationExpression(
NumberExpression(3),
NumberExpression(4)
)
)
result = expression.interpret(Context(None))
print(f"The result of '2 + 3 * 4' is: {result}")16. Iterator
Purpose: Provide sequential access to elements without exposing the underlying representation. In Python, use iter/next.
Example:
class MyIterator:
def __init__(self, data): self.data = data; self.i = 0
def __next__(self):
if self.i >= len(self.data): raise StopIteration
v = self.data[self.i]; self.i += 1; return v
class MyCollection:
def __init__(self, data): self._data = data
def __iter__(self): return MyIterator(self._data)
for x in MyCollection([1,2,3]): print(x)17. Mediator
Purpose: Centralize communication between related objects.
Example:
class Tower:
def request_takeoff(self, plane): print(f"Clearance for {plane}")
class Plane:
def __init__(self, tower, name): self.tower = tower; self.name = name
def request(self): self.tower.request_takeoff(self.name)
t = Tower(); p = Plane(t, 'A1'); p.request()18. Memento
Purpose: Capture an objectβs internal state without breaking encapsulation.
Example:
class Memento:
def __init__(self, state): self._state = state
def state(self): return self._state
class Document:
def __init__(self, text=''): self._text = text
def write(self, t): self._text += t
def create_memento(self): return Memento(self._text)
def restore(self, m): self._text = m.state()
doc = Document('A')
snap = doc.create_memento()
doc.write('B')
doc.restore(snap)19. Observer
Purpose: Define a one-to-many dependency so observers are notified of state changes.
Example:
from abc import ABC, abstractmethod
# Subject Interface
class Subject(ABC):
@abstractmethod
def register_observer(self, observer):
pass
@abstractmethod
def remove_observer(self, observer):
pass
@abstractmethod
def notify_observers(self):
pass
# Concrete Subject
class WeatherStation(Subject):
def __init__(self):
self._observers = []
self._temperature = 0
def register_observer(self, observer):
self._observers.append(observer)
def remove_observer(self, observer):
self._observers.remove(observer)
def notify_observers(self):
for observer in self._observers:
observer.update(self._temperature)
def set_temperature(self, temp):
print(f"WeatherStation: New temperature is {temp}")
self._temperature = temp
self.notify_observers()
# Observer Interface
class Observer(ABC):
@abstractmethod
def update(self, temperature):
pass
# Concrete Observers
class PhoneDisplay(Observer):
def update(self, temperature):
print(f"PhoneDisplay: Temperature is now {temperature} degrees.")
class TVDisplay(Observer):
def update(self, temperature):
print(f"TVDisplay: Current temperature is {temperature} degrees.")
# Usage
weather_station = WeatherStation()
phone_display = PhoneDisplay()
tv_display = TVDisplay()
weather_station.register_observer(phone_display)
weather_station.register_observer(tv_display)
weather_station.set_temperature(25)
print("\n")
weather_station.set_temperature(30)20. State
Purpose: Allow an object to change behavior when its internal state changes.
Example:
from abc import ABC, abstractmethod
# State Interface
class VendingMachineState(ABC):
@abstractmethod
def handle_request(self):
pass
# Concrete States
class ReadyState(VendingMachineState):
def handle_request(self):
print("Ready state: Please select a product.")
class ProductSelectedState(VendingMachineState):
def handle_request(self):
print("Product selected state: Processing payment.")
class PaymentPendingState(VendingMachineState):
def handle_request(self):
print("Payment pending state: Dispensing product.")
# Context
class VendingMachineContext:
def __init__(self):
self._state = ReadyState()
def set_state(self, state):
self._state = state
print(f"Vending machine state changed to: {self._state.__class__.__name__}")
def request(self):
self._state.handle_request()
# Usage
vending_machine = VendingMachineContext()
vending_machine.request()
vending_machine.set_state(ProductSelectedState())
vending_machine.request()
vending_machine.set_state(PaymentPendingState())
vending_machine.request()21. Strategy
Purpose: Encapsulate interchangeable algorithms and make them selectable at runtime.
Example:
from abc import ABC, abstractmethod
# Strategy Interface
class SortingStrategy(ABC):
@abstractmethod
def sort(self, data):
pass
# Concrete Strategies
class BubbleSortStrategy(SortingStrategy):
def sort(self, data):
print("Sorting with Bubble Sort")
n = len(data)
for i in range(n):
for j in range(0, n - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
return data
class QuickSortStrategy(SortingStrategy):
def sort(self, data):
print("Sorting with Quick Sort")
# Simplified quick sort logic for demonstration
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
# Context
class SortingContext:
def __init__(self, strategy: SortingStrategy):
self._strategy = strategy
def set_strategy(self, strategy):
self._strategy = strategy
def perform_sort(self, data):
return self._strategy.sort(data)
# Usage
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
context = SortingContext(BubbleSortStrategy())
sorted_data = context.perform_sort(data)
print(f"Result: {sorted_data}\n")
context.set_strategy(QuickSortStrategy())
sorted_data = context.perform_sort(data)
print(f"Result: {sorted_data}")22. Template Method
Purpose: Define the skeleton of an algorithm in a base class, letting subclasses override steps.
Example:
from abc import ABC, abstractmethod
class BeverageMaker(ABC):
def make_beverage(self):
self.boil_water()
self.brew()
self.pour_in_cup()
self.add_condiments()
def boil_water(self):
print("Boiling water.")
def pour_in_cup(self):
print("Pouring into cup.")
@abstractmethod
def brew(self):
pass
@abstractmethod
def add_condiments(self):
pass
class CoffeeMaker(BeverageMaker):
def brew(self):
print("Brewing coffee grounds.")
def add_condiments(self):
print("Adding sugar and milk.")
class TeaMaker(BeverageMaker):
def brew(self):
print("Steeping the tea bag.")
def add_condiments(self):
print("Adding lemon.")
# Usage
print("Making coffee...")
coffee_maker = CoffeeMaker()
coffee_maker.make_beverage()
print("\nMaking tea...")
tea_maker = TeaMaker()
tea_maker.make_beverage()23. Visitor
Purpose: Add new operations to class hierarchies without modifying the element classes.
Example:
from abc import ABC, abstractmethod
# Visitor Interface
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_square(self, square):
pass
# Element Interface
class Shape(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Concrete Elements
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Square(Shape):
def __init__(self, side):
self.side = side
def accept(self, visitor):
visitor.visit_square(self)
# Concrete Visitor
class AreaCalculator(ShapeVisitor):
def visit_circle(self, circle):
area = 3.14 * circle.radius ** 2
print(f"Calculated area of Circle: {area}")
def visit_square(self, square):
area = square.side ** 2
print(f"Calculated area of Square: {area}")
# Client Code
if __name__ == "__main__":
shapes = [Circle(3), Square(4)]
area_calculator = AreaCalculator()
for shape in shapes:
shape.accept(area_calculator)