Easy Patterns in Rust: The Composite Pattern

Photo by Reinier van Es: https://www.pexels.com/photo/mechanism-of-a-clock-10839226/

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 Rust

In this example we will deal with a country, and provinces. We will start by defining the GeographicalEntity trait:

trait GeographicalEntity {
    fn search(&self, query: &str);
}

Next we define the Country struct:

struct Country {
    name:String,
    provinces: Vec<Box<dyn GeographicalEntity>>,
}

impl Country {
    fn new(name: &str) -> Country {
        Country {
            name: name.to_string(),
            provinces: Vec::new(),
        }
    }

    fn add_province(&mut self, province: Box<dyn GeographicalEntity>) {
        self.provinces.push(province);
    }
}

impl GeographicalEntity for Country {
    fn search(&self, query: &str) {
        println!("Searching for {} in {}", query, self.name);
        for province in &self.provinces {
            province.search(query);
        }
    }
}

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. 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.
  4. 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:

struct Province {
    name: String,
}

impl Province {
    fn new(name: &str) -> Province {
        Province {
            name: name.to_string(),
        }
    }
}

impl GeographicalEntity for Province {
    fn search(&self, query: &str) {
        println!("Searching for {} in {}", query, self.name);
    }
}

Also some notes here:

  1. The Province struct is the Leaf node of our current setup.
  2. Province has one name.
  3. In the search() method we simply announce we are searching

Time to test

We will now see whether our small geographical database does what we want:

fn main() {
    let mut country = Country::new("country1");
    let province1 = Province::new("province1");
    let province2 = Province::new("province2");

    country.add_province(Box::new(province1));
    country.add_province(Box::new(province2));

    country.search("city1");
}

Line by line:

  1. We construct a country.
  2. Next we create two provinces.
  3. We add these objects to the country
  4. Next we set out a search for ‘city1’.

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 *