Applied Design Patterns with Java

Structural :: Composite (163) {C ch 11}

Implementation

Here are implementation issues to consider when using the Composite pattern:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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