Exploring the Bridge Design Pattern: Bridging the Gap in Software Development

In the world of software design, finding the right balance between flexibility and maintainability can be a challenge. The Bridge Design Pattern, one of the structural design patterns, offers a solution to this problem by decoupling abstractions from their implementations. In this article, we’ll delve into the Bridge Design Pattern, understand its core concepts, and explore real-world use cases where it can be applied.

What?

The Bridge Design Pattern is a structural pattern that separates an object’s abstraction from its implementation, allowing them to vary independently.

It’s all about bridging the gap between two orthogonal hierarchies. By decoupling the Abstraction from the Implementor, the Bridge Pattern allows both to evolve independently without affecting each other. This separation fosters flexibility, maintainability, and extensibility in your code.

Note

We’re not talking about interfaces or abstract classes from your programming language. These aren’t the same things.

Abstraction (also called interface) is a high-level control layer for some entity. This layer isn’t supposed to do any real work on its own. It should delegate the work to the implementation layer (also called platform).

Why?

I was asked to design a payment collection system in one my company. It was straight forward requirement to onboard PayTm(gateway) with debit and credit card payment

I create two classes, and we are good to go.

simple payment class

It works perfectly, later the company decided to onboard one more gateway. Now we have 4 classes.

simple payment classes

With the growth of the company and its product, management decided to add UPI as one more mode of payment and Paytm as one more gateway. Now we have 6 classes to deal with

Gateway/Payment modeDebitCreditUPI
PaytmPaytmDebitPaytmCreditPaytmUpi
PaypalPaypalDebitPaypalCreditPaypalUpi

and I realized that, If in future, we want to add one more gateway. The class size will be 9 and it will be a problematic to handle growing number of classes.

Gateway/Payment modeDebitCreditUPI
PaytmPaytmDebitPaytmCreditPaytmUpi
PaypalPaypalDebitPaypalCreditPaypalUpi
InstamojoInstamojoDebitInstamojoCreditInstamojoUpi

With little variation, The number of classes grows exponentially and becomes unmanageable. This is the problem with our code, it has cartesian product complexity.

Not to worry The Bridge Pattern is here to protect us from these class explosions.

In our example, we have two variable

  • Gateway
  • Payment method

Let’s derive the implement classes from the gateway and payment methods and find a way to bridge between them. This can be done by passing one of these two classes as a parameter in the construction of the other class and this design is called a bridge design pattern.

Bridge Design Pattern

In we added UPI payment and instamojo gateway. we will just implement PaymentMethod and Gateway interface respectively and we are good to go.

With approach we have only 3+3 = 6 classes not 9 classes like in previous implementation.

i.e PaymentMethod or Gateway can grow on it own independently.

Code

from abc import ABC, abstractmethod

class Gateway(ABC):
    
    @abstractmethod
    def by_upi(self,amount):
        pass

    @abstractmethod   
    def by_debit(self,amount):
        pass 

    @abstractmethod    
    def by_credit(self,amount):
        pass

class PayTm(Gateway):
    def by_upi(self,amount):
        #upi logic for paytm
        print(f"paid of amount ${amount} via UPI card -- PayTm")
    
    def by_debit(self,amount):
        #debit card logic for paytm
        print(f"paid of amount ${amount} via debit card -- PayTm")
    
    def by_credit(self,amount):
        #credit card logic for paytm
        print(f"paid of amount ${amount} via credit card -- PayTm")
    
class PayPal(Gateway):
    def by_upi(self,amount):
        #upi logic for paypal
        print(f"paid of amount ${amount} via UPI card -- PayPal")
    
    def by_debit(self,amount):
        #debit card logic for paypal
        print(f"paid of amount ${amount} via debit card -- PayPal")
    
    def by_credit(self,amount):
        #credit card logic for paypal
        print(f"paid of amount ${amount} via credit card -- PayPal")
    

class PaymentMethod:

    def __init__(self,gateway):
        self.gateway = gateway

    def make_payment(self):
        pass 

class UPI(PaymentMethod):

    def __init__(self, gateway,amount):
        super().__init__(gateway)
        self.amount = amount

    def make_payment(self):
        self.gateway.by_upi(self.amount)

class Debit(PaymentMethod):

    def __init__(self, gateway,amount):
        super().__init__(gateway)
        self.amount = amount

    def make_payment(self):
        self.gateway.by_debit(self.amount)

class Credit(PaymentMethod):

    def __init__(self, gateway,amount):
        super().__init__(gateway)
        self.amount = amount

    def make_payment(self):
        self.gateway.by_credit(self.amount)

         
paytm  = PayTm()
paypal = PayPal()

# Bridging UPI and Paytm classes
upi1 = UPI(paytm,100)
upi1.make_payment() #paid of amount $100 via UPI card -- PayTm

# Bridging UPI and PayPal classes
upi2 = UPI(paypal,200)
upi2.make_payment() #paid of amount $100 via UPI card -- PayPal

# Bridging Credit and Paytm classes
credit1 = Credit(paytm,300)
credit1.make_payment() #paid of amount $300 via credit card -- PayTm


# Bridging Debit and Paytm classes
credit2 = Debit(paytm,400)
credit2.make_payment() #paid of amount $400 via debit card -- PayTm

Real-World Analogy

To understand the Bridge Pattern better, let’s consider a real-world analogy. Think of a TV remote control as the Abstraction and the TV itself as the Implementor. The remote control provides buttons for changing channels, adjusting the volume, and turning the TV on and off. However, the actual operations are executed by the TV, and different TV models can have different implementations.

Now, if we apply the Bridge Pattern to this scenario, we’d have a remote control Abstraction that’s independent of the TV’s implementation. This means that you can use the same remote control to operate various TV models, making it easy to adapt to new TVs without changing the remote’s design.

Use Cases of the Bridge Design Pattern

The Bridge Pattern is widely used in various software projects where flexibility and extensibility are crucial. Here are a few common use cases:

  1. Graphic User Interfaces (GUI): In GUI libraries, the Bridge Pattern is employed to separate the high-level widgets (buttons, windows) from the low-level windowing system (Windows, macOS). This enables developers to create cross-platform applications easily.
  2. Database Drivers: When designing database drivers, the Bridge Pattern can be used to separate the database connection logic (Implementor) from the high-level query operations (Abstraction). This allows the same high-level interface to work with different database systems.
  3. Remote Controls and Devices: As demonstrated in our earlier analogy, the Bridge Pattern can be applied to remote controls and electronic devices. A single remote control can work with various devices by decoupling the control interface from the actual device implementation.
  4. Operating System File Systems: In the realm of operating systems, the Bridge Pattern is used to provide a unified interface for accessing different file systems (e.g., NTFS, HFS+, ext4) while keeping the platform-independent operations separate.

Benefits of the Bridge Design Pattern

Implementing the Bridge Pattern offers several advantages:

  1. Separation of Concerns: The pattern promotes a clear separation between the Abstraction and Implementor, making the codebase more modular and easier to maintain.
  2. Extensibility: It allows for the addition of new Abstractions and Implementors without modifying existing code, enhancing the software’s scalability.
  3. Platform Independence: When applied to cross-platform development, the Bridge Pattern ensures that high-level code remains independent of low-level platform-specific details.
  4. Code Reusability: By reusing existing Abstractions and Implementors, you can save time and reduce code duplication.

Conclusion

The Bridge Design Pattern is a powerful tool in a software developer’s toolkit, providing a way to balance flexibility and maintainability. By decoupling abstractions from their implementations, this pattern allows you to create more extensible and adaptable software systems. When faced with situations where you need to bridge the gap between different hierarchies, consider implementing the Bridge Pattern to keep your codebase elegant and future-proof.

Resources

For further exploration, make sure to check out these helpful resources:

Leave a Comment