The Composite Pattern
Composite Pattern
Contextual Forces
Motivation
Model simple and complex components in such a way as to allow client entities to consume their behavior in the same way. The composite pattern captures hierarchical relationships of varying complexity and structure.
Terms:
- Simple component - a single class, also called a "Leaf"
- Complex component - a class that contains pointers to sub-ordinate or "child" instances, and may delegate some or all of its responsibilities to them. These child instances may be simple or complex themselves. Also called a "Node".
Encapsulation
- The difference between a simple (Leaf) and complex (Node) component
- The structure of the composite relationship (tree, ring, web, etc... see Options in Implementation)
- The behavior of the complex components (aggregating, best/worse case, investigatory, etc... see Questions, Concerns, Credibility Checks)
Procedural Analog
In procedural systems, process and data are usually represented separately. In data structures, there is a clear analog to a composite pattern in the way tables can contain foreign keys to other tables, allowing for variation in the depth of the data being represented. As with most procedural analogs, what is missing here is encapsulation:
Non-Software Analog
In the military, responsibilities flow from the top of a hierarchy down to the bottom in various ways. Let us assume a Captain is assigning responsibilities for a number of tasks:
- She will order a Lieutenant to write up a set of reports for the pentagon. This Lieutenant will write the reports himself, with no further delegation.
- She will order another Lieutenant to create a training program for the upcoming maneuvers. This Lieutenant will actually delegate the specifics to a set of Drill Sergeants, then assemble their work into a plan, which he'll report back to the Captain.
- She will order a third Lieutenant to take hill 403. This Lieutenant will delegate this responsibility to a set of Sergeants, who will delegate further to Corporals, Privates and other resources (medics, an armor squad), each of which may delegate further, in a very complex arrangement
In each case, the Captain will "order" a single Lieutenant, and the variations in how these orders are accomplished will be encapsulated.
Implementation Forces
Example
public abstract class Component{ public abstract void operation(); } public class Leaf extends Component{ public void operation() { // leaf operation goes here } } public class Node extends Component{ private Component[] myChildren; public void operation() { foreach(Component Child in myChildren) { child.operation(); } } }
Questions, concerns, credibility checks
There are many different forms of the Composite Pattern, but at a high level they can generally be grouped into one of two categories: "Bill of Material", and "Taxonomy".
'A Bill of Material Composite' captures the relationship that exists where a complex object is composed of many smaller objects, each of which may be further composed of other, yet smaller objects. For example, your car could be said to be composed of a body and a chassis. The body is further made up of fenders, doors, panels, a trunk, a hood, etc... the chassis is further composed of a drive train and suspension. The drive train is still further composed of an engine, transmission, differential, and wheels, etc...
'A Taxonomical Composite' captures the logical relationships that exist between more and less general types. The biological phylum works this way... living things are divided into Mammals, Amphibians, Fish, and Birds. Mammals are further sub-divided into oviparous (egg-laying) and viviparous (live birth), etc...
This distinction is important because it gives us clues into the decisions we will have to make when implementing the pattern (see Options in Implementation)
If we are implementing a Taxonomical Composite, our motivation is almost certainly to allow a client entity to traverse the structure, to find information or classify an entity within the context of the existing structure.
If we are implementing a Bill of Material composite, however, we will have many options when implementing its behavior, each of which will have different implications for encapsulation.
- Aggregating Behavior: If we need to determine the weight of the car, for example, we need to aggregate the weights of all the sub-parts (which in turn need to aggregate their sub-parts, etc...). Often, this behavior can be hidden from consuming client entities in the way the Node component is implemented.
- Best/Worst Case Behavior: If we need to determine the overall "health" of the car, the rule might be that the car is healthy if and only if all its parts are healthy. If any one part is in error, perhaps, we might say the entire car is in error. Thus, we would say the car is the "worst case" of all its parts. Again, this behavior can often be hidden from the consuming class in the way the Node component is implemented.
- Investigatory: If we need to determine something specific about a part, or about a set of parts that meet a criteria (all the parts of the car that come in contact with oil, for example), then we might need to allow the client entity to "walk the tree" of parts, looking for those that qualify. In this case, we may not be able to hide the specifics of the composite, much as we generally cannot in a Taxonomical Composite.
In any case, there is a key decision to make when creating a Composite, which is whether or not to put methods into the target abstraction that allow for the client entity to traverse the Composite. These "traversal methods" create coupling between the client and the components, and should not be included unless they are needed.
In a Taxonomy, traversal methods are almost always needed, since the purpose of the Composite is for the client to traverse it.
In a Bill of Material, traversal methods may or may not be needed, depending on the behavior desired.
Options in implementation
The nature of traversal methods, if they are needed, will reflect the nature of the composite. The standard Composite is the "tree" hierarchy:
However, there are many other Composite structures possible. For example, this is also a Composite:
This is a "ring" and as you can see the traversal methods are different. Similar to this form of Composite is also the "bi-directional ring" which would allow for "next" and "previous" traversal (not shown). For a concrete example of a ring Composite, see this example.
Yet another Composite is a “web”, which has yet again different traversal methods implied by its structure:
The point is: the nature of the Composite structure will be reflected in the traversal methods provided. Therefore, if traversal methods are provided, the fact that a Composite is a tree, ring, web, or any other structure will not be encapsulated from entities that consume it. If a Composite were to begin as a tree, then later be changed to a ring, the client entities that interacted with it via these traversal methods would all have to be maintained.
Consequent Forces
Testing issues
Testing Leaf objects is straightforward. However, testing Node objects should include verification that they interact as expected with their sub-ordinate (child) instances at runtime. A Mock object can be substituted for these sub-ordinate instances for testing. Note that if the Mock is conditionable, the same Mock Class can be used for all child nodes, by creating multiple instances and conditioning them for different behavior:
Cost-Benefit (gain-loss)
The client entity is freed from responsibilities that can be delegated to the Composite, and therefore the client entity is made simpler, and more cohesive.
However, we tend to create very generic components, which can create difficulties when attempting to model very complex behavior.
The Composite is always a trade-off between these forces.