by Zoran Horvat
One of the typical complications in code is letting the class contain a collection. The problem with such design is that all operations on that piece of state would have to be aware of the collection. Why Collections Are So Complicated?
Getting hold of a collection comes with many irritating questions. Does the collection contain objects or is it empty? Some aggregate functions cannot be calculated on empty collections. For example, maximum is not defined on an empty collection. Some other aggregate functions do not care about this problem: Sum or count functions would both happily return zero on an empty collection.
When a class controls a collection it has so many things to think about. Is it safe to call certain operation? Should the result be modified somehow before proceeding? Should it iterate through the whole collection or take only some elements into account.
Classes that contain collections tend to grow and become overwhelmed by scaffolding code which has nothing to do with class's responsibilities. The only purpose of that code is to make the class work. This code is a good candidate for a refactoring which would relieve the containing class from having to know how to iterate through multiple elements and how to call processing on them.
Natural solution to the problem is to introduce another class with a single purpose of holding the collection. This class would be responsible for iterating through the collection, filtering elements, invoking operations on elements and aggregating the results.
Final goal of this refactoring is to simplify the original class and let it focus on the value it delivers to the domain model. Lesser responsibilities, such as iterating through a list, should be removed and enclosed in a separate class.
In many cases, operations defined on a single object can also be defined on a collection of such objects. It only takes to define an appropriate aggregate function and to return a single result. This operation would then work uniformly when invoked on a single object and when called on a group of objects.
Main criterion which decides whether the Composite design pattern is applicable in particular case is the interface which exposes the operation. If we can define the same interface on a single element and on the collection, then the Composite pattern can be applied.
Suppose that we want to hire a painter to paint a house. Painter needs certain number of days to finish work. Now suppose that we want to hire several painters to work together. Time they need to finish when working together is shorter.
We can start from a definition of a painter:
class Painter
{
private readonly float daysPerHouse;
public Painter(float daysPerHouse)
{
this.daysPerHouse = daysPerHouse;
}
public float EstimateDaysToPaint(int houses)
{
return houses * daysPerHouse;
}
}
Painter just paints the houses. When asked to estimate work, the painter multiplies number of houses by his own velocity and returns the estimate.
We could introduce a land owner, a guy who owns several houses:
class LandOwner
{
private readonly Painter painter;
private readonly int housesCount;
public LandOwner(Painter painter, int housesCount)
{
this.painter = painter;
this.housesCount = housesCount;
}
public void ManageHouses()
{
float daysToPaint = this.painter.EstimateDaysToPaint(this.housesCount);
Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint);
}
}
Land owner keeps a reference to a painter. When time comes to manage houses, owner asks the painter to tell how much time he would need to paint all the houses.
Problems begin when it turns out that one painter cannot finish all the work in reasonable time. At that point, land owner hires more painters:
class LandOwner
{
private readonly IEnumerable<Painter> painters;
private readonly int housesCount;
public LandOwner(IEnumerable<Painter> painters, int housesCount)
{
this.painters = new List<Painter>(painters);
this.housesCount = housesCount;
}
...
}
But now, calculating time required to paint all the houses becomes an involved process, as shown in the figure below.
Now that the land owner has taken responsibility to do this calculation, its implementation becomes slightly more complex:
class LandOwner
{
private readonly IEnumerable<Painter> painters;
private readonly int housesCount;
public LandOwner(IEnumerable<Painter> painters, int housesCount)
{
this.painters = new List<Painter>(painters);
this.housesCount = housesCount;
}
private float GetVelocity(Painter painter)
{
return painter.EstimateDaysToPaint(1);
}
private float GetTotalVelocity()
{
float sum = 0;
foreach (Painter painter in this.painters)
sum += 1 this.GetVelocity(painter);
return 1 sum;
}
public void ManageHouses()
{
float daysToPaint = this.GetTotalVelocity() * this.housesCount;
Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint);
}
}
This implementation is a bit complicated but it works. What it cannot do is this. Suppose that one of the "painters" is the painting company which employs other painters. Even worse, painting company could employ another company along with its own painters. Land owner would then face multi-level hierarchy of painters. It would be very difficult for him to estimate time required to paint houses under such circumstances.
Organizing the house painters could be made easy if only we could pull out the public interface of a painter:
interface IPainter
{
float EstimateDaysToPaint(int houses);
}
This is the first way towards the composite painter. Land owner steps back from controlling a collection of painters and now controls only one abstract painter:
class LandOwner
{
private readonly IPainter painter;
private readonly int housesCount;
public LandOwner(IPainter painter, int housesCount)
{
this.painter = painter;
this.housesCount = housesCount;
}
public void ManageHouses()
{
float daysToPaint = this.painter.EstimateDaysToPaint(this.housesCount);
Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint);
}
}
This solution differs in only one letter compared to the first LandOwner class implementation shown above – this time, land owner just keeps reference to an abstract painter. What goes on under this abstract interface is of no interest to the land owner.
On the other hand, Painter class would remain the same as it is now, only it will implement the IPainter interface:
class Painter: IPainter
{
...
}
And here comes the real benefit. We are ready to define a composite element – concrete implementation of the IPainter interface which contains multiple contained elements implementing the same interface:
class PaintingCompany: IPainter
{
private readonly IEnumerable<IPainter> painters;
public PaintingCompany(IEnumerable<IPainter> painters)
{
this.painters = new List<IPainter>(painters);
}
private float GetVelocity(Painter painter)
{
return painter.EstimateDaysToPaint(1);
}
private float GetTotalVelocity()
{
float sum = 0;
foreach (Painter painter in this.painters)
sum += 1 this.GetVelocity(painter);
return 1 sum;
}
public float EstimateDaysToPaint(int houses)
{
return this.GetTotalVelocity() * houses;
}
}
Here is the implementation of the painting company. Remember the code from the LandOwner class when it used to control multiple painters? Well, that code has moved into this class. The difference is that painting company is now managing a number of abstract painters. Some of them could be real people, some others could be other painting companies subcontracted to help this company to complete work.
In this article we have seen one example of the Composite design pattern. Applying the Composite pattern has simplified the way in which classes are dealing with collections of objects. Working with the collection complicates code and adds a lot of thinking to the equation. When collection is conveniently wrapped into a class which only takes care of working with the collection, rather than working with its elements in terms of domain logic, we can greatly simplify the solution.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.