The Decorator pattern: an easy way to add functionality
Photo by Thomas balabaud: https://www.pexels.com/photo/framed-photo-lot-1579708/
The Decorator pattern can be used to dynamically alter or add functionality to existing classes.
This pattern is often more efficient than subclassing because it relieves us of the need of defining a new object. So, what does it look like?
To decorate a certain class the following steps must be taken:
- Construct a subclass, or in our case an implementation of the interface you want to decorate.
- In the Decorator class, make sure you add a reference to the original class.
- Also in constructor of the Decorator class, make sure to pass a reference to the original class to the constructor.
- Where needed, forward all requests to methods in the original class.
- And where needed, change the behaviour of the rest.
This all works because both the Decorator and the original class implement the same interface.
Implementation in Rust We will start with the Component trait:
trait Component {
fn operation(&self) -> String;
}
For the sake of simplicity this component only has one method. Now we need to implement this trait:
struct ConcreteComponent;
impl Component for ConcreteComponent {
fn operation(&self) -> String {
"ConcreteComponent".to_string()
}
}
Again, very simple, the operation() method just returns a fixed string. Now it is time to define a BorderDecorator struct with its implementation:
struct BorderDecorator {
component: Box<dyn Component>,
}
impl Component for BorderDecorator {
fn operation(&self) -> String {
format!("[{}]", self.component.operation())
}
}
impl BorderDecorator {
fn new<T: Component + 'static>(component: T) -> Self {
BorderDecorator {
component: Box::new(component)
}
}
}
A few notes:
- As mentioned in the introduction, a decorator wraps the object it wants to decorate.
- We use a generic constructor which can accept any type T which implements the Component trait. Using the <T:Component +'static> syntax we establish a so called trait bound meaning the type T must satisfy the two requirements, i.e. implementing the Component trait and having a 'static lifetime. The 'static lifetime ensures that the type does not contain any borrowed references which could become invalid.
- The decorated class operation() method is called within the Decorator’s operation() method.
Finally we will define a ScrollDecorator struct, along the same lines:
struct ScrollDecorator {
component: Box<dyn Component>,
}
impl Component for ScrollDecorator {
fn operation(&self) -> String {
format!("~{}~", self.component.operation())
}
}
impl ScrollDecorator {
fn new<T: Component + 'static>(component: T) -> Self {
ScrollDecorator {
component: Box::new(component)
}
}
}
Note that the two decorators both implement the Component trait, which means they can be chained as we shall see in the tests. Testing time And now, a simple test:
fn main() {
let component = ConcreteComponent;
let decorated = BorderDecorator::new(component);
let double_decorated = ScrollDecorator::new(decorated);
println!("{}", double_decorated.operation());
}
A breakdown, line by line:
- We construct a ConcreteComponent
- We wrap this in a BorderDecorator.
- Because the BorderDecorator implements the Component trait, we can wrap the decorated component in a ScrollDecorator
- Now we do not call the operation() method on the ConcreteComponent but on the decorated component, made possible by the fact that both implement the same trait.
Conclusion
The Decorator pattern offers a flexible and powerful way to extend object functionality without relying on rigid inheritance hierarchies. By using a wrapper class that implements the same interface as the decorated object, you can add new behaviors dynamically at runtime.
As demonstrated with the Rust example, this approach allows you to chain decorators together, such as applying both a BorderDecorator and a ScrollDecorator to a component.
This promotes a more modular and reusable codebase, as you can mix and match different functionalities as needed, avoiding the complexities of creating countless subclasses for every possible combination.
Read the full article here: https://medium.com/rustaceans/the-decorator-pattern-an-easy-way-to-add-functionality-code-nomad-bce49d233fee