In this lesson, you will learn how to augment existing functionality by wrapping types in "decorators" that implement the same trait, allowing you to add features dynamically without touching original code.
The Problem of Rigid Hierarchies
Often, you might have a struct that performs a specific task—like a Logger that writes messages to a file. If you suddenly need that same logger to also encrypt data or timestamp entries, modifying the original Logger struct violates the Single Responsibility Principle. Instead, we use the Decorator Pattern.
Wrapping Behavior with Traits
In Rust, the Decorator pattern is implemented by creating a new struct that wraps an existing object that implements a common trait. Because the wrapper also implements that same trait, it can stand in for the original object while adding its own logic before or after delegating to the wrapped instance.
Consider this example:
trait DataSource {
fn write(&self, data: &str);
}
struct FileLogger;
impl DataSource for FileLogger {
fn write(&self, data: &str) { println!("Writing to file: {}", data); }
}
// The Decorator
struct TimestampedLogger<T: DataSource> {
inner: T,
}
impl<T: DataSource> DataSource for TimestampedLogger<T> {
fn write(&self, data: &str) {
print!("[{}] ", "2023-10-27"); // Added feature
self.inner.write(data); // Delegation
}
}
Why this is powerful
- Composition over Inheritance: You can stack multiple decorators (e.g.,
Encryption(Timestamp(Logger))) to create complex behavior from simple, interchangeable parts. - Zero Modification: The
FileLoggerremains pristine. You haven't added a single line of code to it, yet it now has the ability to be timestamped. - Type Safety: Because the wrapper takes the trait as a generic parameter (
T: DataSource), you maintain strict type safety while allowing the compiler to optimize these layers away at compile-time via monomorphization.
By nesting these wrappers, you build a "pipeline" of behavior where each layer does one small thing and passes the work along to the next, adhering perfectly to Rust’s philosophy of compositional design.
Key Takeaway: The Decorator pattern uses trait-based wrapping to layer functionality, allowing you to extend an object's capabilities at compile-time without modifying its source code.