A decorator is a class that extends the functionality of another class. A decorator usually implements the same interface so that a decorated object can be used instead of the basic one. A good example is a compressor and/or encrypter applied over a file or, more generally, over a data stream implementation, as shown in the answer by @nits.kk.
In a case of pizza, we should define what behavior we need:
public interface Pizza {
public String getIngredients(); // comma separated
public double getTotalPrice();
}
A pizza consists of two main types of ingredients: a mandatory single base and optional multiple toppings. Each ingredient has its own price.
public class PizzaIngredient {
private double getPrice() {
return 0.0;
}
}
The pizza base is itself a simplest possible pizza, so it must implement the Pizza
interface. It has a size as its attribute (and the price, of course). We could implement a size as a separate class, but I don't consider it reasonable - it's not general enough to be useful outside the pizza universe and not complex enough to deserve its own interface.
public class PizzaBase extends PizzaIngredient implements Pizza {
public PizzaBase(String size) {
this.size = size;
}
public String getIngredients() {
return size + " base"; // the only ingredient is this base
}
public double getTotalPrice() {
return getPrice(); // the base-only pizza costs the base cost
}
private double getPrice() {
if(size == "small")
return 2.0;
if(size == "medium")
return 2.5;
return 3.0; // large and undefined
}
private final String size;
}
Now we need toppings. They wil be added on top of pizza as decorators: a pizza plus a topping is a pizza, too, so the topmost topping will represent the whole composition. Such pizza's ingredients list is a list of ingredients of the underlying pizza plus the name of its topmost topping. Similarly the total price.
public class PizzaTopping extends PizzaIngredient implements Pizza {
public PizzaTopping(String name, Pizza pizza) {
this.name = name;
this.pizza = pizza;
}
public String getIngredients() {
return pizza.getIngredients() + ", " + getName();
}
public double getTotalPrice() {
return pizza.getTotalPrice() + getPrice();
}
public String getName() {
return name;
}
private final String name;
private final Pizza pizza;
}
Let's define some concrete toppings:
public class MozzarellaTopping extends PizzaTopping {
public MozzarellaTopping(Pizza pizza) {
super("mozzarella", pizza);
}
private double getPrice() {
return 0.5;
}
}
public class MushroomTopping extends PizzaTopping {
public MushroomTopping(Pizza pizza) {
super("mushroom", pizza);
}
private double getPrice() {
return 2.0;
}
}
public class PepperoniTopping extends PizzaTopping {
public PepperoniTopping(Pizza pizza) {
super("pepperoni", pizza);
}
private double getPrice() {
return 1.5;
}
}
public class GreenOliveTopping extends PizzaTopping {
public GreenOliveTopping(Pizza pizza) {
super("green olive", pizza);
}
private double getPrice() {
return 1.2;
}
}
Okay, that's lots of classes; but which of them and when shall we need?
Here a Factory joins the team. A factory is a class to create objects of some classes. It's used to hide the creation details behind the scenes, especially when the objects created are complicated or are of different concrete classes. When resulting objects are created as standalone entities, the factory class can be just a namespace with a static method in it. OTOH, if objects are created in some context, the factory could be an object associated with the context (for example, parametrized) and using that context in creation process.
We may use a factory to create pizza ingredients at will, according to user input. Most ingredients are toppings, which must be applied on top of already existing pizza, so let's pass the pizza to a factory to receive the decorated pizza as a result. The special case is creating a pizza base, which is not applied on another pizza; in this case the pizza
parameter is ignored, so we may pass null
.
public class PizzaFactory {
public static Pizza getPizza(Pizza pizza, String name)
{
if ( name.equals("small") || name.equals("medium") || name.equals("large") )
return new PizzaBase(name);
else if ( name.equals("mozzarella") )
return new MozzarellaTopping(pizza); // add topping to the pizza
else if ( name.equals("mushroom") )
return new MushroomTopping(pizza);
else if ( name.equals("pepperoni") )
return new PepperoniTopping(pizza);
else if ( name.equals("green olive") )
return new GreenOliveTopping(pizza);
return null;
}
}
Now we're ready to build our pizza.
class PizzaTest {
public static void main(String[] args) {
DecimalFormat priceFormat = new DecimalFormat("#.##");
Pizza pizza;
pizza = PizzaFactory.getPizza(null, "small");
System.out.println("The small pizza is: " + pizza.getIngredients());
System.out.println("It costs " + priceFormat.format(pizza.getTotalCost()));
pizza = PizzaFactory.getPizza(null, "medium");
pizza = PizzaFactory.getPizza(pizza, "mozzarella");
pizza = PizzaFactory.getPizza(pizza, "green olive");
System.out.println("The medium pizza is: " + pizza.getIngredients());
System.out.println("It costs " + priceFormat.format(pizza.getTotalCost()));
String largePizzaOrder[] = { "large", "mozzarella", "pepperoni",
"mushroom", "mozzarella", "green olive" };
pizza = null;
for (String cmd : largePizzaOrder)
pizza = PizzaFactory.getPizza(pizza, cmd);
System.out.println("The large pizza is: " + pizza.getIngredients());
System.out.println("It costs " + priceFormat.format(pizza.getTotalCost()));
}
}
Warning: there are some pitfalls and shortcuts in the above code.
The most important is the lack of validation of input: when an unexpected command arrives, the factory will return null
which will cause a crash on future use of getIngredients()
or getTotalCost()
.
Another one is hard-coding prices into concrete classes. An actual solution would have to use some price list and fetch prices either on an ingredient creation (and store fetched prices in the ingredient objects) or on use, i.e. in the getCost()
method (which would require some access to the price list from pizzas' ingredients).