Interchangeable algorithms with traits
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. In Rust, traits are the natural way to express this - each strategy implements a common trait.
When you need strategies in Rust:
use std::fmt::Debug;
/// Strategy trait for compression algorithms
pub trait CompressionStrategy: Debug {
fn compress(&self, data: &[u8]) -> Vec<u8>;
fn decompress(&self, data: &[u8]) -> Vec<u8>;
fn name(&self) -> &str;
}
/// Concrete strategy: No compression
#[derive(Debug, Default)]
pub struct NoCompression;
impl CompressionStrategy for NoCompression {
fn compress(&self, data: &[u8]) -> Vec<u8> {
data.to_vec()
}
fn decompress(&self, data: &[u8]) -> Vec<u8> {
data.to_vec()
}
fn name(&self) -> &str {
"none"
}
}
/// Concrete strategy: Run-length encoding
#[derive(Debug, Default)]
pub struct RleCompression;
impl CompressionStrategy for RleCompression {
fn compress(&self, data: &[u8]) -> Vec<u8> {
if data.is_empty() {
return Vec::new();
}
let mut result = Vec::new();
let mut count = 1u8;
let mut current = data[0];
for &byte in &data[1..] {
if byte == current && count < 255 {
count += 1;
} else {
result.push(count);
result.push(current);
count = 1;
current = byte;
}
}
result.push(count);
result.push(current);
result
}
fn decompress(&self, data: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
for chunk in data.chunks(2) {
if chunk.len() == 2 {
let count = chunk[0];
let byte = chunk[1];
result.extend(std::iter::repeat(byte).take(count as usize));
}
}
result
}
fn name(&self) -> &str {
"rle"
}
}
/// Context that uses the strategy
pub struct FileCompressor {
strategy: Box<dyn CompressionStrategy>,
}
impl FileCompressor {
pub fn new(strategy: Box<dyn CompressionStrategy>) -> Self {
FileCompressor { strategy }
}
pub fn set_strategy(&mut self, strategy: Box<dyn CompressionStrategy>) {
self.strategy = strategy;
}
pub fn compress_file(&self, data: &[u8]) -> Vec<u8> {
println!("Compressing with {} strategy", self.strategy.name());
self.strategy.compress(data)
}
pub fn decompress_file(&self, data: &[u8]) -> Vec<u8> {
self.strategy.decompress(data)
}
}
/// Generic strategy using generics instead of trait objects
pub struct GenericCompressor<S: CompressionStrategy> {
strategy: S,
}
impl<S: CompressionStrategy> GenericCompressor<S> {
pub fn new(strategy: S) -> Self {
GenericCompressor { strategy }
}
pub fn compress(&self, data: &[u8]) -> Vec<u8> {
self.strategy.compress(data)
}
}
/// Strategy with closures (functional approach)
pub struct FunctionStrategy<F, G>
where
F: Fn(&[u8]) -> Vec<u8>,
G: Fn(&[u8]) -> Vec<u8>,
{
compress_fn: F,
decompress_fn: G,
}
impl<F, G> FunctionStrategy<F, G>
where
F: Fn(&[u8]) -> Vec<u8>,
G: Fn(&[u8]) -> Vec<u8>,
{
pub fn new(compress_fn: F, decompress_fn: G) -> Self {
FunctionStrategy { compress_fn, decompress_fn }
}
pub fn compress(&self, data: &[u8]) -> Vec<u8> {
(self.compress_fn)(data)
}
pub fn decompress(&self, data: &[u8]) -> Vec<u8> {
(self.decompress_fn)(data)
}
}
/// Payment processing strategies
pub trait PaymentStrategy {
fn pay(&self, amount: f64) -> Result<String, String>;
fn name(&self) -> &str;
}
#[derive(Debug)]
pub struct CreditCardPayment {
card_number: String,
expiry: String,
}
impl CreditCardPayment {
pub fn new(card_number: &str, expiry: &str) -> Self {
CreditCardPayment {
card_number: card_number.to_string(),
expiry: expiry.to_string(),
}
}
}
impl PaymentStrategy for CreditCardPayment {
fn pay(&self, amount: f64) -> Result<String, String> {
// Simulate payment processing
if self.card_number.len() < 13 {
return Err("Invalid card number".to_string());
}
Ok(format!(
"Paid ${:.2} with credit card ending in {}",
amount,
&self.card_number[self.card_number.len() - 4..]
))
}
fn name(&self) -> &str {
"Credit Card"
}
}
#[derive(Debug)]
pub struct PayPalPayment {
email: String,
}
impl PayPalPayment {
pub fn new(email: &str) -> Self {
PayPalPayment { email: email.to_string() }
}
}
impl PaymentStrategy for PayPalPayment {
fn pay(&self, amount: f64) -> Result<String, String> {
Ok(format!("Paid ${:.2} via PayPal ({})", amount, self.email))
}
fn name(&self) -> &str {
"PayPal"
}
}
#[derive(Debug)]
pub struct CryptoPayment {
wallet_address: String,
}
impl CryptoPayment {
pub fn new(wallet_address: &str) -> Self {
CryptoPayment {
wallet_address: wallet_address.to_string(),
}
}
}
impl PaymentStrategy for CryptoPayment {
fn pay(&self, amount: f64) -> Result<String, String> {
Ok(format!(
"Paid ${:.2} in crypto to {}",
amount,
&self.wallet_address[..10]
))
}
fn name(&self) -> &str {
"Cryptocurrency"
}
}
/// Checkout that uses payment strategy
pub struct Checkout {
items: Vec<(String, f64)>,
payment_strategy: Option<Box<dyn PaymentStrategy>>,
}
impl Checkout {
pub fn new() -> Self {
Checkout {
items: Vec::new(),
payment_strategy: None,
}
}
pub fn add_item(&mut self, name: &str, price: f64) {
self.items.push((name.to_string(), price));
}
pub fn set_payment_method(&mut self, strategy: Box<dyn PaymentStrategy>) {
self.payment_strategy = Some(strategy);
}
pub fn total(&self) -> f64 {
self.items.iter().map(|(_, price)| price).sum()
}
pub fn complete_purchase(&self) -> Result<String, String> {
let strategy = self.payment_strategy.as_ref()
.ok_or("No payment method selected")?;
let total = self.total();
println!("Processing ${:.2} via {}", total, strategy.name());
strategy.pay(total)
}
}
impl Default for Checkout {
fn default() -> Self {
Self::new()
}
}
/// Strategy selection at runtime
pub fn select_compression(name: &str) -> Box<dyn CompressionStrategy> {
match name {
"rle" => Box::new(RleCompression),
"none" | _ => Box::new(NoCompression),
}
}
fn main() {
// Compression strategies
let data = b"AAAAAABBBCCCCCCCC";
let mut compressor = FileCompressor::new(Box::new(NoCompression));
let no_compress = compressor.compress_file(data);
println!("No compression: {} bytes", no_compress.len());
compressor.set_strategy(Box::new(RleCompression));
let rle_compress = compressor.compress_file(data);
println!("RLE compression: {} bytes", rle_compress.len());
let decompressed = compressor.decompress_file(&rle_compress);
assert_eq!(data.as_slice(), decompressed.as_slice());
println!("Decompression verified!");
// Generic compressor (monomorphized, no vtable)
let generic = GenericCompressor::new(RleCompression);
let compressed = generic.compress(data);
println!("Generic RLE: {} bytes", compressed.len());
// Functional strategy
let func_strategy = FunctionStrategy::new(
|data| data.to_vec(), // identity compress
|data| data.to_vec(), // identity decompress
);
let result = func_strategy.compress(data);
println!("Functional strategy: {} bytes", result.len());
// Payment strategies
println!("\n=== Payment Strategies ===");
let mut checkout = Checkout::new();
checkout.add_item("Rust Book", 49.99);
checkout.add_item("Coffee", 4.50);
// Pay with credit card
checkout.set_payment_method(Box::new(
CreditCardPayment::new("4111111111111111", "12/25")
));
println!("{}", checkout.complete_purchase().unwrap());
// Switch to PayPal
checkout.set_payment_method(Box::new(
PayPalPayment::new("user@example.com")
));
println!("{}", checkout.complete_purchase().unwrap());
// Runtime strategy selection
let strategy = select_compression("rle");
println!("\nSelected strategy: {}", strategy.name());
}
Box: Runtime polymorphism when needed| Pattern | Purpose | Rust Implementation |
|---------|---------|---------------------|
| Strategy | Algorithm selection | Trait + implementations |
| State | Behavior based on state | Enum or trait objects |
| Template Method | Algorithm skeleton | Trait with default methods |
| Command | Encapsulate action | Fn trait or command struct |
// DON'T: Strategy that does too much
trait BadStrategy {
fn process(&self, data: &[u8]) -> Vec<u8>;
fn validate(&self, data: &[u8]) -> bool;
fn log(&self, message: &str);
fn connect_to_database(&self); // Too many responsibilities!
}
// DON'T: Enum when extensibility is needed
enum ClosedStrategy {
A, B, C, // Can't add new strategies without modifying this enum
}
// DO: Open for extension
trait OpenStrategy {
fn execute(&self);
}
// Anyone can implement new strategies
GzipCompression strategy (using flate2 crate)Run this code in the official Rust Playground