Home/Design Patterns in Rust/Decorator Pattern

Decorator Pattern

Runtime behavior extension

advanced
decoratorstructuralcomposition
🎮 Interactive Playground

What is the Decorator Pattern?

The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality. In Rust, we implement decorators using composition and trait delegation.

The Problem

Decorators are useful when:

  • Cross-cutting concerns: Logging, caching, authentication
  • Runtime composition: Adding features without subclassing
  • Layered functionality: Middleware, filters, transformers
  • Optional features: Enable/disable functionality dynamically

Example Code

use std::time::{Duration, Instant};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

/// Base trait for a data source
pub trait DataSource {
    fn read(&self, key: &str) -> Option<String>;
    fn write(&mut self, key: &str, value: &str);
}

/// Concrete implementation: in-memory storage
#[derive(Default)]
pub struct InMemoryDataSource {
    data: HashMap<String, String>,
}

impl InMemoryDataSource {
    pub fn new() -> Self {
        Self::default()
    }
}

impl DataSource for InMemoryDataSource {
    fn read(&self, key: &str) -> Option<String> {
        self.data.get(key).cloned()
    }

    fn write(&mut self, key: &str, value: &str) {
        self.data.insert(key.to_string(), value.to_string());
    }
}

/// Decorator: Logging wrapper
pub struct LoggingDecorator<D: DataSource> {
    inner: D,
    prefix: String,
}

impl<D: DataSource> LoggingDecorator<D> {
    pub fn new(inner: D, prefix: &str) -> Self {
        LoggingDecorator {
            inner,
            prefix: prefix.to_string(),
        }
    }
}

impl<D: DataSource> DataSource for LoggingDecorator<D> {
    fn read(&self, key: &str) -> Option<String> {
        println!("[{}] Reading key: {}", self.prefix, key);
        let result = self.inner.read(key);
        println!("[{}] Read result: {:?}", self.prefix, result);
        result
    }

    fn write(&mut self, key: &str, value: &str) {
        println!("[{}] Writing key: {} = {}", self.prefix, key, value);
        self.inner.write(key, value);
    }
}

/// Decorator: Caching layer
pub struct CachingDecorator<D: DataSource> {
    inner: D,
    cache: HashMap<String, (String, Instant)>,
    ttl: Duration,
}

impl<D: DataSource> CachingDecorator<D> {
    pub fn new(inner: D, ttl: Duration) -> Self {
        CachingDecorator {
            inner,
            cache: HashMap::new(),
            ttl,
        }
    }

    fn is_valid(&self, key: &str) -> bool {
        if let Some((_, cached_at)) = self.cache.get(key) {
            cached_at.elapsed() < self.ttl
        } else {
            false
        }
    }
}

impl<D: DataSource> DataSource for CachingDecorator<D> {
    fn read(&self, key: &str) -> Option<String> {
        // Check cache first
        if self.is_valid(key) {
            if let Some((value, _)) = self.cache.get(key) {
                println!("[Cache] HIT for key: {}", key);
                return Some(value.clone());
            }
        }

        println!("[Cache] MISS for key: {}", key);
        self.inner.read(key)
    }

    fn write(&mut self, key: &str, value: &str) {
        // Update cache
        self.cache.insert(key.to_string(), (value.to_string(), Instant::now()));
        // Write through to inner
        self.inner.write(key, value);
    }
}

/// Decorator: Encryption layer
pub struct EncryptionDecorator<D: DataSource> {
    inner: D,
    key: u8, // Simple XOR key for demonstration
}

impl<D: DataSource> EncryptionDecorator<D> {
    pub fn new(inner: D, key: u8) -> Self {
        EncryptionDecorator { inner, key }
    }

    fn encrypt(&self, data: &str) -> String {
        data.bytes().map(|b| (b ^ self.key) as char).collect()
    }

    fn decrypt(&self, data: &str) -> String {
        // XOR is symmetric
        self.encrypt(data)
    }
}

impl<D: DataSource> DataSource for EncryptionDecorator<D> {
    fn read(&self, key: &str) -> Option<String> {
        self.inner.read(key).map(|v| self.decrypt(&v))
    }

    fn write(&mut self, key: &str, value: &str) {
        let encrypted = self.encrypt(value);
        self.inner.write(key, &encrypted);
    }
}

/// Decorator: Retry wrapper
pub struct RetryDecorator<D: DataSource> {
    inner: D,
    max_retries: u32,
}

impl<D: DataSource> RetryDecorator<D> {
    pub fn new(inner: D, max_retries: u32) -> Self {
        RetryDecorator { inner, max_retries }
    }
}

impl<D: DataSource> DataSource for RetryDecorator<D> {
    fn read(&self, key: &str) -> Option<String> {
        for attempt in 0..=self.max_retries {
            if let Some(value) = self.inner.read(key) {
                return Some(value);
            }
            if attempt < self.max_retries {
                println!("[Retry] Attempt {} failed, retrying...", attempt + 1);
            }
        }
        None
    }

    fn write(&mut self, key: &str, value: &str) {
        self.inner.write(key, value);
    }
}

/// Function decorator using closures
pub fn with_timing<F, R>(name: &str, f: F) -> R
where
    F: FnOnce() -> R,
{
    let start = Instant::now();
    let result = f();
    println!("[{}] Completed in {:?}", name, start.elapsed());
    result
}

/// Trait-based decorator for HTTP handlers
pub trait HttpHandler {
    fn handle(&self, request: &HttpRequest) -> HttpResponse;
}

#[derive(Debug)]
pub struct HttpRequest {
    pub path: String,
    pub auth_token: Option<String>,
}

#[derive(Debug)]
pub struct HttpResponse {
    pub status: u16,
    pub body: String,
}

/// Concrete handler
pub struct ApiHandler;

impl HttpHandler for ApiHandler {
    fn handle(&self, request: &HttpRequest) -> HttpResponse {
        HttpResponse {
            status: 200,
            body: format!("Handled: {}", request.path),
        }
    }
}

/// Authentication decorator
pub struct AuthDecorator<H: HttpHandler> {
    inner: H,
    valid_token: String,
}

impl<H: HttpHandler> AuthDecorator<H> {
    pub fn new(inner: H, valid_token: &str) -> Self {
        AuthDecorator {
            inner,
            valid_token: valid_token.to_string(),
        }
    }
}

impl<H: HttpHandler> HttpHandler for AuthDecorator<H> {
    fn handle(&self, request: &HttpRequest) -> HttpResponse {
        match &request.auth_token {
            Some(token) if token == &self.valid_token => {
                self.inner.handle(request)
            }
            _ => HttpResponse {
                status: 401,
                body: "Unauthorized".to_string(),
            },
        }
    }
}

/// Timing decorator for HTTP
pub struct TimingDecorator<H: HttpHandler> {
    inner: H,
}

impl<H: HttpHandler> TimingDecorator<H> {
    pub fn new(inner: H) -> Self {
        TimingDecorator { inner }
    }
}

impl<H: HttpHandler> HttpHandler for TimingDecorator<H> {
    fn handle(&self, request: &HttpRequest) -> HttpResponse {
        let start = Instant::now();
        let response = self.inner.handle(request);
        println!("Request to {} took {:?}", request.path, start.elapsed());
        response
    }
}

/// Builder for composing decorators
pub struct DataSourceBuilder {
    source: Box<dyn DataSource>,
}

impl DataSourceBuilder {
    pub fn new() -> Self {
        DataSourceBuilder {
            source: Box::new(InMemoryDataSource::new()),
        }
    }

    pub fn with_logging(self, prefix: &str) -> Self {
        // Note: This is simplified; real implementation would need different approach
        // due to Box<dyn> not being directly wrappable
        self
    }

    pub fn build(self) -> Box<dyn DataSource> {
        self.source
    }
}

fn main() {
    // Basic decorator chain
    let storage = InMemoryDataSource::new();
    let mut logged = LoggingDecorator::new(storage, "DB");

    logged.write("user:1", "Alice");
    let _ = logged.read("user:1");

    println!("\n=== Stacked Decorators ===");

    // Stack multiple decorators
    let storage = InMemoryDataSource::new();
    let encrypted = EncryptionDecorator::new(storage, 42);
    let cached = CachingDecorator::new(encrypted, Duration::from_secs(60));
    let mut logged = LoggingDecorator::new(cached, "APP");

    logged.write("secret", "password123");
    let _ = logged.read("secret"); // Cache miss
    let _ = logged.read("secret"); // Cache hit

    println!("\n=== HTTP Handler Decorators ===");

    // HTTP handler with decorators
    let handler = ApiHandler;
    let authed = AuthDecorator::new(handler, "secret-token");
    let timed = TimingDecorator::new(authed);

    // Authorized request
    let request = HttpRequest {
        path: "/api/users".to_string(),
        auth_token: Some("secret-token".to_string()),
    };
    let response = timed.handle(&request);
    println!("Response: {:?}", response);

    // Unauthorized request
    let bad_request = HttpRequest {
        path: "/api/users".to_string(),
        auth_token: None,
    };
    let response = timed.handle(&bad_request);
    println!("Response: {:?}", response);

    println!("\n=== Function Decorator ===");

    let result = with_timing("compute", || {
        std::thread::sleep(Duration::from_millis(100));
        42
    });
    println!("Result: {}", result);
}

Why This Works

  1. Composition over inheritance: Wrap objects instead of extending classes
  2. Same interface: Decorator implements the same trait as decorated
  3. Transparent stacking: Multiple decorators can be chained
  4. Runtime flexibility: Add/remove decorators dynamically

Decorator Patterns in Rust

| Pattern | Implementation | Use Case |

|---------|----------------|----------|

| Struct Wrapper | struct Dec { inner: T } | Most common |

| Closure Wrapper | fn wrap(f: F) -> impl Fn() | Functions |

| Blanket Impl | impl DecTrait for T | Universal decoration |

| Newtype | struct Dec(Inner) | Simple transformations |

When to Use

  • Middleware: HTTP, message queue processors
  • Cross-cutting concerns: Logging, metrics, caching
  • Feature toggles: Enable/disable functionality
  • Testing: Wrap real services with mock behavior

⚠️ Anti-patterns

// DON'T: Decorator that changes semantics
impl<D: DataSource> DataSource for BadDecorator<D> {
    fn read(&self, key: &str) -> Option<String> {
        // Silently returns different data!
        Some("fake data".to_string())
    }
}

// DON'T: Deep nesting without type aliases
let d = LoggingDecorator::new(
    CachingDecorator::new(
        EncryptionDecorator::new(
            RetryDecorator::new(
                InMemoryDataSource::new(),
                3
            ),
            42
        ),
        Duration::from_secs(60)
    ),
    "DB"
);

// DO: Use type aliases or builders
type CachedEncryptedSource = CachingDecorator<EncryptionDecorator<InMemoryDataSource>>;

Real-World Usage

  • tower: Service middleware stack
  • tracing: Instrumentation decorators
  • std::io: BufReader, BufWriter decorators
  • reqwest: Request middleware

Exercises

  1. Implement a validation decorator that checks data before writing
  2. Create a metrics decorator that counts reads/writes
  3. Build a rate-limiting decorator
  4. Implement a circuit-breaker decorator that fails fast after errors

🎮 Try it Yourself

🎮

Decorator Pattern - Playground

Run this code in the official Rust Playground