Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 53

Dynamic Object Enhancement with Decorator and Composite

Recommended resources

Hello! Welcome back to our series on design patterns.

In our last lesson, we started our journey into Structural Patterns with Adapter and Facade. You learned how these patterns act as "wrappers" to solve distinct problems: Adapter for interface incompatibility and Facade for simplifying complex subsystems. This "wrapper" concept is a powerful tool in an architect's toolkit.

Today, we'll explore two more fundamental structural patterns that also utilize a form of wrapping: Decorator and Composite. While they can look surprisingly similar in a class diagram, their intent is completely different. Mastering this distinction is crucial for developing the architectural mindset you're aiming for.

Our goal for this lesson is to implement the Decorator and Composite structural patterns to enhance object responsibilities dynamically. You will learn how to use Decorator to add features to objects at runtime and how to use Composite to build and manage tree-like object structures.

1. The Decorator Pattern: Adding Responsibilities Dynamically

Imagine you have a class, and you need to add extra functionality to it. The most straightforward object-oriented approach is subclassing. However, what if you need many different combinations of functionalities? If you have a Coffee class and want to add milk, sugar, caramel, and whipped cream, creating a subclass for every possible combination (CoffeeWithMilk, CoffeeWithMilkAndSugar, CoffeeWithCaramel, etc.) would lead to a "class explosion." It's rigid and unmanageable.

The Decorator pattern solves this by letting you attach new behaviors to objects dynamically by placing them inside special "wrapper" objects.

What is the Decorator Pattern? (Software Design Patterns)

To see this problem and its solution in action, let's watch a video from Be A Better Dev. It uses a coffee shop example to perfectly illustrate why inheritance can be problematic and how the Decorator pattern provides a flexible, compositional alternative.

Please watch from 03:11 to 19:27. Pay close attention to: The explanation of why an inheritance-based solution for adding condiments to coffee is inflexible (03:11 - 07:17). The class diagram for the Decorator pattern solution and how it uses abstract classes and composition (07:17 - 13:38). The Java code walkthrough, especially how each decorator holds a reference to the component it wraps and adds its own cost/description (13:38 - 17:27). The final demonstration showing how objects are composed at runtime (17:27 - 19:27).

How it Works: The Structure of a Decorator

As the video demonstrated, the Decorator pattern is all about creating a stack of objects, where each layer adds some functionality. The client interacts with the outermost layer, and the calls are delegated down the stack to the original object, with each decorator adding its behavior either before or after the delegation.

Let's formalize the structure.

UML Class Diagram of the Decorator Design Pattern
A standard UML diagram for the Decorator pattern. Notice how both `ConcreteComponent` and the `Decorator` implement the same `IComponent` interface, making them interchangeable from a client's perspective. The `Decorator` holds a reference to another `IComponent`, which can be either the original object or another decorator.

The key participants are:

  • Component: An interface or abstract class defining the methods that will be implemented by both the concrete components and the decorators. (e.g., ICoffee with a getCost() and getDescription() method).
  • Concrete Component: The base object we want to dynamically add behavior to. (e.g., Espresso).
  • Decorator: An abstract class that also implements the Component interface. It holds a reference (wraps) a Component object. It delegates calls to the wrapped component.
  • Concrete Decorator: The classes that contain the additional responsibilities. They extend the Decorator and override methods to add their own logic before or after delegating the call to the wrapped component. (e.g., WithMilk, WithSugar).

Decorator

For a step-by-step guide and an alternative example, this article from Refactoring.guru is an excellent resource. It also provides clear guidance on when and how to implement the pattern.

Please read the 'Structure', 'Applicability', and 'How to Implement' sections. The implementation steps provide a clear, language-agnostic recipe for building a decorator.

Test your understanding!

Imagine you have a FileDataSource class with writeData(data) and readData() methods. You need to be able to add compression and encryption to your data source objects in any combination at runtime. How would you apply the Decorator pattern?

Show answer
  1. Component: Create a DataSource interface with writeData(data) and readData() methods.
  2. Concrete Component: Your FileDataSource class would implement the DataSource interface.
  3. Decorator: Create an abstract DataSourceDecorator that implements DataSource and holds a reference to another DataSource object. Its methods would simply delegate calls to the wrapped object.
  4. Concrete Decorators:
    • Create a CompressionDecorator. Its writeData method would compress the data before calling the wrapped object's writeData. Its readData method would decompress the data after getting it from the wrapped object.
    • Create an EncryptionDecorator. Its writeData method would encrypt the data before calling writeData on the wrapped object. Its readData would decrypt the data after the call.

You could then create a decorated data source like this (in pseudocode):
dataSource = new FileDataSource()
dataSource = new CompressionDecorator(dataSource)
dataSource = new EncryptionDecorator(dataSource)

The client code would just call dataSource.writeData(...), unaware of the layers of functionality.


2. The Composite Pattern: Building Part-Whole Hierarchies

Now, let's switch gears to the Composite pattern. Its goal is to compose objects into tree structures to represent part-whole hierarchies and let clients treat individual objects and compositions of objects uniformly.

Think of a file system. A directory can contain files, but it can also contain other directories. Or a graphical user interface, where a Panel can contain Buttons and TextFields, but it can also contain other Panels.

In both cases, you want to perform operations on the entire structure. For example, you might want to print() the contents of a directory, which means printing its name and then recursively printing the contents of everything inside it. The Composite pattern allows you to do this seamlessly.

Composite Design Pattern Overview
This diagram shows the core idea of the Composite pattern. Both the individual 'Book' (a Leaf) and the 'BookGroup' (a Composite) share a common 'BookComponent' interface. This allows a client to treat them uniformly, simplifying the code needed to handle complex hierarchical structures.

How it Works: The Structure of a Composite

The magic of the Composite pattern lies in having a common interface that both individual objects (leaves) and group objects (composites) share.

The key participants are:

  • Component: An interface or abstract class that defines common operations for all objects in the composition, both leaves and composites. (e.g., Graphic with a draw() method, or FileSystemItem with a getSize() method).
  • Leaf: Represents the individual, indivisible objects in the composition. It implements the Component interface but has no children. (e.g., Circle, File).
  • Composite: Represents the objects that can have children. It also implements the Component interface. It stores a collection of child Component objects. Its implementation of the Component methods typically iterates over its children and delegates the calls to them, often aggregating the results. (e.g., Panel, Directory).

Explained Design Patterns: Decorator and Composite

This article on Medium provides a concise overview of the Composite pattern, including a practical e-commerce example that should resonate with your web development background.

Please read the sections on the Composite Pattern, starting from the 'Definition' heading through the end of the Python implementation. Focus on: The definition and the part-whole hierarchy concept. The roles of Component, Leaf, and Composite. The e-commerce example with Product (Leaf) and Category (Composite). Pay attention to the Python implementation to see how it's built.


3. Decorator vs. Composite: A Crucial Distinction

At this point, you might have noticed a strong structural similarity. Both patterns use a common interface and composition. In a UML diagram, they can look almost identical. So, what's the real difference? It's all about intent.

  • Decorator is for adding or overriding an object's behavior. Its focus is on adding responsibilities. It typically wraps only one other component.
  • Composite is for structuring a hierarchy and treating a group of objects as a single object. Its focus is on structure and aggregation. It can have many children.

Let's watch a video that dives deep into this comparison. It's a bit more theoretical but is excellent for building the architectural intuition needed to choose the right pattern.

Difference Between Composite and Decorator Pattern – Design Patterns (ep 15)

In this video, Christopher Okhravi dissects the nuances and differences between the Decorator and Composite patterns, moving beyond surface-level similarities.

This is a dense video, so focus on these key segments: Intent (01:10 - 01:55): Listen for the core purpose of each pattern (hierarchical structure vs. altering behavior at runtime). UML Comparison (06:05 - 11:23): Observe the side-by-side drawing of the UML diagrams. Note the similarity and the subtle differences he introduces, like the extra ConcreteDecorator layer. Key Subjective Difference (17:24 - 18:49): This is a critical point. He argues that a Decorator wraps a single component, while a Composite aggregates multiple components. Conclusion (28:40 - 31:13): This summary recaps the main idea: use Composite for tree structures, and Decorator for attaching dynamic behavior.

To summarize the key distinctions:

Aspect Decorator Composite
Intent To add responsibilities to an object dynamically. To represent part-whole hierarchies of objects.
Goal Enhance or alter an object's behavior. Structure objects and treat them uniformly.
Composition Wraps a single component. Composes multiple child components.
Operation Augments the wrapped object's operation (adds to it). Synthesizes its operation from the operations of its children (sums up).
Analogy Putting on clothes (sweater, jacket, raincoat) to add layers of function. An army's chain of command (a general commands colonels, who command majors...).

Conclusion

You've now added two powerful and closely related structural patterns to your skillset. Understanding them individually is important, but understanding when to use one over the other is what elevates a developer to an architect.

Key Takeaways:

  • The Decorator pattern provides a flexible alternative to subclassing for extending functionality. It allows you to add behavior to an object at runtime by wrapping it in one or more decorator objects.
  • The Composite pattern allows you to compose objects into tree structures and then work with these structures as if they were individual objects. It simplifies client code by providing a uniform interface for both individual elements (leaves) and containers (composites).
  • The primary difference lies in their intent: Decorator is about adding behavior, while Composite is about creating structure.

Preview of the Next Lesson:
In our next lesson, we will put everything you've learned about design patterns so far into practice. You will be tasked to select and justify an appropriate design pattern for a given design problem. This will challenge you to analyze a problem and use your architectural judgment to choose the best tool for the job from the creational and structural patterns we've covered.