Easy Patterns in Python: The Bridge

Foto door Pixabay: https://www.pexels.com/nl-nl/foto/luchtfoto-fotografie-van-bridge-220762/

Introduction

The Bridge pattern is a way to design software that helps you separate two main parts of a system:

  1. What something does (the “abstraction”): This is like the idea or concept of what you want to achieve.
  2. How it does it (the “implementation”): This is the specific way you make that idea happen.

The goal is to keep these two parts separate so you can change one without affecting the other.

Think of it like this:

Imagine you have a remote control (the abstraction) and different devices it can control, like a TV, a stereo, or a DVD player (the implementations). The remote control doesn’t care how each device works, just that it can send a signal to it. And you can get a new TV without needing a new remote, as long as the new TV understands the remote’s signals.

This pattern uses techniques like:

  • Encapsulation: Hiding the details of how something works.
  • Aggregation: One object containing another object.
  • Inheritance: Creating new classes based on existing ones.

It might seem complicated, but looking at a diagram helps. You’ll usually see two separate family trees (hierarchies) in the design:

  • One for the “what it does” (Abstraction).
  • One for the “how it does it” (Implementor/Implementation).

This separation means you can change the “how” (e.g., switch from one type of database to another) without changing the “what” (e.g., how your program asks for data). The “what it does” part often just passes tasks on to the “how it does it” part, making it easy to swap out the “how.”

So, when is this pattern useful?

  • When you need to be able to change or add new ways of doing things (implementations) or new ideas of what to do (abstractions) independently.
  • When you want to decide how something will be done while the program is running, rather than fixing it during development.

Implementation in Python

In this example we will deal with two webshops, PremiumWebshop and BasicWebshop, and two payment methods: cash and creditcard.

First things first

We will start by importing an attribute and a baseclass from the abc package:

from abc import ABC, abstractmethod

Line by line:

  1. ABC stands for ‘Abstract Base Class`. Such a class serves as a blueprint or a contract which other classes must follow when inheriting from it.
  2. Decorating a method with the @abstractmethod decorator marks the method as abstract, meaning that any class inheriting from it must implement the method.

The PaymentMethod base class

We will start by defining the PaymentMethod base class:

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount: float):
        pass

Line by line:

  1. By inheriting an abstract base this class can server as the bleuprint for all the payment methods we will define.
  2. Since each payment method is different we must implement the pay() method for each subclass of PaymentMethod, that is what @abstractmethod decorator means.

The concrete payment methods

Next we define the CreditCard payment method:

class CreditCardPayment(PaymentMethod):
    def pay(self, amount: float):
        print(f"Processing credit card payment of ${amount:.2f}")

Paying in this context means nothing more than just printing a message.

The Cash payment method is similar:

class CashPayment(PaymentMethod):
    def pay(self, amount: float):
        print(f"Processing cash payment of ${amount:.2f}")

The Webshop base class

The Webshop is another abstract base class which looks like this:

class Webshop(ABC):
    def __init__(self, payment_method: PaymentMethod):
        self._payment_method = payment_method
    
    @abstractmethod
    def check_out(self, amount: float):
        pass
    
    @property
    def payment_method(self) -> PaymentMethod:
        return self._payment_method
    
    @payment_method.setter
    def payment_method(self, method: PaymentMethod):
        self._payment_method = method

Line by line:

  1. We start by defining a constructor which only takes in a payment method.
  2. Next comes the one abstract method, which is the check_out() method, which has a different implementation for each webshop.
  3. Finally a getter and a setter for the payment method.

The webshop implementations

In the PremiumWebshop customers get a 10% discount on their purchases:

class PremiumWebshop(Webshop):
    def check_out(self, amount: float):
        discount = amount * 0.1  
        final_amount = amount - discount
        print(f"Premium shop: ${amount:.2f} - ${discount:.2f} discount = ${final_amount:.2f}")
        self._payment_method.pay(final_amount)

Note that because the checkout() method was decorated with the @abstractmethod decorator this the one method we have to implement here.

However in the BasicWebshop customers pay an additional 21% tax:

class BasicWebshop(Webshop):
    def check_out(self, amount: float):
        tax = amount * 0.21
        final_amount = amount + tax
        print(f"Basic shop: ${amount:.2f} + ${tax:.2f} tax = ${final_amount:.2f}")
        self._payment_method.pay(final_amount)


Testing time

We can now test our code:

if __name__ == "__main__":

    webshop_a = PremiumWebshop(payment_method=CreditCardPayment())
    webshop_a.check_out(100.0)

    webshop_a.payment_method = CashPayment()
    webshop_a.check_out(50.0)

    webshop_b = BasicWebshop(payment_method=CreditCardPayment())
    webshop_b.check_out(200.0)

Line by line:

  1. We start by creating a premium webshop with a credit card payment method, and do a checkout
  2. We change the payment method to cash, and do another checkout
  3. Finally we create basic webshop with a creditcard payment method and do a checkout.

As you can see, by using interface we change the implementation of PaymentMethod without affecting our webshops.

Conclusion

In conclusion, the Bridge pattern provides a powerful and elegant solution for decoupling an abstraction from its implementation. By separating these two distinct hierarchies—the “what it does” and the “how it does it”—we gain a significant amount of flexibility. As demonstrated in the Python example, this allows us to independently vary our webshop logic and our payment methods. We can introduce new payment types (like a digital wallet) or new webshop tiers (like a VIP webshop) without having to modify the existing classes. This separation also makes our code more maintainable, scalable, and easier to understand, as each component has a single, well-defined responsibility. The Bridge pattern is an excellent choice for any system where you need to avoid tight coupling and allow for runtime flexibility between different parts of your application.

The Code Nomad
The Code Nomad
Articles: 167

Leave a Reply

Your email address will not be published. Required fields are marked *