Template Method Pattern
Template Method is a behavioral design pattern that allows you to define a skeleton of an algorithm in a base class and let subclasses override the steps without changing the overall algorithm’s structure.
Golang Implementation
Example: One Time Password (OTP) Functionality
Let’s consider the example of One Time Password (OTP) functionality. There are different ways that the OTP can be delivered to a user (SMS, email, etc.). But irrespective whether it’s an SMS or email OTP, the entire OTP process is the same:
- Generate a random n digit number.
- Save this number in the cache for later verification.
- Prepare the content.
- Send the notification.
Any new OTP types that will be introduced in the future most likely still go through the above steps.
So, we have a scenario where the steps of a particular operation are the same, but these steps’ implementation may differ. This is an appropriate situation to consider using the Template Method pattern.
First, we define a base template algorithm that consists of a fixed number of methods. That’ll be our template method. We will then implement each of the step methods, but leave the template method unchanged.
otp.go
package main
type IOtp interface {
genRandomOTP(int) string
saveOTPCache(string)
getMessage(string) string
sendNotification(string) error
}
type Otp struct {
iOtp IOtp
}
func (o *Otp) genAndSendOTP(otpLength int) error {
otp := o.iOtp.genRandomOTP(otpLength)
o.iOtp.saveOTPCache(otp)
message := o.iOtp.getMessage(otp)
err := o.iOtp.sendNotification(message)
if err != nil {
return err
}
return nil
}
sms.go
package main
import "fmt"
type Sms struct {
Otp // emdedding Otp struct, so Sms struct will have all the methods of Otp struct, like inheritance
}
func (s *Sms) genRandomOTP(len int) string {
randomOTP := "1234"
fmt.Printf("SMS: generating random otp %s\n", randomOTP)
return randomOTP
}
func (s *Sms) saveOTPCache(otp string) {
fmt.Printf("SMS: saving otp: %s to cache\n", otp)
}
func (s *Sms) getMessage(otp string) string {
return "SMS OTP for login is " + otp
}
func (s *Sms) sendNotification(message string) error {
fmt.Printf("SMS: sending sms: %s\n", message)
return nil
}
email.go
package main
import "fmt"
type Email struct {
Otp // emdedding Otp struct, so Sms struct will have all the methods of Otp struct, like inheritance
}
func (s *Email) genRandomOTP(len int) string {
randomOTP := "1234"
fmt.Printf("EMAIL: generating random otp %s\n", randomOTP)
return randomOTP
}
func (s *Email) saveOTPCache(otp string) {
fmt.Printf("EMAIL: saving otp: %s to cache\n", otp)
}
func (s *Email) getMessage(otp string) string {
return "EMAIL OTP for login is " + otp
}
func (s *Email) sendNotification(message string) error {
fmt.Printf("EMAIL: sending email: %s\n", message)
return nil
}
main.go
package main
import "fmt"
func main() {
smsOTP := &Sms{}
o := Otp{
iOtp: smsOTP,
}
o.genAndSendOTP(4)
fmt.Println("")
emailOTP := &Email{}
o = Otp{
iOtp: emailOTP,
}
o.genAndSendOTP(4)
}
// SMS: generating random otp 1234
// SMS: saving otp: 1234 to cache
// SMS: sending sms: SMS OTP for login is 1234
// EMAIL: generating random otp 1234
// EMAIL: saving otp: 1234 to cache
// EMAIL: sending email: EMAIL OTP for login is 1234
Rust Implementation
Example: Template Method Pattern
main.rs
trait TemplateMethod {
fn template_method(&self) {
self.base_operation1();
self.required_operations1();
self.base_operation2();
self.hook1();
self.required_operations2();
self.base_operation3();
self.hook2();
}
/// trait method in Rust can have default implementation
fn base_operation1(&self) {
println!("TemplateMethod says: I am doing the bulk of the work");
}
fn base_operation2(&self) {
println!("TemplateMethod says: But I let subclasses override some operations");
}
fn base_operation3(&self) {
println!("TemplateMethod says: But I am doing the bulk of the work anyway");
}
fn hook1(&self) {}
fn hook2(&self) {}
fn required_operations1(&self);
fn required_operations2(&self);
}
struct ConcreteStruct1;
impl TemplateMethod for ConcreteStruct1 {
fn required_operations1(&self) {
println!("ConcreteStruct1 says: Implemented Operation1")
}
fn required_operations2(&self) {
println!("ConcreteStruct1 says: Implemented Operation2")
}
fn hook1(&self) {
println!("ConcreteStruct1 says: hook1 is overridden")
}
}
struct ConcreteStruct2;
impl TemplateMethod for ConcreteStruct2 {
fn required_operations1(&self) {
println!("ConcreteStruct2 says: Implemented Operation1")
}
fn required_operations2(&self) {
println!("ConcreteStruct2 says: Implemented Operation2")
}
fn hook2(&self) {
println!("ConcreteStruct2 says: hook2 is overridden")
}
}
fn client_code(concrete: impl TemplateMethod) {
concrete.template_method()
}
fn main() {
println!("Same client code can work with different concrete implementations:");
client_code(ConcreteStruct1);
println!();
println!("Same client code can work with different concrete implementations:");
client_code(ConcreteStruct2);
}
// Same client code can work with different concrete implementations:
// TemplateMethod says: I am doing the bulk of the work
// ConcreteStruct1 says: Implemented Operation1
// TemplateMethod says: But I let subclasses override some operations
// ConcreteStruct1 says: hook1 is overridden
// ConcreteStruct1 says: Implemented Operation2
// TemplateMethod says: But I am doing the bulk of the work anyway
//
// Same client code can work with different concrete implementations:
// TemplateMethod says: I am doing the bulk of the work
// ConcreteStruct2 says: Implemented Operation1
// TemplateMethod says: But I let subclasses override some operations
// ConcreteStruct2 says: Implemented Operation2
// TemplateMethod says: But I am doing the bulk of the work anyway
// ConcreteStruct2 says: hook2 is overridden