Decorator pattern - Java - Explained

Decorator pattern - Java - Explained

The Decorator Pattern attaches additional responsibilities to an object dynamically by wrapping it with other object.

Intent

The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorator provides a flexible alternative to subclassing for extending functionality.

It is a type of structural design pattern which is also called a Wrapper pattern.
The decorator attaches new behavior to an object by placing these objects into a wrapper that contains new behavior.

Motivation behind the pattern/problem it solves?

  1. To save us from the explosion of subclasses
    This means when we are in a design decision where we need to create many subclasses and it's not a wise decision to create many subclasses here we can think about Decorator Pattern because subclassing is done at compile-time and Decorator use composition which is being executed at runtime.

  2. When we want to add responsibilities to an object dynamically and transparently, that is without affecting other objects then think about using Decorator Pattern.

  3. Use the Decorator pattern when you need to be able to assign extra behaviors to objects at runtime by giving justice to the open/close principle.

  4. Use when it's not possible to extend an object behavior using Inheritance.

Design Problem :

Design a solution where we have multiple types of ice creme each varying based on toppings we add to the base ice creme.

Basic Icecreme = $ 0.60
Chocolate toppings = + $1
Mint toppings = + $1.50
Nuts toppings = + $1

A table below shows the different possible combination of ice creme

image.png

There are high chances that we'll have more toppings in the future also we may have the new base variant. A solution must be scalable and extendible.

Solution

As with three toppings and one base variant we have 7 combinations. Consider if we have 4 base variants and 10 different toppings how many combinations we ll have ? a LOT.
This is a case of subclass explosion. It's not practical here to create many subclasses.
This kind of design problem can be tackled using the Decorator Pattern where we wrap the object with other objects to provide additional behavior.

Solution design structure

image.png

Click on the name here to open the new tab DecoratorPatternStructure.jpg

Participants

Component: defines the interface for objects that can have responsibilities added to them dynamically.
It defines a common interface for the wrapper and wrapped objects
ConcreteComponent : defines basic behavior. It's an object which can be altered using decorators
Decorator: maintains a reference to a component object(wrapped object) and a base decorator for concrete decorators to implement.
ConcreteDecorator: adds responsibilities to the component dynamically at the runtime.
Client: can wrap multiple components in hierarchies where each component can be wrapped by another.

code (complete code github link)

### Interfaces 
public abstract class IceCream {

    public String description = "unknown description";

    public String getDescription() {
        return  description;
    }
    public abstract double cost();
}

public abstract class IceCreamDecorator extends IceCream {
    public abstract String getDescription();
}

### Implementation classes 
## basic icecreme
public class BasicIceCream extends IceCream {

    public BasicIceCream() {
        description = "IceCreme-Cone_With-basic-icecreme";
    }

    @Override
    public double cost() {
        return 0.60;
    }
}
## toppings 
public class ChocolateAddon extends IceCreamDecorator {

    IceCream iceCream;

    public ChocolateAddon(IceCream iceCream) {
        this.iceCream = iceCream;
    }

    @Override
    public String getDescription() {
        return iceCream.getDescription() + " + ChocolateAddon";
    }

    @Override
    public double cost() {
        return 1.0 + iceCream.cost();
    }

}

##
public class MintAddon extends IceCreamDecorator {
    IceCream iceCream;

    public MintAddon(IceCream iceCream) {
        this.iceCream = iceCream;
    }

    @Override
    public String getDescription() {
        return iceCream.getDescription()+" + Mint-addon";
    }

    @Override public double cost() {
        return 1.50 + iceCream.cost();
    }
}

## 
public class NutsAddon extends IceCreamDecorator {

    IceCream iceCream;

    public NutsAddon(IceCream iceCream) {
        this.iceCream = iceCream;
    }

    @Override
    public String getDescription() {
        return iceCream.getDescription() + " + Nuts addon";
    }

    @Override
    public double cost() {
        return 1.0 + iceCream.cost();
    }
}

## Main client class
public class DecoratorPatternMain {
    public static void main(String[] args) {
        IceCream basicIceCreme = new BasicIceCream();
        System.out.println(basicIceCreme.getDescription()+ " cost is $"+basicIceCreme.cost());

        // preparing icecreme with chocolate addon
        IceCream iceCreme2 = new BasicIceCream();
        iceCreme2 = new ChocolateAddon(iceCreme2);
        System.out.println(iceCreme2.getDescription()+ " $"+iceCreme2.cost());

        // preparing icecreme with chocolate addon and Mint  and Nuts addone
        IceCream iceCreme3 = new BasicIceCream();
        iceCreme3 = new ChocolateAddon(iceCreme3);
        iceCreme3 = new MintAddon(iceCreme3);
        iceCreme3 = new NutsAddon(iceCreme3);
        System.out.println(iceCreme3.getDescription()+ " $"+iceCreme3.cost());

    }
}

## Output 
IceCreme-Cone_With-basic-icecreme cost is $0.6
IceCreme-Cone_With-basic-icecreme + ChocolateAddon $1.6
IceCreme-Cone_With-basic-icecreme + ChocolateAddon + Mint-addon + Nuts addon $4.1

Pros and Cons

Pros

  1. We can extend an object's behavior without making a new subclass.
  2. We can combine several behaviors by wrapping an object into multiple decorators.
  3. Achieve open/close principle with dynamically additional responsibility.
  4. recursive composition is possible.
  5. It stays true to the Open-Closed principle . open -close principle : classes should be open for extension but close for modification.

Cons

  1. Hard to remove a specific wrapper once it's wrapped.

Relation with other patterns

  1. Adaptor vs Decorator
    The Adapter changes the interface of an existing object, while the Decorator enhances an object without changing its interface. In addition, Decorator supports recursive composition, which isn’t possible when you use Adapter.

  2. Chain Of Responsibilities Vs Decorator
    Both have a very similar structure, both rely on recursive composition to pass the execution through a series of objects, the difference is, the decorators are not allowed to break the flow in between however CoR handlers can stop passing the request further at any point.

  3. Composite Vs Decorator
    A Decorator is like a Composite but only has one child component. There’s another significant difference: Decorator adds additional responsibilities to the wrapped object, while Composite just “sums up” its children’s results. However, the patterns can also cooperate: you can use a Decorator to extend the behavior of a specific object in the Composite tree.

  4. Decorator Vs Strategy
    Decorator changes the skin of the object however strategy use the actual object and changes its state and behaviors.

  5. Decorator Vs Proxy
    Decorator and Proxy have similar structures, but very different intents. Both patterns are built on the composition principle, where one object is supposed to delegate some of the work to another. The difference is that a Proxy usually manages the life cycle of its service object on its own, whereas the composition of Decorators is always controlled by the client.

Some good references for decorator pattern blogs.oracle.com/javamagazine/post/the-deco..