Easy Patterns in Rust: The Bridge Pattern

Photo by nomi gogo: https://www.pexels.com/photo/brown-train-rail-way-555868/

Introduction

The Bridge pattern is a design pattern that is meant to “decouple an abstraction from its implementation so the two can vary independently”. To that end this pattern can use encapsulation, aggregation and also inheritance in order to separate responsibilities.

Since this all sounds rather cryptic, why not look at the UML diagram?

As you can see, there are two hierarchies at work here:

  1. The Abstraction hierarchy
  2. The Implementor/Implementation hierarchy.

This separation makes the two independent of each other or in other words, separates them. As you can see the operation() method is delegated to a class implementing Implementor interface, so these classes can be changed easily.

So, what problems does this pattern solve?

  1. When you need to indepently define or extend either the implementation or the abstraction
  2. If you need to avoid a compile-time binding between implementation and abstraction, but instead want to establish this at run-time.

Implementation in Rust

The payment-methods

We will start with the PaymentMethod trait:

trait PaymentMethod {
    fn pay(&self, amount: f64);
}

The only thing this trait can do is pay a certain amount.

Next we will implement the CreditCard payment method:

struct CreditCard;

impl PaymentMethod for CreditCard {
    fn pay(&self, amount: f64) {
        println!("Paying {} using Credit Card", amount);
    }
}

Since the CreditCard holds no state in our example, the struct can be empty and we do not need some form of constructor.

The same goes for the Cash payment method:

struct Cash;

impl PaymentMethod for Cash {
    fn pay(&self, amount: f64) {
        println!("Paying {} using Cash", amount);
    }
}

The Webshops

We go on by defining the Webshop trait:

trait Webshop {
    fn check_out(&self, amount: f64);
    fn set_method(&mut self, method: Box<dyn PaymentMethod>);
}

A short explanation:

  1. The check_out() method is where the pay() method to the PaymentMethod class gets called
  2. In the set_method() we set the current method of payment.

Now we define our first shop:

struct WebshopA {
    payment_method: Option<Box<dyn PaymentMethod>>,
}

impl WebshopA {
    fn new() -> Self {
        Self {
            payment_method: None,
        }
    }
}

impl Webshop for WebshopA {
    fn check_out(&self, amount: f64) {
        if let Some(ref method) = self.payment_method {
            method.pay(amount);
        } else {
            println!("No payment method set");
        }
    }

    fn set_method(&mut self, method: Box<dyn PaymentMethod>) {
        self.payment_method = Some(method);
    }
}

Quite a lot is happening here:

  1. The WebshopA struct only holds a variable of type Option<Box<dyn PaymentMethod>>. This is because firstly the payment method can be None, when it is not set. Secondly, because it is an interface, its size its known at compile-time, so we need to Box it.
  2. Next we define a small constructor, which sets the payment_method to None.
  3. Note that in the check_out() method we refer to &self and not &mut self because we are not making changes to the struct.
  4. Next we check if we have payment method, if we have None, then print a message, otherwise call the pay() method on the method. Because payment_method is an option, Rust makes it compulsory to unwrap it, and by doing that can handle the errors.
  5. The set_method() method sets the method. Note that we have a &mut self here, because we change the struct.

The second webshop is similar:

struct WebshopB {
    payment_method: Option<Box<dyn PaymentMethod>>,
}

impl WebshopB {
    fn new() -> Self {
        Self {
            payment_method: None,
        }
    }
}

impl Webshop for WebshopB {
    fn check_out(&self, amount: f64) {
        if let Some(ref method) = self.payment_method {
            method.pay(amount);
        } else {
            println!("No payment method set");
        }
    }

    fn set_method(&mut self, method: Box<dyn PaymentMethod>) {
        self.payment_method = Some(method);
    }
}

Time to test

Now we can test our two webshops:

fn main() {
    let mut webshop_a = WebshopA::new();
    let mut webshop_b = WebshopB::new();

    webshop_a.set_method(Box::new(CreditCard));
    webshop_b.set_method(Box::new(Cash));

    webshop_a.check_out(100.0);
    webshop_b.check_out(200.0);
}

Line by line:

  1. Create two webshops
  2. Set the payment method of the first to creditcard, the next to cash
  3. Perform a checkout on both webshops.

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

Conclusion

The Bridge pattern reduces the coupling between classes. It does this by separating the implementation from the abstraction. That means either can be developed independently without interfering with each other.This makes our code more flexible, more readable and less likely to develop errors due to the changes we make

As you can see, implementing this pattern in Rust is quite easy, and even though we are dealing with two hierarchies, the code remains clear and maintanable: it would for example be quite easy to add another payment method.

In a later article I might want to add generics to the mix, to see how flexible this pattern can get.

Leave a Reply

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