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:
- A country consists of some provinces. That is where the provinces variable comes from
- A country also has a name
- 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:
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:
- The Province struct is the Leaf node of our current setup.
- A Province has one name.
- 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:
- We construct a country.
- Next we create two provinces.
- We add these objects to the country
- 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.