Structural Design Patterns in Software Development

Published on 20 April 2024
7 min read
Design Patterns
Software Development
Programming
Python
Tutorial
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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.

Powered by SvelteKit. Icons by Iconoir.