Understand in Seconds - Decorator Pattern

Decorator Pattern

A structural design pattern. It can dynamically add some extra functionalities to an object (this sentence is the core). When it comes to extending functionality, it is more flexible than creating subclasses.

Easy-to-Understand Example

Preface

Suppose we have a simple Coffee class, which represents a cup of coffee, and has a getCost() method to calculate the price of the coffee.

Now, if we want to add additional extension functions (such as milk coffee, sugar coffee, etc.).

If we don't want to modify the code of the Coffee class, we can use the Decorator Pattern in this scenario.

1. First, define the Coffee interface and its implementation class SimpleCoffee

// Coffee interface
interface Coffee {
    double getCost();
}

// SimpleCoffee class, implements the Coffee interface
class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 2.5; // Assume the price of a basic cup of coffee is 2.5 yuan
}
}

*Note: This completes the functionality of a basic cup of coffee

2. Define the abstract class CoffeeDecorator, which implements the Coffee interface and holds a reference to a Coffee object

// CoffeeDecorator abstract class
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
    this.coffee = coffee;
}

@Override
public double getCost() {
    return coffee.getCost(); // By default, return the price of the decorated coffee
}

}

*Note: This is the core idea of the decorator pattern: combine and extend by obtaining the target coffee object via construction

3.1. Create concrete decorator classes, for example: milk coffee

// MilkCoffee class, extends CoffeeDecorator
class MilkCoffee extends CoffeeDecorator {
    public MilkCoffee(Coffee coffee) {
        super(coffee);
    }
@Override
public double getCost() {
    return super.getCost() + 0.5; // Add $0.5 to the original price (milk surcharge)
}

public String getDescription() {
    return "with milk";
}

}

*Note: Inherit the abstract decorator class, and extend functionality based on the incoming coffee instance

3.2. Create concrete decorator classes, for example: sugar coffee

// SugarCoffee class, extends CoffeeDecorator
class SugarCoffee extends CoffeeDecorator {
    public SugarCoffee(Coffee coffee) {
        super(coffee);
    }
@Override
public double getCost() {
    return super.getCost() + 0.3; // Add $0.3 to the original price (sugar surcharge)
}

public String getDescription() {
    return "with sugar";
}

}

4. Combine decorators to create various new coffee products

public class CoffeeShop {
    public static void main(String[] args) {
        // Create a basic coffee object (a cup of black coffee)
        Coffee coffee = new SimpleCoffee();
        System.out.println("Basic coffee price: $" + coffee.getCost());
    // Add milk using decorator (new milk coffee ready)
    Coffee milkCoffee = new MilkCoffee(coffee);
    System.out.println(milkCoffee.getDescription() + " coffee price: $" + milkCoffee.getCost());

    // Add sugar on top of the milk coffee (new sugar and milk coffee ready)
    Coffee milkSugarCoffee = new SugarCoffee(milkCoffee);
    System.out.println(milkSugarCoffee.getDescription() + " and " + milkSugarCoffee.getDescription() + " coffee price: $" + milkSugarCoffee.getCost());
}

}

Question: The Role of the CoffeeDecorator Class

  1. Code reuse and reduced redundancy: If all decorators directly implement the Coffee interface, they will all need to repeatedly implement some common methods or properties. By creating a decorator abstract class, you can place common methods or properties in this abstract class, thereby reducing code redundancy and improving code maintainability.
  2. Simplify interaction between decorators: If decorators need to collaborate with each other or access each other's state, inheriting a common decorator abstract class makes it easier to implement these interactions. The abstract class can provide some protected members or methods for subclasses to access and use.
  3. Type checking and safety: By inheriting the decorator abstract class, you can perform type checking at compile time to ensure that all decorators follow a predetermined convention or behavior. Additionally, using an abstract class can prevent clients from incorrectly passing inappropriate objects to the decorator constructor, as the compiler will require the passed object to be an instance of the decorator abstract class or its subclasses.
  4. Extensibility and maintainability: As the system evolves, you may need to add new decorators or modify existing ones. If all decorators directly implement the interface, you will need to modify all decorator implementations when you need to modify or add new common methods. If you use a decorator abstract class, you only need to modify it once in the abstract class, and all subclasses will inherit this change.
  5. Design pattern consistency: The Decorator Pattern is typically used with abstract classes, as this aligns with the core idea of the pattern: dynamically adding additional responsibilities to an object. Inheriting a common abstract class allows you to more clearly express the concept of "adding responsibilities".

Thank you for reading. I will share more design pattern knowledge later. Although many bloggers have covered this before, I have my own approach: keep it simple and easy to understand, haha.


This is a discussion topic separated from the original post at https://juejin.cn/post/7368692841297641526