Introduction
The template method pattern is a behavourial design pattern which allows you to define the template, or the skeleton of an operation in a base class, while allowing the subclasses to override specific steps or all steps of the algorithm without changing its structure.
This pattern is very useful when you have a series of steps that needs to be performed in a specific order, but you need different implementations in different situations.
So, what does it look like? Well, like this:
A short explanation:
- The Producer is tasked with producing some product, which in this case can be a Car or a Bike. In order to produce these products, two steps are needed.
- BikeProducer and CarProducer are the two concrete Producers.
- The Client class both the step1 and the step2 method to obtain a product.
Since Rust does not support the notion of superclasses or abstract classes, we can implement this with traits.
Implementation in Rust
Open your terminal or commandline in an empty directory and type:
cargo new rust_template_pattern
cd rust_template_pattern
Open the directory in your favourite IDE and open the main.rs file in the src/ directory.
First we will define the trait:
trait Template {
fn add_frame(&self);
fn add_wheels(&self);
}
A vehicle in our world just consists of a frame and wheels, as you can see. Notice the &self arguments to both methods, so we can refer to the implementing struct when needed.
Now define the CarProducer:
struct CarProducer;
impl Template for CarProducer {
fn add_frame(&self) {
println!("Adding a car frame");
}
fn add_wheels(&self) {
println!("Adding 4 wheels");
}
}
A few notes:
- CarProducer is an empty struct as we do not need any data in the struct in this example.
- All the methods do is print out a message.
The BikeProducer is similar:
struct BikeProducer;
impl Template for BikeProducer {
fn add_frame(&self) {
println!("Adding a bike frame");
}
fn add_wheels(&self) {
println!("Adding 2 wheels");
}
}
Now we need the method to assemble it all:
fn produce_vehicle(producer:&dyn Template) {
producer.add_frame();
producer.add_wheels();
}
This method gets one parameter, a struct which implements the Template trait. Because we pass a trait, we use the dyn keyword.
Now it is time to test it all:
fn main() {
let producer=BikeProducer;
produce_vehicle(&producer);
}
Line by line:
- We instatiate BikeProducer struct.
- We pass this to the produce_vehicle function, we can do this because BikeProducer implements the Template trait.
An alternative version of the produce_vehicle function
If you don’t want to have a parameter with dyn, or perhaps you think that generics are more readable, you can always implement the produce_vehicle function like this:
fn produce_vehicle<T:Template>(producer:&T) {
producer.add_frame();
producer.add_wheels();
}
To me this looks more readable, however you are free to choose either way of course, it all depends on your taste and programming style.
Conclusion
The template pattern is one of the easiest patterns to implement. It was quite straightforward to get it right in Rust, and I found that the use of generics can make things more readable.