Summary: in this tutorial, you’ll learn how to use the C# Composite pattern to treat individual objects and compositions of objects uniformly.
Introduction to the C# composite pattern
The C# Composite pattern is a structural design pattern that allows you to create a hierarchy of individual objects and collections of objects through a shared interface. This enables the client to treat individual objects and the group of objects uniformly.
Since an individual object and a collection of objects share a common interface, the client doesn’t need to know where it is managing a single object or a collection object.
Instead, the client can make use of polymorphism without having to apply unnecessary condition statements that check whether the object is an individual object or a collection of objects.
To illustrate this composite pattern, imagine you have to manage a bill of materials (BOM) that contains a list of materials for making a part of a product (assembly) or the whole product.
In some cases, you need to perform the same operation on a component whether it is a single component or an assembly that consists of individual components.
For example, you should be able to get the cost of a component without knowing whether it is a component or an assembly.
The following UML diagram illustrates the composite pattern:
In this diagram:
IComponent
: This is a shared interface that specifies the operation each component needs to implement.Leaf
: This is an individual component.Composite
: This is a collection of components. It can include both leaves and other composites.Client
: This class has access to different components via the shared interfaceIComponent
, taking advantage of polymorphism.
C# Composite pattern example
The following C# program demonstrates the composite pattern:
// Shared interface
public interface IComponent
{
string Name { get; set; }
int Quantity { get; set; }
double GetCost();
}
// Leaf component
public class Part : IComponent
{
public string Name { get; set; }
public int Quantity { get; set; }
public double Cost { get; set; }
public Part(string name, int quantity, double cost)
{
Name = name;
Quantity = quantity;
Cost = cost;
}
public double GetCost() => Cost * Quantity;
}
// Composite component
public class Assembly : IComponent
{
public string Name { get; set; }
public int Quantity { get; set; }
private readonly List<IComponent> _components = new();
public Assembly(string name, int quantity, IEnumerable<IComponent> components)
{
Name = name;
Quantity = quantity;
_components.AddRange(components);
}
public double GetCost() => _components.Sum(component => component.GetCost());
}
// Client
public class Program
{
public static void Main(string[] args)
{
// parts
var engine = new Part("Engine", 1, 5000.0);
var tires = new Part("Tires", 4, 1000.0);
// assembly
var body = new Assembly(
"Body",
1,
new List<IComponent> {
new Part("Frame", 1, 2000.0),
new Part("Doors", 4, 1000.0),
new Part("Windows", 6, 500.0)
}
);
// car
var car = new Assembly(
"Car",
1,
new List<IComponent> {
engine,
tires,
body
});
// Calculate the cost of the car
var carCost = car.GetCost();
Console.WriteLine($"The cost of the car is: {carCost:C}");
}
}
Code language: C# (cs)
Output:
The cost of the car is: $18,000.00
Code language: C# (cs)
How it works.
First, define a shared interface
that both component and assembly need to implement. The IComponent
interface has the IComponent
Name
and Quantity
properties as well as the GetCost()
method:
public interface IComponent
{
string Name { get; set; }
int Quantity { get; set;}
double GetCost();
}
Code language: C# (cs)
Second, define the Part
class that serves as a leaf. The Part
class implements the IComponent
interface:
public class Part : IComponent
{
public string Name { get; set; }
public int Quantity { get; set; }
public double Cost { get; set; }
public Part(string name, int quantity, double cost)
{
Name = name;
Quantity = quantity;
Cost = cost;
}
public double GetCost() => Cost * Quantity;
}
Code language: C# (cs)
In the Part class, the GetCost()
calculates the cost by multiplying the cost of each component by the quantity.
Third, define the Assembly
class that serves as the composite. The Assembly
class implements the
interface and has a field with the type of IComponent
IEnumerable
<
> that represents a list of IComponent
:IComponent
public class Assembly : IComponent
{
public string Name { get; set; }
public int Quantity { get; set; }
private readonly List<IComponent> _components = new();
public Assembly(string name, int quantity, IEnumerable<IComponent> components)
{
Name = name;
Quantity = quantity;
_components.AddRange(components);
}
public double GetCost() => _components.Sum(component => component.GetCost());
}
Code language: C# (cs)
The GetCost()
method calculates the total cost by using the Sum
extension method by adding up the costs of all the components in the _component
list.
Finally, define the Program
class that uses the Part
and Assembly
classes to construct a Car product and call the GetCost()
of the car object to get the total cost of the car:
public class Program
{
public static void Main(string[] args)
{
// parts
var engine = new Part("Engine", 1, 5000.0);
var tires = new Part("Tires", 4, 1000.0);
// assembly
var body = new Assembly("Body", 1, new List<IComponent> {
new Part("Frame", 1, 2000.0),
new Part("Doors", 4, 1000.0),
new Part("Windows", 6, 500.0)
});
// car
var car = new Assembly("Car", 1, new List<IComponent>{
engine,
tires,
body
});
// Calculate the cost of the car
var carCost = car.GetCost();
Console.WriteLine($"The cost of the car is: {carCost:C}");
}
}
Code language: C# (cs)
Summary
- Use the Composite pattern to treat individual objects and compositions of objects uniformly.