Structural Patterns III: Proxy & Flyweight

Design Patterns

Chapter 6 · Structural Patterns III: Proxy & Flyweight

Two more structural patterns, both about a wrapping object that stands in front of another object — but for very different reasons. Proxy controls access to an object, transparently, by standing in its place. Flyweight shares one object across many logical instances, to avoid creating thousands of near-identical objects that only differ in a small amount of context-specific data.

Proxy: Controlling Access to an Object Transparently

A proxy has the exact same interface as the real object it stands in for — the calling code can't tell the difference — but it adds a check, a delay, or a shortcut before (or instead of) forwarding the call through.

Virtual Proxy: Deferring an Expensive Object's Creation

class RealImage: load_count = 0 def __init__(self, filename): RealImage.load_count += 1 self.filename = filename def display(self): return f'Displaying {self.filename}' class ImageProxy: def __init__(self, filename): self.filename = filename self._real_image = None def display(self): if self._real_image is None: self._real_image = RealImage(self.filename) return self._real_image.display()
Verified directly — the expensive object is never created until it's actually needed
Creating ImageProxy('photo.jpg') leaves RealImage.load_count at 0 — nothing expensive has happened yet. The first call to .display() brings load_count to 1, creating the real image for the first time. A second call to .display() leaves load_count at 1 — the proxy reuses the already-created real image rather than loading it again, while both calls return the identical correct result, 'Displaying photo.jpg'.

Protection Proxy: Blocking an Unauthorized Call Before It Happens

class BankAccount: def __init__(self, balance): self.balance = balance def withdraw(self, amount): self.balance -= amount return self.balance class BankAccountProxy: def __init__(self, account, user_role): self._account = account self._user_role = user_role def withdraw(self, amount): if self._user_role != 'owner': raise PermissionError('Only the account owner can withdraw') return self._account.withdraw(amount)
Verified directly — the real object's own method is only reached when access is legitimate
Wrapping a BankAccount(500) in a BankAccountProxy(account, 'owner'): calling .withdraw(50) succeeds, and the real account's balance correctly drops to 450. Wrapping that same real account in a second proxy with role 'guest': calling .withdraw(100) raises PermissionError — and the real account's balance is confirmed still 450 afterward, proving BankAccount.withdraw() itself was never reached at all.
Not the same job as Chapter 4's Adapter
BankAccountProxy's withdraw(amount) method has the exact same signature as the real BankAccount.withdraw(amount) it wraps — nothing is being translated. Chapter 4's Adapter existed specifically because checkout() and LegacyPaymentGateway couldn't communicate at all without translation. A Proxy assumes the interface already matches; its job is controlling when or whether a call reaches the real object, not making an incompatible one usable.

Flyweight: Sharing State Across Many Similar Objects

A forest with 10,000 trees doesn't need 10,000 separate copies of each tree species' own name, color, and texture — those details are identical for every tree of the same species. Flyweight splits an object's data into intrinsic state (shared, reused across every instance — the species' own name/color/texture) and extrinsic state (unique per instance — each individual tree's own x, y position), and hands the extrinsic part in from outside rather than storing it on the shared object.

class TreeType: # the flyweight — shared intrinsic state _cache = {} def __new__(cls, name, color, texture): key = (name, color, texture) if key not in cls._cache: cls._cache[key] = super().__new__(cls) cls._cache[key].name = name cls._cache[key].color = color cls._cache[key].texture = texture return cls._cache[key] def draw(self, x, y): # x, y arrive from outside — extrinsic return f'Drawing {self.name} tree at ({x},{y})' class Tree: # holds only the extrinsic state, plus a reference to the shared flyweight def __init__(self, x, y, tree_type): self.x = x; self.y = y; self.tree_type = tree_type def draw(self): return self.tree_type.draw(self.x, self.y)
Verified directly — 10,000 trees, only 3 shared flyweight objects
Planting 10,000 trees, randomly chosen from just 3 species (Oak, Pine, Birch), produced exactly 3 distinct TreeType objects in TreeType._cache — one per species, no matter how many individual trees were planted. Of those 10,000 trees, 3,273 turned out to be Oak — and the first two Oak trees checked were confirmed to share the identical TreeType object (tree_a.tree_type is tree_b.tree_typeTrue), not just two separately-created objects with equal values.
Why this actually saves memory
Every one of those 3,273 Oak Tree objects stores its own x, y, and a reference to the one shared TreeType — not its own copy of 'Oak', 'Dark Green', and 'Rough'. Without Flyweight, each of the 10,000 trees would carry its own full copy of its species' own name/color/texture data; with it, that data exists exactly 3 times total, regardless of whether the forest has 10,000 trees or 10,000,000.

Where This Connects

This chapter's findingWhat it sets up
Proxy's same-signature, access-gating wrapper, verified distinct from Chapter 4's translating AdapterChapter 7's Strategy pattern also swaps in an object behind an identical interface — but to change behavior, not access
Flyweight's intrinsic/extrinsic split, verified at 10,000-to-3 scaleA concrete example of trading object count for a small amount of externally-passed context — the same tradeoff shows up again in caching strategies generally
Both patterns keeping the calling code's interface completely unchangedEvery Structural pattern in this course (Adapter, Facade, Decorator, Composite, Proxy, Flyweight) shares this one property — worth noticing as the capstone approaches

Hands-On Exercises

Exercise 1

Extend this chapter's own ImageProxy with a preload_count that tracks how many times .display() was called in total (not just how many times the real image was actually loaded). Call .display() four times on the same proxy and verify both counts.

📄 View solution
Exercise 2

Using this chapter's own BankAccount/BankAccountProxy, add a role 'viewer' that is allowed to call a new check_balance() method (added to both classes) but still blocked from withdraw(). Verify a viewer-role proxy can check the balance but is still denied a withdrawal.

📄 View solution
Exercise 3

Using this chapter's own TreeType/Tree/Forest-style setup, plant 500 trees using 7 distinct species instead of 3, and verify how many TreeType objects actually get created. Then explain, in your own words, what would happen to that count if every tree were instead given its own slightly different shade of color.

📄 View solution

Chapter 6 Quick Reference

  • Proxy: stands in for a real object behind an identical interface, controlling access — verified two ways: a virtual proxy deferring an expensive object's creation until first use (load count stayed at 0 until the first real call), and a protection proxy blocking an unauthorized call before the real object's own method was ever reached
  • Flyweight: splits shared intrinsic state from unique extrinsic state, so many logical instances can reuse one physical object — verified: 10,000 planted trees across 3 species produced exactly 3 shared flyweight objects, with same-species trees confirmed to reference the identical object
  • Next chapter: Behavioral Patterns I — Strategy and Template Method