📝 Topics Covered
- 1. What are Structural Patterns?
- 2. Adapter Design Pattern
- 3. Bridge Design Pattern
- 4. Composite Design Pattern
- 5. Decorator Design Pattern
- 6. Flyweight Design Pattern
- 7. Proxy Design Pattern
- 8. Facade Design Pattern
1. What are Structural Patterns?
Structural design patterns explain how to assemble objects and classes into larger structures while keeping these structures flexible and efficient. They focus on simplifying relationships between entities through inheritance, interface adaptations, and compositions.
In this guide, we will explore seven primary structural patterns:
- Adapter: Bridges incompatible interfaces.
- Bridge: Decouples interfaces from concrete implementations.
- Composite: Composes objects in tree structures for uniform client treatment.
- Decorator: Dynamically wrappers objects to append behaviors.
- Flyweight: Minimizes memory consumption by sharing common states.
- Proxy: Introduces a surrogate layer controlling access to a real object.
- Facade: Exposes a simplified interface over complex subsystems.
2. Adapter Design Pattern
The Adapter pattern (also known as the Wrapper pattern) acts as a bridge between two incompatible interfaces. It enables objects with completely different interfaces to collaborate seamlessly without modifying their underlying source code.
🎥 Tutorial: Adapter Design Pattern
- Problems Solved:
- Reusing a legacy class that doesn’t implement the exact interface client code expects.
- Creating a translator layer that allows diverse data models (e.g. XML vs. JSON) to interact.
- Two Key Variants:
- Class Adapter: Adapts interfaces using multiple inheritance (limited to subclassing).
- Object Adapter: Adapts interfaces using object composition (highly flexible; can wrap both classes and objects).
2.1 Adapter Example 1: XML-to-JSON Data Analytics
# Legacy Code (Produces XML data)
class XMLData:
def get_xml_data(self):
return 'XML Data Content'
# Modern Analytics Tool (Accepts JSON data)
class DataAnalyticsTool:
@staticmethod
def analyse_data(json_data):
print(f"Analyzing parsed stream: {json_data}")
# The Adapter (Translates XMLData to JSON format)
class XMLToJSONAdapter:
def __init__(self, xml_data_obj):
self.xml_data_obj = xml_data_obj
def get_json_data(self):
raw_xml = self.xml_data_obj.get_xml_data()
# Simulated parsing logic
return f"ConvertedJSON[{raw_xml}]"
# Orchestrator Factory
class DataAnalyticsToolFactory:
@staticmethod
def process_data(data_type):
if data_type == 'json':
DataAnalyticsTool.analyse_data('Direct JSON Data')
elif data_type == 'xml':
xml_source = XMLData()
adapter = XMLToJSONAdapter(xml_source)
converted_json = adapter.get_json_data()
DataAnalyticsTool.analyse_data(converted_json)
# Usage
if __name__ == "__main__":
factory = DataAnalyticsToolFactory()
factory.process_data('json')
factory.process_data('xml')
2.2 Adapter Example 2: Sparrow adapting to Toy Duck
class Bird:
def make_sound(self):
pass
class Sparrow(Bird):
def make_sound(self):
print("Sparrow: Chirp Chirp!")
class ToyDuck:
def squeak(self):
pass
class PlasticToyDuck(ToyDuck):
def squeak(self):
print("Toy Duck: Squeak Squeak!")
# Wrapper adapting Bird to ToyDuck
class BirdAdapter(ToyDuck):
def __init__(self, bird):
self.bird = bird
def squeak(self):
# Translates squeak request to bird sound
self.bird.make_sound()
# Usage
if __name__ == "__main__":
sparrow = Sparrow()
toy_duck = PlasticToyDuck()
print("--- Real Bird ---")
sparrow.make_sound()
print("--- Toy Duck ---")
toy_duck.squeak()
print("--- Adapted Bird (acts like Toy Duck) ---")
adapted_sparrow = BirdAdapter(sparrow)
adapted_sparrow.squeak()
3. Bridge Design Pattern
The Bridge design pattern separates an abstraction from its implementation so that the two can vary independently. It decouples the interface hierarchy from the concrete implementation hierarchy.
3.1 Adapter vs. Bridge
- Adapter Pattern: Typically applied to
existing applicationsto bridge gaps between incompatible third-party modules or legacy code after design has finalized. - Bridge Pattern: Designed
up-frontas a proactive architectural strategy to allow abstract classes and implementation structures to grow independently.
3.2 Bridge Example: Shape Renderers (Vector vs. Raster)
Rather than subclassing shapes for every combination (e.g. VectorCircle, RasterCircle, VectorSquare), we bridge Shape and Renderer hierarchies.
# The Implementation Hierarchy (Renderers)
class Renderer:
def render_circle(self, radius):
pass
class VectorRenderer(Renderer):
def render_circle(self, radius):
print(f"Vector rendering a circle of radius {radius}")
class RasterRenderer(Renderer):
def render_circle(self, radius):
print(f"Raster rendering a circle of radius {radius} (composed of pixels)")
# The Abstraction Hierarchy (Shapes) bridged with Renderer
class Shape:
def __init__(self, renderer):
self.renderer = renderer
def draw(self):
pass
class Circle(Shape):
def __init__(self, renderer, radius):
super().__init__(renderer)
self.radius = radius
def draw(self):
self.renderer.render_circle(self.radius)
# Usage
if __name__ == '__main__':
vector_renderer = VectorRenderer()
raster_renderer = RasterRenderer()
# Draw shapes using vector renderer
circle1 = Circle(vector_renderer, 5)
circle1.draw()
# Draw shapes using raster renderer
circle2 = Circle(raster_renderer, 10)
circle2.draw()
4. Composite Design Pattern
The Composite pattern (also known as the Object Tree pattern) composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual leaf objects and group compositions uniformly.
🎥 Tutorial: Composite Design Pattern
4.1 Composite Example: Graphical Elements Grouping
class GraphicObject:
def __init__(self, name="Group", color=None):
self.name = name
self.color = color
self.children = []
def __str__(self):
color_part = f"{self.color} " if self.color else ""
return f"{color_part}{self.name}"
def print_hierarchy(self, depth=0):
print(" " * depth + f"- {self}")
for child in self.children:
child.print_hierarchy(depth + 1)
class Circle(GraphicObject):
def __init__(self, color):
super().__init__("Circle", color)
class Square(GraphicObject):
def __init__(self, color):
super().__init__("Square", color)
# Usage
if __name__ == '__main__':
drawing = GraphicObject("Drawing")
drawing.children.append(Square("Red"))
drawing.children.append(Circle("Yellow"))
sub_group = GraphicObject("Sub-Group")
sub_group.children.append(Circle("Blue"))
sub_group.children.append(Square("Blue"))
drawing.children.append(sub_group)
print("--- Composite Graphic Hierarchy ---")
drawing.print_hierarchy()
5. Decorator Design Pattern
Decorator is a structural pattern that attaches new behaviors to objects dynamically by wrapping them inside special wrapper classes.
- Typical Implementations:
- Extending class behavior through composition instead of inheritance.
- Modifying function outputs at runtime using wrapper functions (Closures).
5.1 Decorator Example 1: Basic Python Closures
def make_pretty(func):
def inner():
print("Inner Wrapper Operations...")
func()
return inner
# Applying the decorator syntax
@make_pretty
def ordinary():
print("Ordinary Base Function")
# Usage
if __name__ == '__main__':
ordinary()
5.2 Decorator Example 2: Timing Execution wrapper
import time
def time_it(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Method '{func.__name__}' took {int((end - start) * 1000)}ms to execute.")
return result
return wrapper
@time_it
def heavy_operation():
print("Performing heavy computational tasks...")
time.sleep(1)
print("Computation finished.")
return 200
# Usage
if __name__ == '__main__':
heavy_operation()
6. Flyweight Design Pattern
The Flyweight pattern is an optimization pattern designed to reduce memory footprint when creating massive numbers of similar objects by sharing common shared attributes instead of recreating them in each instance.
- States Division:
- Intrinsic States: Constant, shared, read-only data that resides inside the shared flyweight object (e.g., textures, constants, type definitions).
- Extrinsic States: Contextual data unique to each instance that resides in the client context (e.g., coordinates, runtime speeds).
6.1 Flyweight Example 1: Memory optimization for identical Usernames
class User:
# A global repository mapping unique names to unique ID slots
strings_cache = []
def __init__(self, full_name):
def get_or_cache(name):
if name in self.strings_cache:
return self.strings_cache.index(name)
self.strings_cache.append(name)
return len(self.strings_cache) - 1
# Store integer offsets to the strings cache rather than duplicate strings in memory
self.names = [get_or_cache(part) for part in full_name.split(' ')]
def __str__(self):
return ' '.join([self.strings_cache[idx] for idx in self.names])
# Usage
if __name__ == '__main__':
u1 = User('Amrit Singh')
u2 = User('Rakesh Prasad')
u3 = User('Amrit Prasad') # Shares "Amrit" and "Prasad" indexes!
print(f"Strings cached globally: {User.strings_cache}")
6.2 Flyweight Example 2: Forest Tree Rendering Simulation
import time
# Intrinsic State representation (Shared globally)
class TreeType:
def __init__(self, color):
print(f"Creating TreeType asset instance with '{color}' color...")
time.sleep(1) # Simulate expensive disk I/O loading
self.color = color
self.height = 10
# Flyweight Factory ensuring single instance per color
class TreeFactory:
_types = {}
@classmethod
def get_tree_type(cls, color):
if color not in cls._types:
cls._types[color] = TreeType(color)
return cls._types[color]
# Extrinsic state representation (Unique coordinates)
class Tree:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.tree_type = TreeFactory.get_tree_type(color)
def render(self):
print(f"Tree coordinates [{self.x}, {self.y}] color: '{self.tree_type.color}' height: {self.tree_type.height}m")
# Usage
if __name__ == '__main__':
forest = []
print("--- Planting Trees ---")
forest.append(Tree(10, 20, "Green"))
forest.append(Tree(15, 30, "Brown"))
forest.append(Tree(45, 90, "Green")) # Instantly retrieves cached green type!
forest.append(Tree(45, 95, "Green")) # Instantly retrieves cached green type!
print("--- Rendering Forest ---")
for tree in forest:
tree.render()
7. Proxy Design Pattern
The Proxy pattern introduces a placeholder or surrogate object to control access to a real target object. The proxy acts as an intermediary layer, enabling lazy initialization, protection policies, logging, or access controls.
7.1 Proxy Example 1: Protection Proxy (Access Policies)
class Driver:
def __init__(self, name, age):
self.name = name
self.age = age
class Car:
def __init__(self, driver):
self.driver = driver
def drive(self):
print(f"Car is driven by {self.driver.name} (Age: {self.driver.age})")
# Proxy class enforcing restrictions
class CarProxy:
def __init__(self, driver):
self.driver = driver
self._car = Car(driver)
def drive(self):
if self.driver.age >= 16:
self._car.drive()
else:
print(f"Access Denied: Driver {self.driver.name} is too young to operate vehicles.")
# Usage
if __name__ == '__main__':
young_driver = Driver('Amrit', 15)
print("--- Direct Drive (Unsafe) ---")
car = Car(young_driver)
car.drive()
print("--- Proxy Controlled Drive ---")
proxy_car = CarProxy(young_driver)
proxy_car.drive()
7.2 Proxy Example 2: Virtual Proxy (Lazy Loading)
Ensures expensive assets are not loaded until they are actually rendered on screen.
class BitmapImage:
def __init__(self, filename):
self.filename = filename
print(f"Expensive Disk I/O: Loading bitmap file '{filename}'...")
def draw(self):
print(f"Rendering bitmap graphic '{self.filename}' on canvas.")
# Virtual Proxy class holding a reference
class LazyBitmapImage:
def __init__(self, filename):
self.filename = filename
self._real_image = None
def draw(self):
if not self._real_image:
# Instantiate class only when draw is actually called
self._real_image = BitmapImage(self.filename)
self._real_image.draw()
# Usage
if __name__ == '__main__':
print("--- Direct Loading ---")
img = BitmapImage("landscape.png") # Loads immediately!
print("--- Proxy Loading ---")
lazy_img = LazyBitmapImage("landscape.png") # Doesn't load yet!
print("Ready to display canvas...")
lazy_img.draw() # Loads on-demand!
8. Facade Design Pattern
The Facade pattern provides a simplified, unified interface over a complex, sophisticated subsystem. It hides structural complexities behind a friendly “glass face.”
🎥 Tutorial: Facade Design Pattern
8.1 Facade Example: Food Ordering Subsystems (Zomato)
class OrderSystem:
def place_order(self, item):
return f"Order placed for: {item}"
class PaymentGateway:
def make_payment(self):
return "Transaction successful"
class DeliveryService:
def dispatch(self, item):
return f"Dispatched {item} with courier service"
# The Facade: Wrapping subsystems into a simple, single method
class ZomatoFacade:
def __init__(self):
self.order_sys = OrderSystem()
self.payment_sys = PaymentGateway()
self.delivery_sys = DeliveryService()
def order_food(self, item):
print(f"--- Processing order for '{item}' ---")
print(self.order_sys.place_order(item))
print(self.payment_sys.make_payment())
print(self.delivery_sys.dispatch(item))
print("Enjoy your meal!\n")
# Usage
if __name__ == '__main__':
zomato = ZomatoFacade()
zomato.order_food('Burger')