The Composite Pattern
The composite pattern gives a uniform interface to collections and individual objects, allowing both to be processed by the same logic.
Introduction
Sometimes we find that our code acquires all sorts of logic sprinkled around to deal with calling things that are similar but different enough to require conditional handling. When there are only a few variations this can be fairly manageable, but the maintainability of the code becomes harder when you need to scale. How do we get this type of code that has become too complex back to a state that is easy to test?
The composition pattern is an effective way to manage complexity when a tree like structure of individual items and collections is present. The composition pattern simplifies calling logic by treating individual items and collections with a uniform interface. Callers can ditch complex logic in favor of simple looping constructs. Any logic that remains is often simpler and resides in a component that is easier to test.
If you want to skip right to the code example, use this link: https://github.com/ccozad/composite-js-example The code has its own README that details what is going on. There will also be light commentary at the end of the article.
The composite pattern gives a uniform interface to collections and individual objects, allowing both to be processed by the same logic.
As an Amazon Associate, I earn commission from qualifying purchases.
A trip back in history.
The composition pattern was one of the original 23 patterns discussed in the 1994 classic Design Patterns (affiliate link). The book can be difficult to understand but the design patterns cataloged in this book are present in many high quality library and frameworks.
Life Without The Composite Pattern
You can build functioning software without the composite pattern. (or any pattern really) The problem is your code starts to acquire cumbersome idioms that hinder maintenance. If it’s Type A call it this way, a collection of Type A call it in a slightly different way or if it is Type B, call it in a completely different way. Before the composite pattern is adopted the code might have flows something like this:
Each color represents a slightly different calling conventions. The caller is carefully crafted with complex logic to call each variation in the system. Changes to how a component is called ripple into the caller. Implementation mistakes in the caller for one component can potentially break code that calls other components. It starts to feel like there aren’t really components at all, just a tangle of brittle code. Teams may decide to deal with this situation by enforcing strict rules around changing the complex control logic. This might work for a time but at some point the pace of change grinds to a halt and cascading defects become common.
Life With The Composite Pattern
Instead of fixing the problem with policy, we can fix the problem using the composite pattern. Our first task is to group everything that we do similar things with. For example, we might have a number of widgets we draw on the screen, a number of calculation steps we perform or a number of documents we process. Once we think we have a grouping of similar things we need to find a common verb to operate on everything. Finding a generic operator can often be quite challenging since different variants can have dramatically different calling conventions.
When forming this abstraction you might feel the pull to make little specializations in the wording. If you were trying to reduce the complexity of parsing documents you might start adding methods like processText(), processWordDocument(), processPDF(), processHTML. You might feel good that each variation has a lane, but just saying words with a common prefix is not the composite pattern. This approach is still a useful intermediate step because the part that overlaps between everything is often the operator you are looking for, which is this case is simply process()
It is common for the uniform operator to be something quite generic like:
render
update
calculate
process
draw
Depending on the use case, it might be necessary to pass some type of shared state. For example, a process to calculate a bill may need to know some value to calculate a total or the total cost and location to calculate the correct tax. Once the shift is made to a uniform interface, the caller often moves from complex logic to a holder of simple state that iterates over components.
The new arrangement looks something like this:
Note how we have taken a complex knot of logic and distributed it to smarter components that each have their own simple logic. While the sum of the behavior may be equivalent to the original control logic, each piece becomes easier to reason about and test
This subtle shift means that future items can be added without modifying the calling code as long as the calling code continues to only use the common interface. The top level code doesn’t care what it is calling, as long as it conforms to the interface. Another positive side effect is branching decisions are pushed to lower levels that can be more effectively developed and tested in isolation.
Linking To Other Concepts
If we want to think in terms of SOLID principles, the composite pattern is related to the Liskov Substitution Principle and Dependency Inversion
The Liskov Substitution Principle
Functions that use references to base classes must be able to use objects of the derived class without knowing it
In our case, we are not enforcing a subclass relationship but we are making individual objects and collections of objects interchangeable by ensuring that they conform to the same interface. The interface is the abstraction we depend on, which leads right into the Dependency Inversion Principle
The Dependency Inversion Principle
High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
All the caller knows is that a uniform interface is implemented and the caller does not care if the operator is for one level or a complex tree of operation.
Are There Any Real World Examples?
The composite pattern appears in a variety of cases where a tree is a natural data structure. Here’s a sampling of usages:
Godot
The Godot Game Engine has a Node base class with _process() and _physics_process() that are called every frame and every physics update interval respectively. Game developers update the state of their game world for the node and update graphics in the scene. Hundreds of Nodes can all go about processing information and contributing their small part to a larger game world simulation.
React
A React Component is required to support one method, render(). The virtual DOM that React manages form a complex tree of components that are all being rendered based on reactions to various changes.
Flutter
Flutter is a framework originally targeted at mobile apps. Widgets display content on the screen and each widget implements a build() method that describes the widget. Widgets are composed in trees and the framework walks the entire widget tree to then calculate a difference for display updates.
Show Me Some Code
A companion code example of the composite pattern is available on Github: https://github.com/ccozad/composite-js-example The code has a detailed overview so only the highlights will be covered here.
The example shows how to use the composite pattern to model a complex restaurant bill. The example includes:
Single items that add to the to the total
Combos representing multiple items that add to the total
Discounts that dynamically reduce the total based on the amount spent or items purchased
Fees that dynamically calculate based on the total
Despite each of these calculations operating very differently, they can all be composed together into a tree and processed by a simple loop
Resources
The Liskov Substitution Principle https://condor.depaul.edu/dmumaugh/OOT/Design-Principles/lsp.pdf
The Dependency Inversion Principle https://condor.depaul.edu/dmumaugh/OOT/Design-Principles/dip.pdf
Refactoring Guru - Composite pattern https://refactoring.guru/design-patterns/composite
_process() and _physics_process() in Godot Nodes https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html
The render() method on a React Component https://react.dev/reference/react/Component#render
The build() method on a Flutter Widget https://api.flutter.dev/flutter/widgets/StatelessWidget/build.html