Introduction
The composite pattern allows you treat a group of objects like a single object. The objects are composed into some form of a tree-structure to make this possible.
This patterns solves two problems:
- Sometimes it is practical to treat part and whole objects the same way
- An object hierarchy should be represented as a tree structure
We do this by doing the following:
- Define a unified interface for both the part objects (Leaf) and the whole object (Composite)
- Composite delegate calls to that interface to their children, Leaf objects deal with them directly.
This all sounds rather cryptic, so let us have a look at the diagram:
This is basically a graphical representation of the last two points: Composites delegate and Leafs perform the actual operation.
Implementation in Go
In this example we will deal with a country, and provinces. We will start with the usual preliminaries:
package main
import "fmt"
Next we define a GeographicalEntity interface, with just one method:
type GeographicalEntity interface {
search(string)
}
Next we define the Country struct:
type Country struct {
provinces []GeographicalEntity
name string
}
func newCountry(name string) Country {
return Country{
provinces: make([]GeographicalEntity, 0),
name: name,
}
}
func (c Country) search(term string) {
fmt.Printf("Search for city %s in country %s\n", term, c.name)
for _, composite := range c.provinces {
composite.search(term)
}
}
func (c *Country) addProvince(province GeographicalEntity) {
c.provinces = append(c.provinces, province)
}
Some notes:
- A country consists of some provinces. That is where the provinces variable comes from
- A country also has a name
- The newCountry function is a utility function to construct a new Country struct
- In the search we iterate over the provinces variable. Since the objects implement the search() method, we delegate our search-request to them. Note that any object that satisfies the GeographicalEntity interface could be in that array.
- The addProvince() simply adds a province, or to be precise an object satisfying the GeographicalEntity interface, to the provinces slice.
Next we implement the Province:
type Province struct {
name string
}
func newProvince(name string) Province {
return Province{
name: name,
}
}
func (p Province) search(term string) {
fmt.Printf("Search for city %s in province %s\n", term, p.name)
}
Also some notes here:
- The Province struct is the Leaf node of our current setup.
- A Province has one name.
- The newProvince() function is a utility function to create a Province struct
- In the search() method we simply announce we are searching
Time to test
Time to set up a small geographical database, and see if we can search it:
func main() {
province1 := newProvince("province1")
province2 := newProvince("province2")
country := newCountry("country")
country.addProvince(province1)
country.addProvince(province2)
country.search("city")
}
Line by line:
- We construct two provinces, and one country
- We add the provinces to the country
- And the we search for the city ‘city’
Conclusion
This pattern is one of the most versatile patterns. I have used it here to model a country with provinces. Some of the more canonical examples use a file-system with files and folders, and there are probably dozens other use-cases.
One possible enhancement would be to make the search multi-threaded, so that it can be done more efficiently. In our use-case that could work since the searches are independent of each other.