Decorator Design Pattern in Rust

1. Definition

The Decorator design pattern allows you to attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality, by wrapping objects inside objects with extended features.

2. Problem Statement

Suppose you're building a system to manage beverages. Over time, the number of beverage variations and their customizations (like condiments) grows. Rather than creating individual subclasses for each possible beverage combination, how can you dynamically add features to objects?

3. Solution

The Decorator pattern allows you to wrap objects with objects that augment their functionalities. You can thus compose various features by stacking decorators on top of a base object.

4. Real-World Use Cases

1. Graphical User Interface (GUI) toolkits where individual graphical components can be decorated with features like borders, shadows, or scrollbars.

2. Middleware layers in web frameworks.

3. Enhancing streams in I/O operations with features like buffering, encryption, or compression.

5. Implementation Steps

1. Define a common interface for both core objects and decorators.

2. Create concrete core objects that implement the interface.

3. Create decorator classes that also implement the interface, and wrap core or other decorator objects.

6. Implementation in Rust Programming

// Step 1: Common interface for core and decorators
pub trait Beverage {
    fn cost(&self) -> f32;
    fn description(&self) -> String;
}
// Step 2: Concrete core object
pub struct Coffee;
impl Beverage for Coffee {
    fn cost(&self) -> f32 {
        2.0
    }
    fn description(&self) -> String {
        "Coffee".to_string()
    }
}
// Step 3: Decorator classes
pub struct Milk<'a> {
    beverage: &'a dyn Beverage,
}
impl<'a> Milk<'a> {
    pub fn new(beverage: &'a dyn Beverage) -> Self {
        Milk { beverage }
    }
}
impl<'a> Beverage for Milk<'a> {
    fn cost(&self) -> f32 {
        self.beverage.cost() + 0.5
    }
    fn description(&self) -> String {
        format!("{} with Milk", self.beverage.description())
    }
}
pub struct Sugar<'a> {
    beverage: &'a dyn Beverage,
}
impl<'a> Sugar<'a> {
    pub fn new(beverage: &'a dyn Beverage) -> Self {
        Sugar { beverage }
    }
}
impl<'a> Beverage for Sugar<'a> {
    fn cost(&self) -> f32 {
        self.beverage.cost() + 0.25
    }
    fn description(&self) -> String {
        format!("{} with Sugar", self.beverage.description())
    }
}
// Client code
fn main() {
    let my_coffee = Coffee;
    let my_coffee_with_milk = Milk::new(&my_coffee);
    let my_coffee_with_milk_and_sugar = Sugar::new(&my_coffee_with_milk);
    println!("{}", my_coffee_with_milk_and_sugar.description());
    println!("Cost: ${}", my_coffee_with_milk_and_sugar.cost());
}

Output:

"Coffee with Milk with Sugar"
"Cost: $2.75"

Explanation:

1. The Beverage trait defines a common interface for both core and decorators.

2. Coffee is the core object implementing the Beverage trait.

3. Milk and Sugar are decorators. They wrap around a Beverage and extend its functionalities.

4. In the client code, we created a coffee and decorated it with milk and sugar. When we ask for the description and cost, the decorators augment the base coffee's properties.

7. When to use?

The Decorator pattern is useful when:

1. You need to add responsibilities to individual objects dynamically and transparently, without affecting other objects.

2. Extending functionalities by subclassing is impractical due to the explosion of subclasses.

3. You want to keep new functionalities separated and easily removable.


Comments