Please use a larger screen

This presentation is designed for desktop or projector displays.

← Back to curriculum

Python Object Oriented Programming

Design Patterns (Part 2)

Course 10

Creational Factory how objects are built Structural Decorator Facade how objects compose Behavioral Strategy · Iterator Observer how objects interact

to navigate

Course Curriculum

Where we are in the journey

Quiz Time! (Lecture 09)

What are the three phases of the TDD cycle?

A) Write — Test — Ship

B) Plan — Code — Debug

C) Red — Green — Refactor

D) Arrange — Act — Assert

🔴 RED — write a failing test. 🟢 GREEN — make it pass with the simplest code. 🔵 REFACTOR — clean up without breaking the tests. (AAA is about one test's structure, not the TDD loop.)

Recap: Lecture 09

Test-Driven Development

  • 🔴 RED — write a test that fails for the right reason
  • 🟢 GREEN — simplest code that makes the test pass
  • 🔵 REFACTOR — clean up, tests remain green
  • 📚 Tests act as documentation and a safety net
def test_deposit_adds_money():
    acc = Account()
    acc.deposit(100)
    assert acc.balance == 100

# Fail → implement → pass → clean

Quiz Time! (Lecture 07)

What does the 'D' in SOLID stand for?

A) Decorator Pattern

B) Dependency Inversion Principle

C) Data Abstraction Principle

D) Dynamic Binding Principle

DIP says high-level modules should not depend on low-level modules — both should depend on abstractions.

Recap: Lecture 07

SOLID: Dependency Inversion

  • High-level modules should not depend on low-level modules
  • Both should depend on abstractions
  • Inject dependencies instead of creating them inside classes
  • Makes code testable and flexible
class NotificationService:
    def __init__(self, sender):
        self._sender = sender

    def notify(self, msg):
        self._sender.send(msg)

sms = SMSSender()
svc = NotificationService(sms)

Quiz Time! (Lecture 08)

Which design pattern solves the problem in this code?

class Pizza:
    def __init__(self, size, crust, sauce, cheese,
                 toppings, extra_cheese, extra_sauce,
                 gluten_free, vegan, thin_crust, ...):
        # 15+ parameters, most optional, most combinations nonsensical
        ...

pizza = Pizza("L", "thin", None, True, ["mushrooms"], False, None, ...)

A) Singleton

B) Builder

C) Strategy

D) Factory Method

Builder replaces the monstrous constructor with step-by-step fluent methods: Pizza.builder().size("L").thin_crust().add("mushrooms").build().

Recap: Lecture 08

Design Patterns: Builder

  • 🔨 Constructs complex objects step by step
  • 🫁 Replaces the monstrous constructor with a fluent API
  • 🧩 Each call configures one aspect — readable, reorderable
  • ✅ Use when an object has many optional parameters or construction variants
pizza = (Pizza.builder()
    .size("L")
    .thin_crust()
    .add("mushrooms")
    .extra_cheese()
    .build())

# Readable. No positional noise.
# Only the options you want.

Agenda for Today

🟡 Strategy — swappable algorithms

🟢 Decorator — layered behaviors

🔸 Factory — centralized creation

🔷 Facade — simplified interface

🔴 Iterator — sequential access

🟣 Observer — one-to-many notifications

✅ Anti-patterns & AI in design

Design Patterns: Recap

  • 📚 Reusable solutions to commonly occurring problems in software design
  • 🧱 Not finished code — a template for how to solve a problem
  • 👥 Shared vocabulary among developers: "Let's use a Factory here"
  • 🛠️ Introduced by the Gang of Four (GoF) in 1994 — 23 patterns
Patterns are tools, not rules. Use them when they simplify, not when they impress.
Design Patterns: Elements of Reusable Object-Oriented Software (Gang of Four book cover)

Gamma, Helm, Johnson, Vlissides — the Gang of Four

Three Categories

🏭

Creational

How objects are created

  • Singleton
  • Factory Method
  • Abstract Factory
  • Builder
  • Prototype

🧩

Structural

How objects are composed

  • Adapter
  • Decorator
  • Proxy
  • Composite
  • Facade

💬

Behavioral

How objects communicate

  • Observer
  • Strategy
  • Iterator
  • State
  • Template Method

Strategy Pattern

Define a family of algorithms, put each in a separate class,
and make them interchangeable.

Strategy pattern illustration

Strategy — Real-world Analogy

Imagine you have to get to the airport. Different strategies:

  • 🚌 Bus — cheapest, but slowest
  • 🚕 Taxi — fast and comfortable, but expensive
  • 🚲 Bicycle — free and healthy, but weather-dependent
Same goal (reach the airport), different strategies. You pick one depending on budget, time, or weather.
Transportation strategies: bus, cab, and bicycle to reach the airport

Strategy — The Solution

Strategy — Structure

Context - strategy: Strategy «interface» Strategy + execute() ConcreteStrategyA ConcreteStrategyB ConcreteStrategyC

Strategy in Python

from abc import ABC, abstractmethod

class RouteStrategy(ABC):
    @abstractmethod
    def calculate_route(self, origin, destination):
        pass

class WalkingStrategy(RouteStrategy):
    def calculate_route(self, origin, destination):
        return f"Walking from {origin} to {destination} via parks"

class RoadStrategy(RouteStrategy):
    def calculate_route(self, origin, destination):
        return f"Driving from {origin} to {destination} via highway"

class PublicTransportStrategy(RouteStrategy):
    def calculate_route(self, origin, destination):
        return f"Taking bus from {origin} to {destination}"
class Navigator:
    def __init__(self, strategy: RouteStrategy):
        self.strategy = strategy

    def navigate(self, origin, destination):
        return self.strategy.calculate_route(origin, destination)

nav = Navigator(WalkingStrategy())
print(nav.navigate("Home", "Airport"))

nav.strategy = RoadStrategy()  # swap at runtime!
print(nav.navigate("Home", "Airport"))

Strategy — Pros & Cons

✅ Pros

  • Swap algorithms at runtime
  • Isolate implementation details of each algorithm
  • OCP — add new strategies without modifying context
  • Replace inheritance with composition

❌ Cons

  • Overkill if you only have a couple of algorithms that rarely change
  • Clients must know the differences between strategies to pick the right one

Decorator Pattern

Attach new behaviors to objects by wrapping them
in special wrapper objects.

Decorator pattern illustration

Decorator — Real-world Analogy

Wearing layers of clothing:

  • 👕 You start with a t-shirt (base object)
  • 🧣 Add a sweater when it's cold (decorator 1)
  • 🧥 Put on a jacket for rain (decorator 2)
  • 🌂 Add a raincoat for heavy rain (decorator 3)
Each layer wraps the previous one. You can add or remove layers without changing the t-shirt itself.
Decorator analogy: layering clothes — t-shirt, sweater, jacket, raincoat

Decorator — Visual Intuition

Decorator pattern visual intuition

Decorator — OOP Pattern

from abc import ABC, abstractmethod

class Text(ABC):
    @abstractmethod
    def render(self):
        pass

class PlainText(Text):
    def __init__(self, content):
        self.content = content

    def render(self):
        return self.content

class TextDecorator(Text):
    def __init__(self, wrapped: Text):
        self.wrapped = wrapped

class BoldText(TextDecorator):
    def render(self):
        return f"<b>{self.wrapped.render()}</b>"

class ItalicText(TextDecorator):
    def render(self):
        return f"<i>{self.wrapped.render()}</i>"
text = PlainText("Hello")
bold = BoldText(text)
bold_italic = ItalicText(bold)
print(bold_italic.render())  # <i><b>Hello</b></i>

Python Decorators (functions)

Python has built-in decorator syntax using @:

import time

def time_logger(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper
@time_logger
def process_data(data):
    time.sleep(0.5)
    return f"Processed {len(data)} items"

process_data([1, 2, 3])
# process_data took 0.5012s

Another Example: @auth_required

Guard a function so only authenticated users can call it:

def auth_required(func):
    def wrapper(user, *args, **kwargs):
        if not user.is_authenticated:
            raise PermissionError("Login required")
        return func(user, *args, **kwargs)
    return wrapper
@auth_required
def view_profile(user):
    return f"Welcome, {user.name}"

view_profile(alice)   # Welcome, Alice
view_profile(guest)   # PermissionError: Login required

Decorator — Pros & Cons

✅ Pros

  • Extend behavior without subclassing
  • Add/remove at runtime
  • Combine multiple behaviors by stacking decorators
  • SRP — each decorator handles one concern

❌ Cons

  • Hard to remove a specific wrapper from the middle of a stack
  • Order-dependent — stacking order matters
  • Complex configuration with many layers

Creational Pattern

Factory

Define an interface for creating objects — let subclasses decide which class to instantiate

Factory Method pattern illustration

Factory: Problem

A logistics app that initially handles only truck transport:

class Logistics:
    def plan_delivery(self, order):
        truck = Truck()          # hardcoded!
        truck.load(order)
        truck.deliver()

# Now we need ships too — and drones, trains...
class Logistics:
    def plan_delivery(self, order):
        if order.destination == "overseas":
            ship = Ship()
            ship.load(order); ship.deliver()
        else:
            truck = Truck()
            truck.load(order); truck.deliver()
        # every new transport = edit this method
Violates the Open-Closed Principle.
Factory Method problem: logistics app tightly coupled to Truck class, can't accommodate Ship

Factory: Solution

  • 🔨 Centralize all creation logic in one place
  • 🟢 Clients ask what they need — the factory picks which class
  • 🔁 Adding a new product = one edit to the factory, zero changes to callers
Most useful when many inputs determine which class to instantiate — all that complexity lives in one place.

▶ The diagram on the right shows Factory Method (GoF) — a polymorphic variant with subclass overrides. In Python, the Simple Factory below (a single create() with if/else) is what you'll use 95% of the time.

Factory Method solution diagram

In practice — many params → one of many classes:

class TransportFactory:
    @staticmethod
    def create(destination, weight, urgent):
        if destination == "overseas":
            return Ship(weight)
        if urgent and weight < 10:
            return Drone(weight)
        if weight > 1000:
            return Train(weight)
        return Truck(weight)

t = TransportFactory.create("overseas", 50, False)

Factory: Code

from abc import ABC, abstractmethod

class Transport(ABC):
    @abstractmethod
    def deliver(self):
        pass

class Truck(Transport):
    def deliver(self):
        return "Delivering by land in a box"

class Ship(Transport):
    def deliver(self):
        return "Delivering by sea in a container"
class TransportFactory:
    @staticmethod
    def create_transport(transport_type: str) -> Transport:
        factories = {
            "truck": Truck,
            "ship": Ship,
        }
        return factories[transport_type]()
transport = TransportFactory.create_transport("ship")
print(transport.deliver())
# "Delivering by sea in a container"

Factory: Pros & Cons

🟢 Pros

  • Follows Open-Closed Principle
  • Loose coupling between creator and products
  • Easy to add new product types
  • Single Responsibility — creation logic in one place

🔴 Cons

  • Can lead to many subclasses
  • More complex than direct instantiation
  • Overkill for simple cases with few types

Structural Pattern

Facade

Provide a simplified interface to a complex subsystem

Facade pattern illustration

Facade: Problem

Without a Facade, client code must know all the internals of the subsystem.

Facade: Solution

Real-world: Phone Operator

Customer on phone talking to a call center operator who orchestrates warehouse, packaging, suppliers, delivery, and taxes

The customer talks only to the operator. The warehouse, packaging, suppliers, delivery, and taxes are all hidden behind the facade.

Real-world: Video Conversion Library

A video conversion library internally uses many classes:

  • 🎬 VideoFile, OggCompressionCodec, MPEG4CompressionCodec
  • 🎵 AudioMixer, BitrateReader
  • 📹 CodecFactory, format-specific handlers…
Without a facade, the client must orchestrate all of these in the right order.
Video conversion facade example diagram

Facade: Structure

Client Facade Simple, unified interface SubsystemA SubsystemB SubsystemC

Facade: Code

class VideoFile:
    def __init__(self, filename):
        self.filename = filename

class CodecFactory:
    def extract(self, file):
        return f"codec for {file.filename}"

class BitrateReader:
    def read(self, file, codec):
        return f"reading {file.filename}"

class AudioMixer:
    def fix(self, result):
        return f"fixing audio: {result}"
class VideoConverter:
    def convert(self, filename, format):
        file = VideoFile(filename)
        codec = CodecFactory().extract(file)
        result = BitrateReader().read(file, codec)
        result = AudioMixer().fix(result)
        return f"{filename} converted to {format}"
converter = VideoConverter()
print(converter.convert("funny_cats.ogg", "mp4"))
# "funny_cats.ogg converted to mp4"

Facade: Pros & Cons

🟢 Pros

  • Isolate complexity from client code
  • Promotes loose coupling
  • Makes subsystem easier to use
  • Can structure subsystem into layers

🔴 Cons

  • Can become a god object coupled to everything
  • May hide necessary complexity
  • Extra layer of indirection

Facade: When to Use

Behavioral Pattern

Iterator

Traverse a collection without exposing its internal structure

Iterator pattern illustration

Iterator — Real-world Analogy

Think of visiting a city as a tourist:

  • 🗺️ The city has many landmarks scattered across districts
  • 📝 You use a guidebook or map that shows you the order to visit them
  • 🚶 You walk from one to the next — without knowing the city's street layout
  • 🌐 Different guides (sightseeing, food tour, art tour) offer different orders over the same city
The guide is the iterator. The city is the collection. You traverse the landmarks without caring how they're stored internally.
Tourist following a guide through city landmarks — the iterator pattern analogy

Iterator: Problem

  • 📦 Collections can have different structures: list, tree, stack, graph
  • 🔄 Each structure stores elements differently
  • 💥 Client code shouldn't need to know how elements are stored to iterate over them
  • 😳 Mixing traversal logic with business logic makes code hard to maintain
We need a way to access elements sequentially without exposing the underlying representation.
Different collection structures require different traversal logic

Iterator: Problem (continued)

Client code gets bloated with traversal logic for different collection types

Adding new collection types bloats the client with more traversal variants. Every new structure forces every consumer to learn its internals.

Iterator: Solution

  • 🔨 Extract traversal behavior into a separate iterator object
  • 🔭 In Python: implement __iter__ and __next__
  • 🟢 The collection provides an iterator — clients use it without knowing internals
  • 🔁 Multiple iterators can traverse the same collection independently
Python has this built in! Every for loop uses the iterator protocol.
Iterator pattern solution: collection returns iterator objects; client traverses through the iterator interface

Python Generators — iterators made simple

Writing __iter__ + __next__ is verbose. Python offers a shortcut: generator functions. Use yield instead of return — Python builds the iterator for you.

A full generator in 4 lines:

def countdown(n):
    while n > 0:
        yield n
        n -= 1

for i in countdown(3):
    print(i)  # 3, 2, 1

How it works — each yield pauses and resumes:

  • ✅ Calling countdown(3) returns a generator object, an iterator for free
  • 🎯 yield n pauses execution and hands n to the caller
  • 🔁 Next iteration resumes right after the yield with local state intact
  • ✅ When the function ends, StopIteration is raised automatically

Also: generator expressions — one-liners:

squares = (x * x for x in range(10))
print(next(squares))  # 0
print(next(squares))  # 1
print(sum(squares))   # 4+9+16+...+81 = 280
Generators are lazy — they produce values on demand, not all at once. Perfect for huge or infinite sequences.

Iterator & Python's for Loop

Python's for loop is syntactic sugar for the iterator protocol:

Under the hood

it = iter(collection)
while True:
    try:
        item = next(it)
    except StopIteration:
        break

What you write

for item in collection:
    process(item)
💡 Any object with __iter__ + __next__ works with for, list(), sum(), etc.

Behavioral Pattern

Observer

Notify many objects when one of them changes

Observer pattern illustration

Observer — Real-world Analogy

Think of a YouTube channel:

  • 🎥 The channel (subject) publishes new videos
  • 👤 Subscribers (observers) get notified automatically
  • ❌ The channel doesn't care who subscribes or how many
  • ✅ Subscribers can join or leave freely — no coupling to the channel's internals
One publisher, many subscribers. Loose coupling.
YouTube subscribe button — the publisher-subscriber analogy for the Observer pattern

Observer — The Problem

A Customer wants the new iPhone model that's arriving at a Store soon. Two bad options:

  • 🚶 Customer visits daily — most trips are wasted while the product is en route
  • 📧 Store emails everyone on every new product — spam for customers who don't care
Either the customer wastes time polling, or the store wastes resources spamming the wrong people.
Customer checking the store vs. store sending spam emails — both wasteful without the Observer pattern

Observer in Python

The Subject keeps a list of observers:

class Order:
    def __init__(self):
        self._observers = []
        self._status = "new"

    def subscribe(self, observer):
        self._observers.append(observer)

    def set_status(self, status):
        self._status = status
        for obs in self._observers:
            obs.update(self)

Observers implement a simple interface:

class EmailNotifier:
    def update(self, order):
        print(f"Email: order is now {order._status}")

class Logger:
    def update(self, order):
        print(f"[LOG] {order._status}")

Usage:

order = Order()
order.subscribe(EmailNotifier())
order.subscribe(Logger())

order.set_status("shipped")
# Email: order is now shipped
# [LOG] shipped

Observer — Pros & Cons

✅ Pros

  • Decoupled — subject and observers don't know each other's types
  • Open for extension — add new observers without touching the subject
  • Perfect for event-driven systems, UI updates, pub/sub

❌ Cons

  • Notification order is unpredictable
  • Memory leaks if observers forget to unsubscribe
  • Hard to debug cascading updates across many observers

When to Use Which Pattern?

Pattern Use when…
Strategy You have several algorithms doing the same thing and want to pick at runtime
Decorator You need to add behavior to an object without subclassing
Factory Object creation logic shouldn't live in the caller
Facade Client code is drowning in subsystem complexity
Iterator You want to traverse a collection without exposing internals
Observer Multiple objects must react to a state change
Not sure? Write the simple version first. Refactor to a pattern when the real need appears.

Best Practices

  • 🔎 Understand the problem fully before picking a pattern
  • 💯 KISS — keep it simple, don't over-engineer
  • 🛠️ Follow SOLID principles — patterns naturally emerge
  • 🔄 Refactor to patterns when the need becomes clear, not upfront
  • ⚖️ Understand trade-offs — every pattern has pros and cons
  • 📚 Spot patterns in frameworks you already use: Flask @app.route is a Decorator, Django signals are an Observer, open() is a Factory

⚠️ Anti-patterns

Common solutions that are ineffective or counterproductive.

Anti-patterns illustration

🚫 Anti-pattern: God Object

A single object that does too much — knows too much, controls too much.

class Application:
    def authenticate_user(...): ...
    def process_payment(...): ...
    def send_email(...): ...
    def generate_report(...): ...
    def manage_inventory(...): ...
    def handle_shipping(...): ...
❌ Violates SRP. Break into focused classes: AuthService, PaymentService, etc.
God Class anti-pattern visualization: one massive class absorbing all responsibilities

🚫 Anti-pattern: Spaghetti Code

Tangled control flow, deeply nested, hard to follow.

def process(data):
    if data:
        for item in data:
            if item.active:
                if item.type == "A":
                    if item.value > 0:
                        for sub in item.children:
                            if sub.valid:
                                ...
  • 🚫 Hard to read, debug, and test
  • ✅ Fix with: early returns, extract methods, design patterns
Spaghetti code visualization — tangled dependencies

🚫 Anti-pattern: Golden Hammer

"If all you have is a hammer, everything looks like a nail."

You just learned 6 design patterns. Now every problem looks like one of them.

# "I need to add two numbers. Obviously Strategy."
class AdditionStrategy:
    def execute(self, a, b): return a + b

class Calculator:
    def __init__(self, s): self.s = s
    def calc(self, a, b): return self.s.execute(a, b)

calc = Calculator(AdditionStrategy())
result = calc.calc(2, 3)  # 20 lines to compute 2 + 3
✅ Use patterns when they simplify, not when they impress. A function is often enough.
Mjölnir — the Golden Hammer

🚫 Anti-pattern: Copy-Paste Programming

The same logic repeated 5 times, each slightly different. Fix a bug in one place — it still lives in the other four.

def process_customer(data):
    if not data: return
    cleaned = data.strip().lower()
    db.save("customer", cleaned)

def process_order(data):
    if not data: return
    cleaned = data.strip().lower()
    db.save("order", cleaned)

def process_product(data):
    if not data: return
    cleaned = data.strip().lower()
    db.save("product", cleaned)
DRY (Don't Repeat Yourself) — extract the shared logic into one function that takes the entity type as a parameter.

🤖 AI & Design Patterns

AI knows every GoF pattern by heart — do you still need to learn them?

🤖 Why Patterns Matter More with AI

Patterns don't become obsolete — they become the interface between you and AI-generated code.

🤖 AI & Patterns: Pros & Cons

✅ Pros

  • 💡 Can suggest the right pattern for a described problem
  • ⚡ Generates pattern boilerplate in seconds (Strategy, Factory skeletons)
  • 📚 Explains why an existing codebase uses a specific pattern
  • 🔄 Refactors tangled code toward a clean pattern on request

❌ Cons

  • 🔨 Golden Hammer — AI loves patterns, will use them where a 3-line function works
  • 🤦 Can invent "standard" patterns that don't actually exist
  • 🗸️ Doesn't weigh trade-offs in your specific codebase context
  • 🔒 Critical design calls (which pattern, if any) still need human judgment
The rule: AI suggests patterns — you decide if they fit. Knowing the patterns is how you can tell.

🧠 Pattern Check

Can you spot the pattern from the code?

Quiz Time! (End of Lecture)

Which pattern does this code implement?

class LoggedReader:
    def __init__(self, reader):
        self._reader = reader

    def read(self):
        data = self._reader.read()
        log(f"Read {len(data)} bytes")
        return data

reader = LoggedReader(FileReader("data.txt"))
reader.read()

A) Facade

B) Decorator

C) Strategy

D) Observer

Decorator: LoggedReader wraps another reader and adds logging behavior without modifying the original class. Same interface (read()), extra behavior layered on top.

Quiz Time! (End of Lecture)

And this one?

class UsaTax:
    def apply(self, amount):
        return amount * 0.08

class EuTax:
    def apply(self, amount):
        return amount * 0.20

class TaxCalculator:
    def __init__(self, rule):
        self._rule = rule
    def calc(self, amount):
        return self._rule.apply(amount)

usa = TaxCalculator(UsaTax())
eu  = TaxCalculator(EuTax())

A) Factory Method

B) Decorator

C) Strategy

D) Iterator

Strategy: the tax rule is injected at construction time and called by calc(). Swap the algorithm (USA vs EU) without changing the TaxCalculator class.

Key Takeaways

Patterns are tools, not rules. Use them when they simplify, not when they impress.

Next Up

Libraries & Frameworks

pandas data NumPy numeric FastAPI web Flask web

Pandas, NumPy, FastAPI, Flask — and how to build your own.

📢 Coursework Deadline

Sunday April 26, 23:59 Vilnius time

Time remaining

——h ——m ——s

Submit before this moment. After this moment, it's late.

📋 Coursework FAQ

What about April 27 at 09:00?

❌ Now you're just being optimistic.

What about April 27 at 00:30?

❌ Still after.  Time is linear.

Is April 27 at 00:00:01 after the deadline?

❌ Yes.  It is. Sorry.

📋 Coursework FAQ — Cutting it close

Is April 26 at 23:59:59 OK?

⚠️ Technically.  Pure adrenaline. Don't.

Is April 26 at 23:58 OK?

✅ Yes.  Responsible adult behavior.

What if I'm in a different time zone?

🌏 23:59 Vilnius time. Set your watch.

📋 Coursework FAQ — Excuses

My laptop crashed at 23:58.

Should've submitted at 23:00. 🤷

Can I email the file instead?

❌ No. Use GitHub and Moodle.

My cat walked on the keyboard and deleted my project.

🐈 Use git. git push = cat-proof.

📋 Coursework FAQ — The bigger picture

Can I get an extension?

❌ No.  Unless you have a really good reason.

Real one from 2 years ago: "My neighbor's room caught on fire, I had to evacuate. By the time I got back it was 4 am. Can I have a second chance to defend my course work?"  ✅ That worked.

When should I actually start?

Right now.  Check the countdown — it only goes one way.