Decorator Pattern
The Decorator design pattern is a structural design pattern that allows you to add behavior or responsibilities to objects dynamically, without altering their code. It is one of the Gang of Four design patterns and is used to achieve flexibility in object composition by allowing behavior to be added or removed from individual objects, rather than modifying the entire class hierarchy.
This pattern is particularly useful when you have a set of objects with similar functionalities, but you want to provide a way to extend or modify their behavior in a flexible manner.
It provides an alternative to subclassing for extending functionality, allowing behavior to be added or overridden at runtime.
Structure
The key components of the Decorator pattern are:
Component: This is the base interface or abstract class that defines the common interface for all concrete components and decorators.
Concrete Component: This is the basic implementation of the Component interface. It defines the core behavior that decorators can augment.
Decorator: This is an abstract class or interface that extends the Component interface and holds a reference to a Component object. It has the same interface as the Component, allowing it to wrap around concrete components and add additional behavior.
Concrete Decorator: These are classes that extend the Decorator class and provide concrete implementations of additional behaviors. They can add new methods, fields, or modify the behavior of the wrapped Component.
Examples
Breverage Ordering System
Let’s take the example of a beverage ordering system. We have a base component Beverage
with two concrete components: Coffee
and Tea
.
classDiagram class Beverage { <> Beverage +getDescription() : String +cost() : double } class Coffee { +getDescription() : String +cost() : double } class Tea { +getDescription() : String +cost() : double } Beverage <|.. Coffee Beverage <|.. Tea
We want to add decorators for adding additional options like milk, sugar, and whipped cream.
classDiagram class BeverageDecorator { -beverage : Beverage +BeverageDecorator(beverage: Beverage) +getDescription() : String } class MilkDecorator { +MilkDecorator(beverage: Beverage) +getDescription() : String +cost() : double } class SugarDecorator { +SugarDecorator(beverage: Beverage) +getDescription() : String +cost() : double } class Beverage { <> Beverage +getDescription() : String +cost() : double } BeverageDecorator <|-- MilkDecorator BeverageDecorator <|-- SugarDecorator BeverageDecorator o-- Beverage
- Component (Beverage):
- Concrete Components (Coffee and Tea):
- Decorator (BeverageDecorator):
- Concrete Decorators (MilkDecorator, SugarDecorator):
Usage:
In this example, the Decorator pattern allowed us to dynamically add milk and sugar as decorators to the coffee object, altering its description and cost without changing the original Coffee class.
Pizzas and Toppings
classDiagram class Pizza { <> +getDescription() : String +cost() : double } class MargheritaPizza { +getDescription() : String +cost() : double } class PepperoniPizza { +getDescription() : String +cost() : double } class PizzaDecorator { -pizza : Pizza +PizzaDecorator(pizza: Pizza) +getDescription() : String } class CheeseTopping { +CheeseTopping(pizza: Pizza) +getDescription() : String +cost() : double } class MushroomTopping { +MushroomTopping(pizza: Pizza) +getDescription() : String +cost() : double } class JalapenoTopping { +JalapenoTopping(pizza: Pizza) +getDescription() : String +cost() : double } Pizza <|.. MargheritaPizza Pizza <|.. PepperoniPizza PizzaDecorator <|-- CheeseTopping PizzaDecorator <|-- MushroomTopping PizzaDecorator <|-- JalapenoTopping PizzaDecorator o-- Pizza
Implementation
1. Base Pizza Component:
This represents the basic pizza. It will be our base component, and we’ll add toppings to it using decorators.
2. Concrete Pizza Types:
We’ll have two types of pizzas: MargheritaPizza
and PepperoniPizza
.
3. Toppings (Decorators):
Now, let’s define some toppings that can be added to the pizzas.
Usage:
Now let’s create some pizzas and add various toppings to them.
Benefits of the Decorator Pattern
Flexible extension: You can add or remove responsibilities from objects at runtime, making it easy to create different combinations of behaviors.
Open-closed principle: You can extend the behavior of objects without modifying their source code, promoting the open-closed principle of software design.
Single Responsibility Principle: The pattern allows for separation of concerns by breaking down functionalities into small, focused classes.
Drawbacks of the Decorator Pattern
Complexity: The pattern can lead to a large number of small classes if not carefully managed, which might be difficult to understand and maintain.
Order of wrapping: The order in which decorators are added can impact the final behavior, and this might be confusing in some scenarios.
In conclusion, the Decorator design pattern is a powerful tool for enhancing the functionality of individual objects while maintaining a clear separation of concerns and adhering to the principles of object-oriented design.