Decorator Design Pattern

The Decorator design pattern can be beneficial when the features are mutually independent, and applying each feature to an object results in the same type of object. In this pattern, applying features in any order produces consistent results or objects. The primary advantage is a reduction in the number of classes needed to create objects with various combinations of features.

To apply the decorator pattern, identify features such as f1, f2, f3, ..., fi, ..., fn. If adding feature fi to an object still results in an object of the same type (i.e., obj + fi remains obj), then it is a feasible candidate for decoration.

Key considerations for the Decorator design pattern:

  1. Abstract Class: Ensure that all distinct classes extend the abstract class.

  2. Abstract Decorator Class: Make sure that all distinct decorator classes extend the abstract decorator class.

  3. Abstract Decorator Class Inheritance: Ensure that the abstract decorator class inherits from the previously created abstract class.

Example:

Let's take the popular example of pizza. In this scenario, various pizza varieties are identified, such as FarmHousePizza, VegDelight, and Margherita. These three pizzas exhibit significant differences, and as such, they do not share a common set of features. Therefore, they belong to three distinct categories.

Key Considerations:

  1. We have chosen to use an abstract class because the calculation of the cost of a pizza may vary depending on the specific type of pizza.

  2. All the pizza variants must extend the same abstract class to ensure consistency and maintainability in our pizza ordering system.

Base

package Base;

public abstract class BasePizza {
    public abstract int cost();
}

public class FarmHousePizza extends BasePizza {
    @Override
    public int cost() {
        return 200;
    }
}

public class VegDelight extends BasePizza {
    @Override
    public int cost() {
        return 120;
    }
}

public class Margherita extends BasePizza {
    @Override
    public int cost() {
        return 100;
    }
}

Features

Next, we have the features ExtraCheese and Mushroom.These features are applicable to pizzas of any type and can be added in any sequence, whether it's Mushroom first and then Extra Cheese, or vice versa.

Abstract Decorator class extends from Abstract Object

package Decorator;

import Base.BasePizza;

public abstract class ToppingsDecorator extends BasePizza {
}


public class ExtraCheese extends ToppingsDecorator{

    private BasePizza pizza;

    public ExtraCheese(BasePizza pizza){
        this.pizza = pizza;
    }

    @Override
    public int cost() {
        return pizza.cost() + 10;
    }
}

public class Mushroom extends ToppingsDecorator{

    private BasePizza pizza;

    public Mushroom(BasePizza pizza) {
        this.pizza = pizza;
    }

    @Override
    public int cost() {
        return pizza.cost()+20;
    }
}

Usage

import Base.BasePizza;
import Base.FarmHousePizza;
import Decorator.ExtraCheese;
import Decorator.Mushroom;

public class MakePizza {

    public static void main(String[] args) {
        BasePizza pizza = new FarmHousePizza();
        pizza = new Mushroom(new ExtraCheese(pizza));
        System.out.println(pizza.cost());
    }
}

Conclusion: When the features are independent of each other, the decorator pattern becomes the preferred choice. It's important to note that you should create two abstract classes, and the feature abstract class should always inherit from the base abstract class.