The Composite pattern represents part-whole hierarchies of objects. "Part-whole hierarchies" is a really fancy way of saying you can represent all or part of a hierarchy by reducing the pieces in said hierarchy down to common components.
When using this pattern, clients should be able to treat groups of objects in a hierarchy as "the same" even though they can be different. You can do this selectively to parts of the hierarchy, or to the entire hierarchy.
NOTE: This post is part of a series demonstrating software design patterns using C# and .NET. The patterns are taken from the book Design Patterns by the Gang of Four. Check out the other posts in this series!
The Rundown
- Type: Structural
- Useful? 4/5 (Very)
- Good For: Treating different objects in a hierarchy as the same.
- Example Code: On GitHub
The Participants
- The Component declares an interface for objects in the composition. It also implements behavior that is common to all objects in said composition. Finally, it must implement an interface for adding/removing it's own child components.
- The Leaves represent leaf behavior in the composition (a leaf is an object with no children). It also defines primitive behavior for said objects.
- The Composite defines behavior for components which have children (contrasting the Leaves). It also stores its child components and implements the add/remove children interface from the Component.
- The Client manipulates objects in the composition through the Component interface.
A Delicious Example
To model the Composite pattern effectively, let's think about a soda dispenser. Specifically, one of the Coca-Cola Freestyle machines you'll find at certain restaurants or movie theatres.
For those of you that haven't seen these monstrosities, they're not at all like the regular soda dispensers you'll find at restaurants. The regular dispenses have six, or eight, or maybe twelve flavors; the Freestyle machines have potentially hundreds. Any flavor of drink that the Coca-Cola company makes in your part of the world, you can order at this machine.
The most interesting part of this device, though, is its interface. The Freestyle wants you to "drill-down" by first selecting a brand (e.g. Coke, Fanta, Sprite, Dasani, etc.) and then selecting a flavor (e.g. Cherry, Vanilla, etc.). In effect, this creates a hierarchy where "Soda" itself is the root Component; the brands are the child Components, and the flavors are Leaves.
A simplified version of this hierarchy might look like this:
Let's model this hierarchy. For all possible flavors of soda that our machine dispenses, we need to know how many calories each particular flavor has. So, in our abstract class that represents all soft drinks, we need a property for Calories:
/// <summary>
/// Component abstract class
/// </summary>
public abstract class SoftDrink
{
public int Calories { get; set; }
public List<SoftDrink> Flavors { get; set; }
public SoftDrink(int calories)
{
Calories = calories;
Flavors = new List<SoftDrink>();
}
/// <summary>
/// "Flatten" method, returns all available flavors
/// </summary>
public void DisplayCalories()
{
Console.WriteLine(this.GetType().Name + ": "
+ this.Calories.ToString() + " calories.");
foreach (var drink in this.Flavors)
{
drink.DisplayCalories();
}
}
}
Note the DisplayCalories()
method. This is a recursive method that will show the calories for all nodes, starting from the current node. We will use this method a bit later to output all calories for all drinks.
Next up, we need to implement several Leaves participants for the concrete soda flavors.
/// <summary>
/// Leaf class
/// </summary>
public class VanillaCola : SoftDrink
{
public VanillaCola(int calories) : base(calories) { }
}
/// <summary>
/// Leaf class
/// </summary>
public class CherryCola : SoftDrink
{
public CherryCola(int calories) : base(calories) { }
}
/// <summary>
/// Leaf class
/// </summary>
public class StrawberryRootBeer : SoftDrink
{
public StrawberryRootBeer(int calories) : base(calories) { }
}
/// <summary>
/// Leaf class
/// </summary>
public class VanillaRootBeer : SoftDrink
{
public VanillaRootBeer(int calories) : base(calories) { }
}
/// <summary>
/// Leaf class
/// </summary>
public class LemonLime : SoftDrink
{
public LemonLime(int calories) : base(calories) { }
}
We now need to implement the Composite participant, which represents objects in the hierarchy which have children. For our decision tree, we have two Composites: Colas
and RootBeers
.
/// <summary>
/// Composite class
/// </summary>
public class Cola : SoftDrink
{
public Cola(int calories) : base(calories) { }
}
/// <summary>
/// Composite class
/// </summary>
public class RootBeer : SoftDrink
{
public RootBeer(int calories) : base(calories) { }
}
Finally, we need one more composite class for the root node. In our diagram, the root node is soda water.
/// <summary>
/// Composite class, root node
/// </summary>
public class SodaWater : SoftDrink
{
public SodaWater(int calories) : base(calories) { }
}
The Composite classes are exactly the same as the Leaf classes, and this is not an accident.
Finally, our Main()
shows how we might initialize a SoftDrink
hierarchy with several flavors and then display all of the calories for each flavor:
static void Main(string[] args)
{
var colas = new Cola(210);
colas.Flavors.Add(new VanillaCola(215));
colas.Flavors.Add(new CherryCola(210));
var lemonLime = new LemonLime(185);
var rootBeers = new RootBeer(195);
rootBeers.Flavors.Add(new VanillaRootBeer(200));
rootBeers.Flavors.Add(new StrawberryRootBeer(200));
SodaWater sodaWater = new SodaWater(180);
sodaWater.Flavors.Add(colas);
sodaWater.Flavors.Add(lemonLime);
sodaWater.Flavors.Add(rootBeers);
sodaWater.DisplayCalories();
Console.ReadKey();
}
The output of this method being:
Will I Ever Use This Pattern?
Will you ever have hierarchical data? If so, probably yes. The key part of this pattern is that you can treat different objects as the same, provided you set up the appropriate interfaces and abstracts.
Summary
The Composite pattern takes objects in a hierarchy and allows clients to treat different parts of that hierarchy as being the same. Then, among other things, you can "flatten" all or part of the hierarchy to get only the data that's common to all of the parts.
(For all you international soda drinkers out there, maybe you can help me with something. I was not aware that the flavor "root beer" was solely a North American thing, but Wikipedia says it is. Do you all not get this kind of soda in your country? Because it is the bomb and you should try it.)
Happy Coding!