This post shows how to use the Composite design pattern in Rust. Also, it offers a sample implementation with two levels of composition. For instance, we have a group of lists of products.
The Composite Design Pattern Use Case
Our use case is relatively simple for this post – displaying a product’s information. Moreover, the data may come from a single product, a collection of products, or a group of collections. Applying the Composite design pattern, we have the Sellable trait that the relevant structs need to implement.
A Rust Trait To Process Structs Uniformly
The Sellable trait allows us to process any structs that implement the trait uniformly. For instance, consider the following Rust trait. It has five methods relevant to a single product and a collection of products. Therefore, it will help us implement the Composite design pattern in Rust.
1 2 3 4 5 6 7 8 | trait Sellable { fn add(&mut self, sellable: Box<dyn Sellable>); fn get_name(&self) -> String; fn get_price(&self) -> f64; fn get_count(&self) -> usize; fn get_information(&self) -> Vec<&Box<dyn Sellable>>; } |
Next, we have a processor of Sellable struct instances. Consider the following struct and its implementation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct SellableProcessor {} impl SellableProcessor { pub fn process(&self, sellable_item: Box<dyn Sellable>) { println!("Sellable Product Information to process"); println!("======================================="); println!("{} item(s) for the price of {} ", sellable_item.get_count(), sellable_item.get_price()); for i in sellable_item.get_information().iter() { println!("name: {}, price: {}, count: {}", i.get_name(), i.get_price(), i.get_count()); } println!("\n\n"); } } |
The SellableProcessor displays a summary and details of the product or products if we are dealing with collections.
Rust Struts That Implement The Composite Pattern
Then, we go through the Rust structs that implement the Composite design pattern. First, we have the Product struct that has a straightforward implementation. Moreover, it does not allow for multiple products because it is our base struct to build on. Hence, the panic macro in the add method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | struct Product { name: String, price: f64 } impl Sellable for Product { fn add(&mut self, sellable: Box<dyn Sellable>) { panic!("Not supported for single product") } fn get_name(&self) -> String { self.name.clone() } fn get_price(&self) -> f64 { self.price } fn get_count(&self) -> usize { 1 } fn get_information(&self) -> Vec<&Box<dyn Sellable>> { vec![] } } |
It also returns an empty list of Sellables via the get_information method because we have the get_name and get_price methods to provide data. Meanwhile, the get_count returns 1 to represent a single product.
Next, we have another struct that holds a collection of Sellable products for our Composite design pattern implementation. Unlike the base struct, the CollectionOfProducts struct does more computation on the list of items. For example, the price is the sum of all prices, and the product count is the size of the collection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | struct CollectionOfProducts { collection_of_products: Vec<Box<dyn Sellable>> } impl Sellable for CollectionOfProducts { fn add(&mut self, sellable: Box<dyn Sellable>) { self.collection_of_products.push(sellable); } fn get_name(&self) -> String { String::from("Aggregated Product Name is not supported") } fn get_price(&self) -> f64 { let mut sum = 0.0; for i in self.collection_of_products.iter() { sum = sum + i.get_price() } sum } fn get_count(&self) -> usize { self.collection_of_products.len() } fn get_information(&self) -> Vec<&Box<dyn Sellable>>{ let mut list: Vec<&Box<dyn Sellable>> = vec![]; let temp = &self.collection_of_products; for i in temp.iter() { list.push(i); } list } } |
Lastly, we have a struct that represents a group of collections of products. More importantly, it pushes our Composite design pattern in Rust to the limit by handling multilevel composition.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | struct GroupOfCollectionsOfProducts { group_of_collections_of_products: Vec<Box<dyn Sellable>> } impl Sellable for GroupOfCollectionsOfProducts { fn add(&mut self, sellable: Box<dyn Sellable>) { self.group_of_collections_of_products.push(sellable) } fn get_name(&self) -> String { panic!("Aggregated Product Name is not supported") } fn get_price(&self) -> f64 { let mut sum = 0.0; for i in self.group_of_collections_of_products.iter() { sum = sum + i.get_price() } sum } fn get_count(&self) -> usize { let mut count:usize = 0; for i in self.group_of_collections_of_products.iter() { count = count + i.get_count(); } count } fn get_information(&self) -> Vec<&Box<dyn Sellable>> { let mut list: Vec<&Box<dyn Sellable>> = vec![]; // TODO: use recursion let groups = &self.group_of_collections_of_products; for group in groups.iter() { let collections = group.get_information(); for a_product in collections { list.push(a_product); } } list } } |
The Composite Design Pattern Demo
We need a main function with sample data to test our Composite design pattern Rust codes. For example, consider the following codes. They have test cases for a single product, a collection of products, and a group of collections of products.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | fn main() { let processor = SellableProcessor {}; let mut single_product = Product{ name: "lone product".to_string(), price: 3.0 }; // Not supported for lone product // single_product.add( Box::new(Product{ name: "lone product".to_string(), price: 3.0 })); let mut collection_of_products = CollectionOfProducts { collection_of_products: vec![] }; collection_of_products.add(Box::new(Product{ name: "Collection 1, Product 1".to_string(), price: 3.0 })); collection_of_products.add(Box::new(Product{ name: "Collection 1, Product 2".to_string(), price: 2.0 })); collection_of_products.add(Box::new(Product{ name: "Collection 1, Product 3".to_string(), price: 1.0 })); collection_of_products.add(Box::new(Product{ name: "Collection 1, Product 4".to_string(), price: 0.5 })); collection_of_products.add(Box::new(Product{ name: "Collection 1, Product 5".to_string(), price: 17.0 })); let mut collection_2_of_products = CollectionOfProducts { collection_of_products: vec![] }; collection_2_of_products.add(Box::new(Product{ name: "Collection 2, Product A".to_string(), price: 6.0 })); collection_2_of_products.add(Box::new(Product{ name: "Collection 2, Product B".to_string(), price: 1.75 })); let mut collection_3_of_products = CollectionOfProducts { collection_of_products: vec![] }; collection_3_of_products.add(Box::new(Product{ name: "Collection 3, Product C".to_string(), price: 12.0 })); collection_3_of_products.add(Box::new(Product{ name: "Collection 3, Product D".to_string(), price: 4.01 })); let mut group_of_collections_of_products = GroupOfCollectionsOfProducts { group_of_collections_of_products: vec![] }; group_of_collections_of_products.add(Box::new(collection_2_of_products)); group_of_collections_of_products.add(Box::new(collection_3_of_products)); processor.process(Box::new(single_product)); processor.process(Box::new(collection_of_products)); processor.process(Box::new(group_of_collections_of_products)); } |
When we run all the codes, we get the following sample result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | Sellable Product Information to process ======================================= 1 item(s) for the price of 3 Sellable Product Information to process ======================================= 5 item(s) for the price of 23.5 name: Collection 1, Product 1, price: 3, count: 1 name: Collection 1, Product 2, price: 2, count: 1 name: Collection 1, Product 3, price: 1, count: 1 name: Collection 1, Product 4, price: 0.5, count: 1 name: Collection 1, Product 5, price: 17, count: 1 Sellable Product Information to process ======================================= 4 item(s) for the price of 23.759999999999998 name: Collection 2, Product A, price: 6, count: 1 name: Collection 2, Product B, price: 1.75, count: 1 name: Collection 3, Product C, price: 12, count: 1 name: Collection 3, Product D, price: 4.01, count: 1 |
We tested the codes using Rust 1.60.0.