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.
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.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));
}
Box<dyn ...>
Facilitates the Bridge Pattern