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
.
Example# 1:
Here’s the Java implementation:
// 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 theassemble
method. - The
BasicCar
class is a concrete component that implements theCar
interface. - The
CarDecorator
class is an abstract decorator class that also implements theCar
interface and contains a reference to aCar
object. - The
LuxuryCar
andSportCar
classes are concrete decorator classes that extendCarDecorator
and add specific features to theassemble
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:
// Component interface
interface Coffee {
double cost();
String description();
}
2. Concrete Component:
// Concrete Component
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 2.0;
}
@Override
public String description() {
return "Simple Coffee";
}
}
3. Decorator Classes:
// 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:
// 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 theCoffee
interface and contains a reference to the decorated coffee. - The
MilkDecorator
andSugarDecorator
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.