Dwarves
Memo
Type ESC to close search bar

A Tour of Template method pattern with Golang

Problem

Just imagine we need to implement a registration feature for our applications (web, mobile). A typical registration will have some basic steps such as fill in the form, verify account, redirect to login page, etc.

At the beginning, our apps only supported verification via email. After months, the team realized that the amount of mobile users significantly increased. So we decided to support more verification methods via SMS/authenticator app or allow to register without verification.

Concept

Template method is one of Behavioral design patterns. It can be defined by the followings:

Solution by Template method

The Template method offers us an answer to those issues from the above scenario. By applying the pattern, we break the operation (registration) down into multiple steps (receive user form, verify, welcome, redirect to login page, etc.) and create a method which invokes the steps in a specific order. Basically we construct a template by calling the steps inside a method. That’s why the template is called Template method.

All work above can be put together inside a single place (aka base class/type). The steps can either be abstract, or have default implementation:

Structure

Note: We can have multiple template methods, in case we need to use those same steps but in different order

Applicability

Use Template method pattern when you have multiple approaches to achieve your task, but they have many identical steps and just a few steps with minor differences

Use Template method pattern when:

Pseudocode (golang)

Since Go does not have abstract class and inheritance, the implementation will be a bit different

Step 1: create an interface (instead of abstract class) declaring abstract methods which represent every step of registration process

type IRegistration interface {
  Start()
  Collect()
  Verify()
  Welcome()
  Redirect()
} 

Step 2: Define template method Register() including steps’ invocations

func Register(r IRegistration) {
  r.Start()
  r.Collect()
  r.Verify()
  r.Welcome()
  r.Redirect()
}

Pay attention to the only paramter r, it will be determined and provided by the client (details in step 5)

Step 3: create base type BaseRegistration implements IRegistration

type Registration struct {
  Name     string
  Phone    string
  Email    string
  Verified bool
}

// step 1
func (r *Registration) Start() {
  println("Welcome to Dwarves Foundation")
}

// step 2
func (r *Registration) Collect() {
  // Receive and handle user inputs
  // ...
  // db.Save(r.Name, r.Phone, ...)
}

// step 3 implementation will be delegated for other sub types
// func (r *Registration) Verify()

// step 4 - display a welcome message to newcomer
// this is an optional step in registration process
// so we can either define it as a hook or provide a default implementation
func (r *Registration) Welcome() {
  status := ""
  if r.Verified {
    status = "✅"
  }
  fmt.Printf("Hi, %s %s\n", r.Name, status)
}

// step 5 - common step
func (r *Registration) Redirect() {
  println("Redirecting to login page ...")
  // context.Redirect('/login')
}

Step 4: Verify() implementations We use composition instead of inheritance in Go by embedding struct Registration

type Sms struct {
  Registration
}

func (s *Sms) Verify() {
  // generate code
  // send code using sms provider
  // ...
  println("Verification code sent to your phone")

  // verify code
  // ...
  r.Verified = true
  println("You have verified successfully!")
}

type Email struct {
  Registration
}

func (e *Email) Verify() {
  // generate code
  // send code using email provider
  // ...
  println("Verification code sent to your email")

  // verify code
  // ...
  r.Verified = true
  println("You have verified successfully!")
}

For example, we may also support non-verified registration in the future (limited features)

type NonVerified struct {
  Registration
}

func (v *NonVerified) Verify() {
  // nothing to do here
}

Step 5: Client code main.go - assume that we select verification method based on user device


func main() {
  // check user device
  ua := context.Header("User-Agent")
  var r registration.IRegistration
  switch true {
  case isDesktop(ua):
    r = &registration.Email{}
  case isMobile(ua):
    r = &registration.Sms{}
  default:
    r = &registration.NonVerified{}
  }

  // invoke template method
  registration.Register(r)
}

Benefits & Drawbacks

Benefits

Drawbacks

References