Applied Design Patterns with Java
Structural :: Composite (163) {C ch 11}
Implementation
Here are implementation issues to consider when using the
Composite pattern:
- Explicit parent references. Maintaining references from child components to their parent can simplify the traversal
and management of a Composite structure. The parent reference simplifies moving up the structure
and deleting a component. Parent references also help support the Chain
of Responsibility (223) pattern.
The usual place to define the parent reference is in the Component class. Leaf and Composite classes can inherit
the reference and the operations that manage it. With parent references, it's essential to maintain the invariant
that all children of a Composite have as their parent the Composite that in turn
has them as children. The easiest way to ensure this is to change a component's parent only when it's being added
or removed from a composite. If this can be implemented once in the Add and Remove operations of the Composite
class, then it can be inherited by all the subclasses, and the invariant will be maintained automatically.
- Sharing components. It's often useful to share components to reduce storage. But when a component can have
no more than one parent, sharing components becomes difficult. A possible solution is for children to store multiple
parents. But that can lead to ambiguities as a request propagates up the structure. The Flyweight (195) pattern shows how to rework a design to avoid storing parents altogether.
It works in cases where children can avoid sending parent requests by externalizing some or all of their state.
- Maximizing the Component interface. One of the goals of the Composite pattern is to make clients unaware of the specific Leaf or Composite
classes they're using. The Component class should define many common operations for Composite and Leaf classes.
The Component class provides default implementations for these operations, and Leaf and Composite
subclasses will override them. This goal may conflict with the principle of class hierarchy design that says a
class should only define operations that are meaningful to its subclasses. There are many operations that Component
supports that don't make sense for Leaf classes. Sometimes an operation that would appear to make sense only for
Composites can be implemented for all Components by moving it to the Component class. The interface
for accessing children is a fundamental part of a Composite class but not necessarily Leaf classes. But if we have a Leaf as
a Component without children, this allows defining a default operation for child access in the Component class
that never returns any children. Leaf classes can use the default implementation, but Composite
classes will reimplement it to return their children.
- Declaring the child management operations. Although the Composite class implements the Add and Remove operations for managing children,
an issue in the Composite pattern is which classes declare these operations in the Composite
class hierarchy. Should these operations be in the Component and make them meaningful for Leaf classes, or should
they be defined only in Composite and its subclasses? The decision involves a trade-off between safety
and transparency. Defining the child management interface at the root of the class hierarchy gives transparency,
because all components can be treated uniformly. It costs safety, however, because clients may try to do meaningless
things like add and remove objects from leaves. Defining child management in the Composite class gives safety,
because any attempt to add or remove objects from leaves will be caught at compile-time. But transparency is lost,
because leaves and composites have different interfaces.
- Should Component implement a list of Components? If the set of children are defined as an instance variable in the
Component class where the child access and management operations are declared means putting the child pointer in
the base class and incurs a space penalty for every leaf, even though a leaf never has children. This is worthwhile
only if there are few children in the structure.
- Child ordering. Many
designs specify an ordering on the children of Composite. In the earlier Graphics example, ordering may reflect front-to-back
ordering. If Composites represent parse trees, then compound statements can be instances of a Composite
whose children must be ordered to reflect the program. When child ordering is an issue, design child access and
management interfaces carefully to manage the sequence of children. The Iterator (257) pattern
is a guide in this.
- Caching to improve performance.
To traverse or search compositions frequently, the Composite class can cache traversal or search information about its children. The Composite can
cache actual results or just information that lets it short-circuit the traversal or search. Changes to a component
will require invalidating the caches of its parents. This works best when components know their parents. When using
caching, define an interface for telling Composites that their caches are invalid.
- Who should delete components?
In languages without garbage collection, it's best to make a Composite responsible for deleting its children when it's destroyed. An exception to this
rule is when Leaf objects are immutable and thus can be shared. Since Java is garbage collected, this does not
apply.
- What's the best data structure for storing components? Composites may use a variety of data structures to store their children, including linked
lists, trees, arrays, and hash tables. The choice of data structure depends on efficiency. It isn't necessary to
use a general-purpose data structure. Sometimes Composites have a variable for each child, but this requires each subclass of Composite to
implement its own management interface.
Related Patterns
Often the component-parent link is used for a Chain
of Responsibility (223).
Decorator (175) is often used with Composite, and they will usually have a common parent class. So Decorators
will have to support the Component interface with operations like Add, Remove, and GetChild.
Flyweight (195) allows sharing components, but they can no longer refer to their parents.
Iterator (257) can be used to traverse composites.
Visitor (331) localizes operations and behavior that would otherwise be distributed across Composite
and Leaf classes.
Catalog Structural Prev Next