Elegant and Easy: Unleashing the Power of the Composite Pattern in Go

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:

  1. A country consists of some provinces. That is where the provinces variable comes from
  2. A country also has a name
  3. The newCountry function is a utility function to construct a new Country struct
  4. 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.
  5. 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:

  1. The Province struct is the Leaf node of our current setup.
  2. A Province has one name.
  3. The newProvince() function is a utility function to create a Province struct
  4. 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:

  1. We construct two provinces, and one country
  2. We add the provinces to the country
  3. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *