Box<dyn ...> enables the Bridge Pattern

Continouing the series about design patterns today I wanted to explore the Bridge pattern. Reading the GOF book it states “Decouple an abstraction from its implementation so that the two can vary indepedently”. In Rust, the use of Box<dyn ...> makes it easier to implement the Bridge Pattern due to its ability to handle dynamic dispatch and encapsulate different implementations behind a uniform interface.

The Bridge Pattern is a structural design pattern that separates the abstraction from its implementation. This separation allows you to change both the abstraction and the implementation independently without affecting each other.

bridge

Key Components of the Bridge Pattern:

  • Abstraction: An abstract class or interface defining the high-level operations.
  • Refined Abstraction: A class that extends the abstraction to add more functionalities.
  • Implementor: An interface for the implementation classes.
  • Concrete Implementors: Classes that implement the Implementor interface and provide the concrete behavior.

Example

Let’s consider a scenario where we have different types of bank accounts (like SavingsAccount and CheckingAccount) and different types of account operations (like Deposit and Withdraw). We want to decouple the accounts from the operations so that we can mix and match them independently.

Implementor trait

trait AccountOperation {
    fn execute(&self, amount: f64) -> String;
}

Concreate implementors

struct Deposit;
struct Withdraw;

impl AccountOperation for Deposit {
    fn execute(&self, amount: f64) -> String {
        format!("Deposited ${}", amount)
    }
}

impl AccountOperation for Withdraw {
    fn execute(&self, amount: f64) -> String {
        format!("Withdrew ${}", amount)
    }
}

Abstraction Trait

trait BankAccount {
    fn perform_operation(&self, amount: f64) -> String;
}

Refined abstractions

struct SavingsAccount {
    operation: Box<dyn AccountOperation>,
}

struct CheckingAccount {
    operation: Box<dyn AccountOperation>,
}

impl SavingsAccount {
    fn new(operation: Box<dyn AccountOperation>) -> Self {
        SavingsAccount { operation }
    }
}

impl CheckingAccount {
    fn new(operation: Box<dyn AccountOperation>) -> Self {
        CheckingAccount { operation }
    }
}

impl BankAccount for SavingsAccount {
    fn perform_operation(&self, amount: f64) -> String {
        format!("Savings Account: {}", self.operation.execute(amount))
    }
}

impl BankAccount for CheckingAccount {
    fn perform_operation(&self, amount: f64) -> String {
        format!("Checking Account: {}", self.operation.execute(amount))
    }
}

Using the brindge

fn main() {
    let deposit = Box::new(Deposit);
    let withdraw = Box::new(Withdraw);

    let savings_account = SavingsAccount::new(deposit);
    let checking_account = CheckingAccount::new(withdraw);

    println!("{}", savings_account.perform_operation(100.0));
    println!("{}", checking_account.perform_operation(50.0));
}

How Box<dyn ...> Facilitates the Bridge Pattern

  • Dynamic Dispatch: The Box<dyn …> allows for dynamic dispatch, meaning the method to be called is determined at runtime. This is crucial for implementing the Bridge Pattern as it allows the abstraction to call the correct implementation methods without knowing the concrete class.
  • Encapsulation: Box<dyn …> encapsulates the implementation details, adhering to the principle of hiding the complexities of the implementation from the client.
  • Flexibility: With Box<dyn …>, we can easily swap out different implementations without changing the client code.
Published 8 Jun 2024

Tüftler (someone who enjoys working on and solving technical problems, often in a meticulous and innovative manner). Opinions are my own and not necessarily the views of my employer.
Avraam Mavridis on Twitter