decorator design pattern

What is decorator design pattern and its advantages in java

The Decorator Pattern is a structural 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. It is achieved by creating a set of decorator classes that are used to wrap concrete components. Decorator classes mirror the type of the components they decorate, but they add or override behavior.

The Decorator design pattern is used to dynamically add or alter the behavior of objects without changing their code. In this example, let’s consider a Car interface with an assemble method. We’ll have a BasicCar class that implements the Car interface, and then we’ll create CarDecorator as a decorator class. Finally, we’ll have concrete decorator classes LuxuryCar and SportCar that inherit from CarDecorator.

decorator design pattern

Example# 1:

Here’s the Java implementation:

Example
// Car interface
interface Car {
    void assemble();
}

// Concrete component: BasicCar
class BasicCar implements Car {
    @Override
    public void assemble() {
        System.out.println("Basic Car");
    }
}

// Decorator class: CarDecorator
class CarDecorator implements Car {
    protected Car car;

    public CarDecorator(Car car) {
        this.car = car;
    }

    @Override
    public void assemble() {
        car.assemble();
    }
}

// Concrete decorator: LuxuryCar
class LuxuryCar extends CarDecorator {
    public LuxuryCar(Car car) {
        super(car);
    }

    @Override
    public void assemble() {
        super.assemble();
        System.out.println("Adding Luxury Features");
    }
}

// Concrete decorator: SportCar
class SportCar extends CarDecorator {
    public SportCar(Car car) {
        super(car);
    }

    @Override
    public void assemble() {
        super.assemble();
        System.out.println("Adding Sport Features");
    }
}

// Client code
public class DecoratorPatternExample {
    public static void main(String[] args) {
        // Creating a basic car
        Car basicCar = new BasicCar();

        // Decorating the basic car with luxury features
        Car luxuryCar = new LuxuryCar(basicCar);
        luxuryCar.assemble();

        System.out.println();

        // Decorating the basic car with sport features
        Car sportCar = new SportCar(basicCar);
        sportCar.assemble();

        System.out.println();

        // Decorating the basic car with both luxury and sport features
        Car luxurySportCar = new SportCar(new LuxuryCar(basicCar));
        luxurySportCar.assemble();
    }
}

In this example:

  • The Car interface defines the assemble method.
  • The BasicCar class is a concrete component that implements the Car interface.
  • The CarDecorator class is an abstract decorator class that also implements the Car interface and contains a reference to a Car object.
  • The LuxuryCar and SportCar classes are concrete decorator classes that extend CarDecorator and add specific features to the assemble method.

The client code demonstrates how to create a basic car and then decorate it with luxury features, sport features, or a combination of both. The decorators can be combined in various ways to create different combinations of features dynamically.

Example# 2:

Let’s create a simple example using a Coffee class and decorators to add different condiments.

1. Component Interface:

Example
// Component interface
interface Coffee {
    double cost();
    String description();
}

2. Concrete Component:

Example
// Concrete Component
class SimpleCoffee implements Coffee {
    @Override
    public double cost() {
        return 2.0;
    }

    @Override
    public String description() {
        return "Simple Coffee";
    }
}

3. Decorator Classes:

Example
// Decorator abstract class
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost();
    }

    @Override
    public String description() {
        return decoratedCoffee.description();
    }
}

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public double cost() {
        return super.cost() + 0.5;
    }

    @Override
    public String description() {
        return super.description() + ", with Milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public double cost() {
        return super.cost() + 0.2;
    }

    @Override
    public String description() {
        return super.description() + ", with Sugar";
    }
}

4. Client Code:

Example
// Client code
public class DecoratorPatternExample {
    public static void main(String[] args) {
        // Use the Decorator Pattern

        // Create a simple coffee
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println("Cost: $" + simpleCoffee.cost() + ", Description: " + simpleCoffee.description());

        // Decorate the coffee with milk
        Coffee milkCoffee = new MilkDecorator(simpleCoffee);
        System.out.println("Cost: $" + milkCoffee.cost() + ", Description: " + milkCoffee.description());

        // Decorate the coffee with sugar
        Coffee sugarCoffee = new SugarDecorator(simpleCoffee);
        System.out.println("Cost: $" + sugarCoffee.cost() + ", Description: " + sugarCoffee.description());

        // Decorate the coffee with both milk and sugar
        Coffee milkSugarCoffee = new SugarDecorator(new MilkDecorator(simpleCoffee));
        System.out.println("Cost: $" + milkSugarCoffee.cost() + ", Description: " + milkSugarCoffee.description());
    }
}

In this example:

  • The Coffee interface is the component interface that defines the basic behavior of a coffee.
  • The SimpleCoffee class is a concrete component representing a simple coffee.
  • The CoffeeDecorator abstract class is a decorator that extends the Coffee interface and contains a reference to the decorated coffee.
  • The MilkDecorator and SugarDecorator classes are concrete decorators that add the cost and description of milk and sugar, respectively.
  • In the DecoratorPatternExample, we create a simple coffee and then decorate it with various condiments (milk, sugar, or both), showing how decorators can be combined to modify the behavior of the original component.

This example demonstrates how the Decorator Pattern allows us to add new functionalities to objects by creating a series of decorator classes that wrap the original object. Each decorator contributes its own behavior, and the client code can combine decorators in different ways.


Advantages of the Decorator Design Pattern in Java

1. Open/Closed Principle: The Decorator pattern follows the Open/Closed Principle, allowing you to introduce new functionality by extending classes without modifying existing code. This promotes code extensibility.

2. Flexibility and Dynamism: Decorators can be added or removed at runtime, providing a flexible and dynamic way to enhance the behavior of objects. This dynamic behavior allows for more versatility in object composition.

3. Code Reusability: Decorators can be reused to wrap different components, promoting code reusability. The same decorators can be applied to various objects to provide different combinations of features.

4. Maintainability: Changes to individual components or decorators have minimal impact on the rest of the system. This makes the codebase more maintainable, as modifications are localized to specific classes.

5. Separation of Concerns: The Decorator pattern separates concerns by having a clear distinction between the core functionality and the additional features provided by decorators. This separation simplifies the understanding and maintenance of the code.

6. Consistent Interface: Decorators and components share a common interface, ensuring that clients can treat both in a uniform way. This consistent interface simplifies client code and promotes a clear understanding of how objects can be extended.

In summary, while the Decorator pattern provides numerous advantages such as flexibility, maintainability, and extensibility, it also comes with some drawbacks related to complexity, potential overuse, and performance considerations. It is essential to carefully evaluate whether the Decorator pattern is the right choice based on the specific requirements of the application.

Leave a Reply

Your email address will not be published. Required fields are marked *