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.

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.