composite pattern - Java - Explained

composite pattern - Java - Explained

Intent

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

The Composite Design Pattern is a structural design pattern that allows you to compose objects into tree structures and then work with these structures as if they were individual objects.

Motivation behind the pattern/problem it solves?

This design pattern resolves difficulties and allows you to design objects in such a way that you can use that object as a composition of objects as well as single individual object.

This pattern solves the challenges faced when creating hierarchical tree structures to provide clients with a uniform way to access and manipulate objects in the tree.

design problem scenario 1 - strategy pattern

Problem Statement :

Suppose we are developing an e-commerce application where we have to implement the checkout process. The checkout process involves multiple steps such as adding items to the cart, applying discounts, selecting payment methods, and finally placing the order. Each of these steps can have different implementation details and can be subject to change in the future or can have specific conditions before execution. Our task is to design a flexible and extensible solution that can handle these changes and allow us to easily add new steps to the checkout process.

Solution

UML for composite design pattern

Participants

Component: Implement default behaviour which is common to all simple and complex components.
Leaf: It's a leaf object in the composition which does not have children.
Composite: It has client components, and it defines the behaviour of components having children.
Client: Manipulates objects in the composition through the component interface.

code example

[github link]

// simple component interface
public interface CheckoutStep {
    void perform();
}

// component with behavior
public class CartProductsValidationStep implements CheckoutStep {

    @Override
    public void perform() {
        // Implementation details for adding items to cart
        System.out.println("Performing some validationa on the products added 
        to the cart");
    }
}

// component with behavior
public class DiscountsStep implements CheckoutStep {

    @Override
    public void perform() {
        // Implementation details for discounts on items to cart
        System.out.println("Performing promotions and discounts step");
    }
}

// component with behavior
public class ShippingAddressStep implements CheckoutStep {

    @Override
    public void perform() {
        // Implementation details for shipping address step
        System.out.println("Performing shipping address step");
    }
}


// component with behavior
public class BillingAddressStep implements CheckoutStep {

    @Override
    public void perform() {
        // Implementation details for billing address step
        System.out.println("Performing billing address step");
    }
}

// component with behavior
public class PaymentStep implements CheckoutStep {

    @Override
    public void perform() {
        // Implementation details for payment step
        System.out.println("Performing payment step");
    }
}

// component with behavior
public class PlaceOrderStep implements CheckoutStep {

    @Override
    public void perform() {
        // Implementation details for adding items to cart
        System.out.println("PlaceOrder step");
    }
}

// composite class with has childrens and also has the behaviour to use the childrens
public class CheckoutCompositeStep implements CheckoutStep {
    private List<CheckoutStep> steps = new ArrayList<>();

    public void addStep(CheckoutStep step) {
        steps.add(step);
    }

    public void removeStep(CheckoutStep step) {
        steps.remove(step);
    }

    @Override
    public void perform() {
        for (CheckoutStep step : steps) {
            step.perform();
        }
    }
}

// main client class
public class CompositePatternMain {
    public static void main(String[] args) {
        CheckoutCompositeStep checkoutProcess = new CheckoutCompositeStep();
        checkoutProcess.addStep(new CartProductsValidationStep());
        checkoutProcess.addStep(new DiscountsStep());
        checkoutProcess.addStep(new ShippingAddressStep());
        checkoutProcess.addStep(new BillingAddressStep());
        checkoutProcess.addStep(new PaymentStep());
        checkoutProcess.addStep(new PlaceOrderStep());

        System.out.println("Executing checkout steps");
        checkoutProcess.perform();
    }
}


//OUTPUT
Executing checkout steps
Performing some validationa on the products added to the cart
Performing promotions and discounts step
Performing shipping address step
Performing billing address step
Performing payment step
PlaceOrder step

In this example, we have defined the CheckoutStep interface that defines the perform() method for each step of the checkout process.
We have implemented six concrete classes that represent the leaf nodes of the hierarchy: CartProductsValidationStep, DiscountsStep, ShippingAddressStep, BillingAddressStep, PaymentStep and PlaceOrderStep.
We have also implemented the CheckoutCompositeStep class that represents the composite node of the hierarchy. It has a list of child nodes and implements the perform() method by calling the perform() method of each child node.

We can use the CheckoutCompositeStep class to define a hierarchy of checkout steps and treat the entire hierarchy as a single object. In the usage example, we have created a CheckoutCompositeStep instance and added the leaf nodes to it.
We can then call the perform() method of the CheckoutCompositeStep instance to perform the entire checkout process, including all the child nodes.

If we need to add a new step in the future, we can simply create a new leaf node class or a new composite node class and add it to the hierarchy.

Pros and Cons

Pros

  1. Flexibility: The composite pattern makes it easy to add new types of components to the hierarchy without having to change the existing code.

  2. Simplified client code: Clients can treat individual objects and groups of objects in the same way, which simplifies the code and makes it easier to maintain.

  3. Improved scalability: The composite pattern allows you to build complex hierarchical structures that can be easily scaled and modified as needed.

  4. Reduced coupling: The pattern reduces the coupling between the client and the objects in the hierarchy, making it easier to modify and maintain the code.

Cons

  1. Increased complexity: The pattern can make the code more complex and harder to understand, especially when dealing with deep and complex hierarchies.

  2. Performance overhead: The pattern can result in some performance overhead, especially when dealing with large hierarchies, due to the extra layers of abstraction and indirection.

  3. Limited functionality: The pattern is best suited for hierarchies of similar objects, and may not be suitable for other types of structures.

Relation with other patterns

  1. 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.

  2. You can use Iterators to traverse Composite trees.

  3. Chain of Responsibility is often used in conjunction with Composite. In this case, when a leaf component gets a request, it may pass it through the chain of all of the parent components down to the root of the object tree.

Difference between composite vs decorator pattern

The composite pattern is used to represent part-whole hierarchies. It allows you to treat individual objects and groups of objects in the same way. In the composite pattern, a single object can be composed of one or more objects of the same type. The composite pattern provides a way to treat groups of objects in the same way as individual objects.

The decorator pattern is used to add functionality to an existing object without changing its structure. The decorator pattern allows you to add behaviour to objects dynamically at runtime, without modifying the underlying object. The decorator pattern involves wrapping an object with one or more decorators to add new functionality to the object.

Some key differences between the composite and decorator design patterns in Java:

  1. Purpose: The composite pattern is used to represent part-whole hierarchies, while the decorator pattern is used to add functionality to an existing object.

  2. Structure: In the composite pattern, a single object can be composed of one or more objects of the same type, while in the decorator pattern, an object is wrapped with one or more decorators.

  3. Functionality: The composite pattern allows you to treat groups of objects in the same way as individual objects, while the decorator pattern allows you to add new functionality to an object.

  4. Complexity: The composite pattern can make the code more complex and harder to understand, especially when dealing with deep and complex hierarchies. The decorator pattern can also add complexity to the code, but it is usually easier to understand than the composite pattern.