Structural Design Patterns in Software Development
Introduction
Structural design patterns are essential tools in software development that help manage relationships between objects, making systems more flexible, scalable, and maintainable. This post will cover the Proxy, Flyweight, Facade, Decorator, Composite, Bridge, and Adapter patterns.
Proxy Pattern
Introduction
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. This pattern is used to create a representative object that controls access to another object, often to defer the full cost of its creation and initialization until we actually need to use it.
Definition
The Proxy pattern is a structural design pattern that provides an object that acts as a substitute for a real service object, controlling access to it.
Use Cases
- Lazy initialization
- Access control
- Logging requests
- Reducing the cost of accessing heavyweight objects
Benefits
- Adds a level of indirection to support distributed, controlled, or intelligent access
- Enhances security by controlling access
- Reduces the cost of accessing objects
Examples
class RealSubject:
def request(self):
return "RealSubject: Handling request."
class Proxy:
def __init__(self, real_subject):
self._real_subject = real_subject
def request(self):
print("Proxy: Checking access prior to firing a real request.")
result = self._real_subject.request()
print("Proxy: Logging the time of request.")
return result
real_subject = RealSubject()
proxy = Proxy(real_subject)
print(proxy.request())
Real-world Applications
Proxies are widely used in web development for caching web pages, as well as in virtual proxies for memory-intensive objects in graphical applications.
Flyweight Pattern
Introduction
The Flyweight pattern is used to minimize memory usage by sharing as much data as possible with similar objects. It is particularly useful for large numbers of similar objects.
Definition
The Flyweight pattern is a structural design pattern that allows programs to support vast quantities of objects by keeping their memory consumption low.
Use Cases
- When an application uses a large number of similar objects
- To avoid redundancy when there are many objects with shared data
- In graphical applications for rendering similar objects
Benefits
- Reduces memory consumption
- Improves performance by reusing existing objects
Examples
class Flyweight:
def __init__(self, intrinsic_state):
self._intrinsic_state = intrinsic_state
def operation(self, extrinsic_state):
print(f"Flyweight: Displaying shared {self._intrinsic_state} and unique {extrinsic_state} state.")
class FlyweightFactory:
_flyweights = {}
@classmethod
def get_flyweight(cls, intrinsic_state):
if intrinsic_state not in cls._flyweights:
cls._flyweights[intrinsic_state] = Flyweight(intrinsic_state)
return cls._flyweights[intrinsic_state]
factory = FlyweightFactory()
flyweight1 = factory.get_flyweight("shared_state")
flyweight1.operation("unique_state_1")
flyweight2 = factory.get_flyweight("shared_state")
flyweight2.operation("unique_state_2")
print(flyweight1 is flyweight2) # Output: True
Real-world Applications
Flyweights are commonly used in text editors to manage character formatting, where characters are shared across different parts of the document.
Facade Pattern
Introduction
The Facade pattern provides a simplified interface to a complex subsystem. This pattern aims to reduce the complexity of interacting with a large system by providing a single entry point.
Definition
The Facade pattern is a structural design pattern that offers a simple interface to a complex subsystem, making the subsystem easier to use.
Use Cases
- To provide a simple interface to a complex system
- To decouple clients from complex subsystem components
- To make a library easier to use and understand
Benefits
- Simplifies the use of a complex system
- Reduces dependencies on the internal details of the subsystem
- Improves code readability and maintenance
Examples
class Subsystem1:
def operation1(self):
return "Subsystem1: Ready!"
class Subsystem2:
def operation2(self):
return "Subsystem2: Go!"
class Facade:
def __init__(self):
self._subsystem1 = Subsystem1()
self._subsystem2 = Subsystem2()
def operation(self):
results = []
results.append(self._subsystem1.operation1())
results.append(self._subsystem2.operation2())
return " ".join(results)
facade = Facade()
print(facade.operation())
Real-world Applications
Facades are commonly used in software libraries, where a single interface is provided to interact with complex subsystems like databases, file systems, or network services.
Decorator Pattern
Introduction
The Decorator pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class. This pattern is often used for extending functionalities in a flexible and reusable way.
Definition
The Decorator pattern is a structural design pattern that allows you to dynamically add behaviors to an object by placing these objects inside special wrapper objects that contain the behaviors.
Use Cases
- To add responsibilities to objects dynamically and transparently
- To extend the functionality of objects without modifying their code
- To compose behaviors at runtime
Benefits
- Promotes code reusability
- Provides greater flexibility than static inheritance
- Extends functionalities without modifying existing code
Examples
class Component:
def operation(self):
pass
class ConcreteComponent(Component):
def operation(self):
return "ConcreteComponent"
class Decorator(Component):
def __init__(self, component):
self._component = component
def operation(self):
return self._component.operation()
class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({self._component.operation()})"
class ConcreteDecoratorB(Decorator):
def operation(self):
return f"ConcreteDecoratorB({self._component.operation()})"
component = ConcreteComponent()
decorator1 = ConcreteDecoratorA(component)
decorator2 = ConcreteDecoratorB(decorator1)
print(decorator2.operation())
Real-world Applications
In GUI frameworks, decorators are used to add functionalities like scrolling, borders, and tooltips to graphical components dynamically.
Composite Pattern
Introduction
The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. This pattern enables clients to treat individual objects and compositions of objects uniformly.
Definition
The Composite pattern is a structural design pattern that lets you compose objects into tree structures and work with these structures as if they were individual objects.
Use Cases
- To represent part-whole hierarchies
- To allow clients to treat individual objects and compositions uniformly
- When you need to traverse complex structures of objects
Benefits
- Simplifies client code
- Makes it easier to add new kinds of components
- Promotes consistency across similar objects
Examples
class Component:
def operation(self):
pass
class Leaf(Component):
def operation(self):
return "Leaf"
class Composite(Component):
def __init__(self):
self._children = []
def add(self, component):
self._children.append(component)
def remove(self, component):
self._children.remove(component)
def operation(self):
results = []
for child in self._children:
results.append(child.operation())
return f"Composite({'+'.join(results)})"
leaf1 = Leaf()
leaf2 = Leaf()
composite = Composite()
composite.add(leaf1)
composite.add(leaf2)
print(composite.operation())
Real-world Applications
File systems are a common real-world application of the Composite pattern, where files and directories are represented as individual and composite objects respectively.
Bridge Pattern
Introduction
The Bridge pattern is used to separate the abstraction from its implementation so that the two can vary independently. This pattern decouples an abstraction from its implementation, allowing the two to be developed independently.
Definition
The Bridge pattern is a structural design pattern that splits a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently.
Use Cases
- When you want to avoid a permanent binding between an abstraction and its implementation
- When both the abstractions and their implementations should be extensible by subclassing
- To share an implementation among multiple objects
Benefits
- Decouples the abstraction from its implementation
- Increases extensibility
- Hides implementation details from clients
Examples
class Implementation:
def
operation_implementation(self):
pass
class ConcreteImplementationA(Implementation):
def operation_implementation(self):
return "ConcreteImplementationA"
class ConcreteImplementationB(Implementation):
def operation_implementation(self):
return "ConcreteImplementationB"
class Abstraction:
def __init__(self, implementation):
self._implementation = implementation
def operation(self):
return f"Abstraction: Base operation with: {self._implementation.operation_implementation()}"
class ExtendedAbstraction(Abstraction):
def operation(self):
return f"ExtendedAbstraction: Extended operation with: {self._implementation.operation_implementation()}"
implementation_a = ConcreteImplementationA()
abstraction = Abstraction(implementation_a)
print(abstraction.operation())
implementation_b = ConcreteImplementationB()
extended_abstraction = ExtendedAbstraction(implementation_b)
print(extended_abstraction.operation())
Real-world Applications
The Bridge pattern is used in GUI toolkits to separate the abstraction of UI components from their platform-specific implementations.
Adapter Pattern
Introduction
The Adapter pattern allows incompatible interfaces to work together. This pattern involves a single class which is responsible to join functionalities of independent or incompatible interfaces.
Definition
The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate.
Use Cases
- When you want to use an existing class, but its interface does not match the one you need
- To create a reusable class that can interact with unrelated or unforeseen classes
Benefits
- Promotes reusability of existing functionality
- Provides a way to use existing classes with incompatible interfaces
- Enhances flexibility in using different interfaces
Examples
class Target:
def request(self):
return "Target: The default target's behavior."
class Adaptee:
def specific_request(self):
return ".eetpadA eht fo roivaheb laicepS"
class Adapter(Target):
def __init__(self, adaptee):
self._adaptee = adaptee
def request(self):
return f"Adapter: (TRANSLATED) {self._adaptee.specific_request()[::-1]}"
adaptee = Adaptee()
adapter = Adapter(adaptee)
print(adapter.request())
Real-world Applications
Adapters are frequently used in legacy system integration where new systems need to communicate with older systems with incompatible interfaces.
Conclusion
Structural design patterns are essential tools in software development that help manage relationships between objects, making systems more flexible, scalable, and maintainable. By understanding and applying these patterns, developers can create robust and adaptable software architectures.