TUTORIAL SERIES

Design Patterns in Python: Bridge

Linking Abstraction and Implementation

Amir Lavasani
8 min readApr 23, 2024

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Bridge Pattern

What is the Bridge Design Pattern?

The Bridge Design Pattern is a structural design pattern that decouples an abstraction from its implementation, allowing them to vary independently. It achieves this by providing a bridge between the abstraction and its implementation, enabling changes to one without affecting the other.

When to Use the Bridge Pattern:

When considering the use of the Bridge Design Pattern:

  1. Divide and organize a monolithic class with multiple variants: If a class handles various functionalities, such as working with different database servers, and you want to avoid a monolithic structure.
  2. Extend a class in orthogonal dimensions: When you need to extend a class in multiple independent dimensions, the Bridge Pattern suggests creating separate class hierarchies for each dimension.
  3. Switch implementations at runtime: If you need the flexibility to replace implementation objects within the abstraction dynamically, the Bridge Pattern allows for easy implementation swapping.

Practical Example: File Storage Abstraction

This example demonstrates the Bridge Design Pattern by creating a file storage abstraction with implementations for local, cloud, and network storage.

It shows how the pattern separates storage abstraction from specific implementations for flexibility and maintenance.

Dall-E generated image with the following concept: A conceptual drawing of a bridge built from abstract shapes

Abstraction and Implementation

In the Bridge Design Pattern context, the terms Abstraction and Implementation refer to fundamental concepts that facilitate the separation of concerns and promote code maintainability.

  1. Abstraction serves as the high-level control layer, delegating work to the Implementation layer. Abstraction, like a graphical user interface (GUI), interacts with the underlying operating system code (API).
  2. Implementation, or the platform, contains platform-specific code and is declared through a common interface. This allows interchangeable implementations linked to the abstraction object.

By splitting classes into Abstraction and Implementation hierarchies, the Bridge pattern simplifies code maintenance and promotes flexibility.

In the above example, GUI changes won’t affect API-related classes, and adding support for new operating systems in API requires minimal modifications.

Terminology and Key Components

Here are the fundamental components involved:

  1. Abstraction: Provides high-level control logic and relies on the implementation object for low-level work.
  2. Implementation: Declares the interface common to all concrete implementations, allowing communication with the abstraction through declared methods.
  3. Concrete Implementations: Contain platform-specific code and provide implementations for the methods declared in the implementation interface.
  4. Refined Abstractions: Variants of control logic that work with different implementations via the general implementation interface.
  5. Client: Primarily interacts with the abstraction, responsible for linking the abstraction object with one of the implementation objects.

Understanding how these components interact is crucial for effectively utilizing the Bridge Pattern in software design.

Bridge design pattern structure diagram. Image from refactoring.guru

Bridge Implementation in Python

Step 1: Define Abstraction

Abstract class representing the high-level control layer.

# Step 1: Define Abstraction (Abstract class)
class Abstraction(ABC):
"""Abstract class representing the high-level control layer."""

def __init__(self, implementation):
self._implementation = implementation

@abstractmethod
def perform_action(self):
"""Perform an action using the implementation."""
pass

Step 2: Define Implementation

Abstract class representing the platform-specific code.

# Step 2: Define Implementation (Abstract class)
class Implementation(ABC):
"""Abstract class representing the platform-specific code."""

@abstractmethod
def action_impl(self):
"""Perform the actual action."""
pass

Step 3: Create Concrete Implementations

Concrete implementation for platforms A and B.

# Step 3: Create Concrete Implementations
class ConcreteImplementationA(Implementation):
"""Concrete implementation for platform A."""

def action_impl(self):
return "Action performed by Implementation A"

class ConcreteImplementationB(Implementation):
"""Concrete implementation for platform B."""

def action_impl(self):
return "Action performed by Implementation B"

Step 4: Create Refined Abstractions

Refined abstraction provides variants of control logic.

# Step 4: Create Refined Abstractions
class RefinedAbstraction(Abstraction):
"""Refined abstraction providing variants of control logic."""

def perform_action(self):
"""Perform an action using the implementation."""
return self._implementation.action_impl()

Main Client Code

The main section showcases usage.

# Main section to showcase usage
if __name__ == "__main__":
# Create concrete implementations
implementation_a = ConcreteImplementationA()
implementation_b = ConcreteImplementationB()

# Create refined abstractions and link them with concrete implementations
refined_abstraction_a = RefinedAbstraction(implementation_a)
refined_abstraction_b = RefinedAbstraction(implementation_b)

# Use refined abstractions to perform actions
print(refined_abstraction_a.perform_action()) # Output: Action performed by Implementation A
print(refined_abstraction_b.perform_action()) # Output: Action performed by Implementation B

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

Practical Example: File Storage

Step 1: Define Abstraction

Define an abstract class representing the file storage abstraction. This class will declare methods for saving files.

from abc import ABC, abstractmethod

# Step 1: Define Abstraction (Abstract class)
class FileStorage(ABC):
"""Abstract class representing the file storage abstraction."""

@abstractmethod
def save_file(self, file_name):
"""Abstract method to save a file."""
pass

Step 2: Define Implementation

Define an abstract class representing the storage implementation. This class will declare methods for saving files specific to each storage location.

# Step 2: Define Implementation (Abstract class)
class StorageImplementation(ABC):
"""Abstract class representing the storage implementation."""

@abstractmethod
def save(self, file_name):
"""Abstract method to save a file."""
pass

Step 3: Create Concrete Implementations

Implement concrete classes for local storage, cloud storage, and network storage. Each class will provide a specific implementation for saving files.

# Step 3: Create Concrete Implementations
class LocalStorage(StorageImplementation):
"""Concrete implementation for local file storage."""

def save(self, file_name):
"""Save a file locally."""
return f"File '{file_name}' saved locally"
class CloudStorage(StorageImplementation):
"""Concrete implementation for cloud file storage."""

def save(self, file_name):
"""Save a file to the cloud."""
return f"File '{file_name}' saved to the cloud"
class NetworkStorage(StorageImplementation):
"""Concrete implementation for network file storage."""

def save(self, file_name):
"""Save a file to a network location."""
return f"File '{file_name}' saved to a network location"

Step 4: Create Refined Abstractions

Create a refined abstraction class that extends the file storage abstraction. This class will delegate file storage operations to the appropriate storage implementation based on runtime configurations.

# Step 4: Create Refined Abstraction
class AdvancedFileStorage(FileStorage):
"""Refined abstraction for advanced file storage."""

def __init__(self, storage_impl):
"""Initialize with a specific storage implementation."""
self._storage_impl = storage_impl

def save_file(self, file_name):
"""Save a file using the specified storage implementation."""
return self._storage_impl.save(file_name)

Main Client Code

# Main section to showcase usage
if __name__ == "__main__":
# Create concrete implementations
local_storage = LocalStorage()
cloud_storage = CloudStorage()
network_storage = NetworkStorage()

# Create refined abstractions and link them with concrete implementations
advanced_local_storage = AdvancedFileStorage(local_storage)
advanced_cloud_storage = AdvancedFileStorage(cloud_storage)
advanced_network_storage = AdvancedFileStorage(network_storage)

# Use refined abstractions to save files
print(advanced_local_storage.save_file("example.txt")) # Output: File 'example.txt' saved locally
print(advanced_cloud_storage.save_file("example.txt")) # Output: File 'example.txt' saved to the cloud
print(advanced_network_storage.save_file("example.txt")) # Output: File 'example.txt' saved to a network location

Real-World Use Cases for Bridge

  1. Python’s Tkinter and PyQT: Similar to Java GUI frameworks, Python’s GUI libraries employ the Bridge pattern to separate the GUI components from platform-specific details.
  2. OpenGL and DirectX: Graphics rendering libraries like OpenGL and DirectX utilize the Bridge pattern to abstract away hardware-specific implementations, allowing developers to create cross-platform graphics applications.
  3. jQuery: The jQuery JavaScript library employs the Bridge pattern to abstract away browser differences and provide a consistent API for DOM manipulation and event handling across different web browsers.
  4. TensorFlow and PyTorch: Deep learning frameworks like TensorFlow and PyTorch use the Bridge pattern to separate the high-level neural network abstraction from specific hardware accelerators and optimization techniques, enabling scalable and efficient deep learning computations.
  5. Quartz Scheduler: The Quartz Scheduler library utilizes the Bridge pattern to decouple the scheduling logic from specific job store implementations, allowing developers to store job data in various databases or other storage systems.
Dall-E generated image with the following concept: A conceptual drawing of a bridge built from abstract shapes

Bridge Pors and Cons

When working with the Bridge Design Pattern, it’s essential to understand its advantages and potential drawbacks:

Pros

  1. Platform Independence: Bridge allows the creation of platform-independent classes and applications, enhancing portability.
  2. Abstraction: Client code interacts with high-level abstractions, shielding it from platform-specific details, thus promoting clarity.
  3. Open/Closed Principle: Bridge supports the principle of open/closed, enabling the introduction of new abstractions and implementations independently.
  4. Single Responsibility Principle: Bridge encourages separation of concerns, allowing focus on high-level logic in abstractions and platform details in implementations.

Cons

Increased Complexity: Overuse of the pattern in highly cohesive classes can lead to code complexity, making it harder to understand and maintain.

Dall-E generated image with the following concept: Two circles representing abstraction and implementation, linked by a bridge

Bridge’s Relations with Other Patterns

When looking at the Bridge Design Pattern, it’s important to know how it connects with other patterns.

Bridge vs. Adapter:

Bridge is typically designed up-front, allowing the development of parts of an application independently.

Conversely, the Adapter is commonly used with existing applications to reconcile otherwise-incompatible classes.

Bridge, State, Strategy, and Adapter:

These patterns share similar structures, based on composition and delegating work to other objects.

However, they address distinct problems and communicate different solutions to developers.

Bridge with Abstract Factory:

Abstract Factory complements Bridge when specific abstractions defined by Bridge require particular implementations.

Abstract Factory encapsulates these relationships, simplifying the complexity of client code.

Bridge with Builder:

Builder can be combined with Bridge, where the director class acts as the abstraction and different builders serve as implementations.

This combination allows for the creation of complex objects while maintaining the separation of concerns.

Conclusion

In conclusion, we’ve delved into the Bridge Pattern in Python, discovering its ability to separate abstraction from implementation, offering flexibility and maintenance advantages.

We’ve explored its key components, pros and cons, and compared it to related patterns. Additionally, we implemented a practical file storage system showcasing the pattern’s power.

Keep coding and exploring new design patterns! 👩‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

Explore the GitHub Repo 🎉

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Bridge
  3. Head First Design Patterns (Book)
  4. Wikipedia Bridge Pattern
  5. Sourcemaking Bridge Design Pattern
  6. Bridge Design Pattern — A different kind of Golden Gate

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃