Composite Pattern Overview
The Composite Pattern, also known as the Part-Whole pattern, is a structural design pattern that allows treating individual objects and compositions of objects uniformly. It achieves this by defining a common interface for both leaf nodes (individual objects) and composite nodes (containers that hold other objects), enabling clients to interact with all objects in the same way regardless of their specific type.
Composition vs. Aggregation
Understanding the difference between composition and aggregation is crucial when implementing the Composite Pattern:
- Composition: Represents a "has-a" relationship where the part and whole have the same lifecycle. If the whole object is destroyed, all its parts are also destroyed. They share a生死 (life-death) relationship.
- Aggregation: Represents a "contains-a" relationship where the parts can exist independently of the whole. For example, an aircraft carrier fleet contains various ships and aircraft, but these components can exist separately from the fleet.
Pattern Structure
The Composite Pattern organizes objects into a tree structure where:
- The topmost node is called the root
- Root nodes can contain branch nodes and leaf nodes
- Branch nodes can contain other branch nodes and leaf nodes
In this pattern, both root and branch node are essentially the same data type and can act as containers. Although leaf nodes are semantically different, they're treated as the same type through a common interface, allowing for consistent behavior across the entire tree structure.
UML Diagram Components
The Composite Pattern consists of three main components:
- Component (Abstract Root Node): Defines the common interface for all objects in the composition, including both leaf and composite objects. It may provide default implementations for some operations.
- Composite (Branch Node): Defines behavior for components with children, stores child components, and implements operations that manage child components.
- Leaf (Leaf Node): Represents leaf objects in the composition, which have no childran and are the end nodes of the structure.
Implementation Approaches
There are two primary ways to implement the Composite Pattern:
Transparent Composite Pattern
In this approach, all common methods are defined in the Component interface. This allows clients to treat all objects uniformly without needing to distinguish between leaf and composite nodes. However, this approach violates the Interface Segregasion Principle because leaf nodes inherit methods they don't need (those related to managing children).
Safe Composite Pattern
This approach defines only the most basic common behaviors in the Component interface, while placing composite-specific methods (like adding or removing children) in the Composite class itself. This follows the Single Responsibility Principle and Interface Segregation Principle but requires clients to differentiate between leaf and composite nodes, violating the Dependency Inversion Principle.
Application Scenarios
The Composite Pattern is particularly useful when:
- You want clients to treat individual objects and compositions uniformly
- Your system has a tree-like structure representing part-whole hierarchies
- You need to represent part-whole hierarchies of objects
Common examples include file system structures, organizational hierarchies, and graphical user interface components.
Real-World Examples in Source Code
HashMap's putAll() Method
public void putAll(Map<? extends K, ? extends V> sourceMap) {
for (Map.Entry<? extends K, ? extends V> entry : sourceMap.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
Here, the putAll() method accepts any Map implementation, treating it as an abstract component. HashMap acts as a composite, while individual key-value pairs are leaf nodes.
ArrayList's addAll() Method
public boolean addAll(Collection<? extends E> collection) {
Object[] elements = collection.toArray();
int newElementsCount = elements.length;
ensureCapacity(size + newElementsCount);
System.arraycopy(elements, 0, elementData, size, newElementsCount);
size += newElementsCount;
return newElementsCount != 0;
}
MyBatis SQL Node Processing
public interface SqlNode {
boolean evaluate(DynamicContext context);
}
MyBatis uses the Composite Pattern to parse SQL mapping files. Each node in an XML file becomes a SqlNode object, and these nodes are combined to form complete SQL statements.
Advantages and Disadvantages
Advantages
- Clearly defines hierarchical complex objects
- Allows clients to ignore structural differences
- Simplifies client code
- Follows the Open/Closed Principle
Disadvantages
- Can become complex when type restrictions are needed
- Increases design abstraction
The choice between transparent and safe implementations depends on your specific use case. Transparent Composite is better when most nodes share common behaviors, while Safe Composite is preferable when behaviors differ significantly or the structure is stable.
Remember that design patterns should be understood for their intent rather than their exact form. In practice, you'll often use variations of patterns that best fit your project's specific needs.