Design pattern "Decorator"

Let's learn what is the "Decorator" design pattern πŸͺ†β˜•β˜•β˜•
Tuesday, July 9, 2024

Real world example

Consider the scenario where we own a coffee shop. We offer a variety of coffee blends to our patrons:

Loading graph...

Now, we aim to provide the option of personalizing our coffee.

Without design pattern

(Bad) solution 1

One approach to this could be to establish a separate class for each type of coffee.

However, adopting such a strategy could lead to an exponential increase in the number of classes.

Loading graph...

Indeed, we’re facing a combinatorial explosion.

Envision a scenario where we want to introduce additional elements to the coffee: the class count would skyrocket exponentially.

(Bad) solution 2

Let’s adopt a more pragmatic approach and incorporate properties instead.

Loading graph...

This approach also has its shortcomings:

  • If the cost of ingredients fluctuates, we would need to adjust the code accordingly.
  • Should we decide to incorporate additional ingredients, we would have to introduce new methods and alter the calcPrice() method.
  • If we consider introducing new beverage types (such as IceTea), some ingredients may not be suitable.
  • What if a customer requests a double serving of chocolate in their drink? This scenario isn’t accounted for in the current setup.

In plain words

Decorator pattern lets you dynamically change the behavior of an object at run time by wrapping them in an object of a decorator class.

Wikipedia definition

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern.

Programmatic example

interface Coffee {
public double getCost();
public String getDescription();
}
class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 2.0; // Base cost of a simple coffee
}
@Override
public String getDescription() {
return "Simple Coffee";
}
}
// Add decorators
abstract class CoffeeDecorator implements Coffee {
abstract public double getCost();
abstract public String getDescription();
}
class MilkDecorator extends CoffeeDecorator {
private final Coffee coffee;
public MilkDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 1.0; // Additional cost for milk
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
}
class ChocolateDecorator extends CoffeeDecorator {
private final Coffee coffee;
public ChocolateDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 0.5; // Additional cost for chocolate
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Chocolate";
}
}
public class CoffeeShop {
public static void main(String[] args) {
// Create a simple coffee
Coffee simpleCoffee = new SimpleCoffee();
System.out.println(simpleCoffee.getDescription() + " - Cost: $" + simpleCoffee.getCost());
// Simple Coffee - Cost: $2.0
// Add milk to the coffee
Coffee milkCoffee = new MilkDecorator(new SimpleCoffee());
System.out.println(milkCoffee.getDescription() + " - Cost: $" + milkCoffee.getCost());
// Simple Coffee, Milk - Cost: $3.0
// Add sugar to the coffee
Coffee sugarCoffee = new ChocolateDecorator(new SimpleCoffee());
System.out.println(sugarCoffee.getDescription() + " - Cost: $" + sugarCoffee.getCost());
// Simple Coffee, Chocolate - Cost: $2.5
// Add both milk and sugar
Coffee milkAndSugarCoffee = new ChocolateDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(milkAndSugarCoffee.getDescription() + " - Cost: $" + milkAndSugarCoffee.getCost());
// Simple Coffee, Milk, Chocolate - Cost: $3.5
}
}

Diagram

Loading graph...

Some other examples

  • Coffee Shop Application: In a coffee shop system, you can use the Decorator pattern to compute the cost of beverages with different ingredients. Instead of creating a subclass for each possible combination of ingredients, you can create a decorator for each type of ingredient and dynamically add these decorators to the beverage object to calculate the total cost.
  • File Encryption Application: The Decorator pattern can be used in a file encryption application. You can have a basic file writer object and then use decorators to add additional features like encryption, compression, and buffering.
  • GUI Toolkits: In graphical user interface (GUI) toolkits, the Decorator pattern can be used to add behaviors to individual UI components without affecting other components. For example, you can add scrolling to a window without modifying the window itself.
  • Java I/O Classes: The Java I/O classes use the Decorator pattern. The InputStream, OutputStream, Reader, and Writer classes are all abstract base components, while the concrete components and decorators are subclasses that provide additional functionality like buffering and formatting.
  • Middleware in Web Frameworks: In web frameworks like Express.js, middleware functions, which have access to the request and response objects, can be seen as decorators. They can add headers, format the response, handle errors, etc.
  • Python Flask Web Framework: In Python’s Flask web framework, decorators are used for route handlers. This allows for the dynamic addition of functionality to routes.
  • Caching and Logging: Decorators can be used to add caching and logging functionality to applications. This allows you to store the results of expensive function calls and return the cached result when the same inputs occur.
  • Access Control and Authentication: In web development, decorators can be used to restrict access to certain parts of an application based on user roles.
  • Transaction Management: In database operations, decorators can be used to manage transactions, allowing you to encapsulate commit and rollback operations.
  • Decorating Messages: By implementing the Decorator design pattern, you can develop an application for processing a message through layers, where each layer will process it on different levels. For example, an object can be converted into XML, then it’s going to be encapsulated into a SOAP message which will then be encrypted.

Recommended articles