Decorator Pattern — A Quick Guide
Design Patterns: Decorator Pattern
--
“Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”
Aside from formal definition, Decorator Pattern wraps around an object of a Decorator class, allowing you to dynamically change the behavior of an object at client code.
Simple Example:
Suppose you own a local pizza shop.
Of course you have menus with fixed prices, but you also have a “have-it-your-way” style pizza that allows customers to add whatever ingredient they want.
You calculated the costs of them all by yourself with a piece of paper and a pen, but now your business is growing larger and you can’t catch up anymore.
It’s time to build a software for it! :)
So, let’s make a Pizza superclass with subclasses such as PizzaWithMushrooms, PizzaWithCorn, PizzaWithSalami, PizzaWithMushroomsAndCorn, PizzaWithMushroomsAndSalami, PizzaWithCornAndSalami, PizzaWithMushroomsAndCornAndSalami.
Oops. That’s not a very good practice, is it?
What if we want to add a new ingredient in the future? What if we want to add a new crust type, like Thin Crust Pizza? What if we want to update prices of each ingredient? What if we want to add x2 more corn?
In our case, this practice would result in a maintenance nightmare.
Our goal must be to allow our classes to add new behaviors, without modifying existing code. (Open-Closed Principle)
Written code should be reusable.
To achieve this, we can implement Decorator Pattern such as:
public interface Pizza {
String getSummary();
int getPrice();
}
pizza classes that implements the Pizza interface
public class StandardCrustPizza implements Pizza private static final String PRICE = 25;
@Override
public String getSummary() {
return "Standard Pizza (" + PRICE + ")";
}
@Override
public int getPrice() {
return PRICE;
}
}public class ThinCrustPizza implements Pizza { private static final String PRICE = 30;
@Override
public String getSummary() {
return "Thin Crust Pizza (" + PRICE + ")";
}
@Override
public int getPrice() {
return PRICE;
}
}
pizza decorator abstract class that implements the Pizza interface
abstract class PizzaDecorator implements Pizza {
@Override
public abstract String getSummary();
@Override
public abstract int getPrice();
}
pizza ingredients that extends PizzaDecorator abstract class
public class Corn extends PizzaDecorator {
private Pizza pizza;
private static final String PRICE = 4;
Corn(Pizza pizza) {
this.pizza = pizza;
}
@Override
public String getSummary() {
return pizza.getSummary() +
", with corn (" + PRICE + ")";
}
@Override
public int getPrice() {
return pizza.getPrice() + PRICE;
}
}public class Mushrooms extends PizzaDecorator {
private Pizza pizza;
private static final String PRICE = 6;
Mushrooms(Pizza pizza) {
this.pizza = pizza;
}
@Override
public String getSummary() {
return pizza.getSummary() +
", with mushrooms (" + PRICE + ")";
}
@Override
public int getPrice() {
return pizza.getPrice() + PRICE;
}
}public class Salami extends PizzaDecorator {
private Pizza pizza;
private static final String PRICE = 5;
Salami(Pizza pizza) {
this.pizza = pizza;
}
@Override
public String getSummary() {
return pizza.getSummary() +
", with salami (" + PRICE + ")";
}
@Override
public int getPrice() {
return pizza.getPrice() + PRICE;
}
}
And the client code would be like this:
Pizza pizza = new StandardPizza(); // standard pizza, base: 25
pizza = new Salami(pizza); // salami added, total: 30
pizza = new Corn(pizza); // corn added, total: 34
pizza = new Corn(pizza); // corn x 2 portion added, total: 38Output:
Standard Pizza (25), with salami (5), with corn (4), with corn (4)
Price: 38
Pizza pizza = new ThinCrustPizza(); // thin crust pizza, base: 30
pizza = new Mushroom(pizza); // mushroom added, total: 36
pizza = new Salami(pizza); // salami added, total: 41Output:
Thin Crust Pizza (30), with mushroom (6), with salami (5)
Price: 41
See, it’s all very reusable! If one day you want to add a new crust/ingredient or update the prices of current ones, you won’t need to touch the client code.
Each class will only care of its own. (Single Responsibility Principle)
Real-Life Example:
Decorator Pattern is widely used in java.io package, such as:
InputStream in = new FileInputStream(file);
in = new BufferedInputStream(in);
in = new DataInputStream(in);
FileInputStream is the crust type here, while BufferedInputStream and DataInputStream are the ingredients.
You can even write your own decorator to your program by simply extending the java.io package decorators’ superclass (FilterInputStream) and wrap it around the base class FileInputStream.
public class UpperCaseInputStream extends FilterInputStream {
public UpperCaseInputStream(InputStream in) {
super(in);
} @Override
public int read() throws IOException {
return Character.toUpperCase(super.read());
}
}
And the client code would be like this:
InputStream in = new FileInputStream(file);
in = new UpperCaseInputStream(in);// other decorators
In short, Decorator Pattern helps us to dynamically attach additional functionalities to our base class as we write code.
Thank you for reading and being a part of my journey!
Reference(s)
- Head First Design Patterns. by Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Released October 2004. Publisher(s): O’Reilly Media, Inc.