Design Pattern: Chain of Responsibility Pattern in Go

Chain of Responsibility pattern chains the steps of processing and performs the process step by step. So the processing does not depend on a single object, but multiple objects are responsible for the processing, which is decided on the fly while processing.

This article demonstrates Chain of Responsibility pattern implementations in Golang. Check the following examples.

Implementation

Follow the steps below for Chain of responsibility pattern implementation-

  • Create an interface for ensuring a common interface for each step. Declare method as required, make sure to add one method for the main execution.
  • Create struct for each step.
  • In the struct declare a field of the type of the interface. This will be used to store the information/reference to the next step.
  • Implement the interface for the struct. In the main execution method perform all required operations for that step, then call the next step execution method (if next step is defined).

Here is a simple example-

// Chain of responsiblity pattern implementaion in Go

package main

import "fmt"

// Step interface
type Step interface {
	Execute()
}

// Step 1
type Step1 struct {
	nextStep Step
}

func NewStep1(nextStep Step) (step1 *Step1) {
	step1 = &Step1{}
	step1.nextStep = nextStep
	return
}

func (step1 *Step1) Execute() {
	// Perform all required step for this step
	fmt.Println("Execute all operation for Step1 processing")

	// Call the next step execution if the step is defined
	if step1.nextStep != nil {
		step1.nextStep.Execute()
	}
}

// Step 2
type Step2 struct {
	nextStep Step
}

func NewStep2(nextStep Step) (step2 *Step2) {
	step2 = &Step2{}
	step2.nextStep = nextStep
	return
}

func (step2 *Step2) Execute() {
	// Perform all required step for this step
	fmt.Println("Execute all operation for Step2 processing")

	// Call the next step execution if the step is defined
	if step2.nextStep != nil {
		step2.nextStep.Execute()
	}
}

// Step 1
type Step3 struct {
	nextStep Step
}

func NewStep3(nextStep Step) (step3 *Step3) {
	step3 = &Step3{}
	step3.nextStep = nextStep
	return
}

func (step3 *Step3) Execute() {
	// Perform all required step for this step
	fmt.Println("Execute all operation for Step3 processing")

	// Call the next step execution if the step is defined
	if step3.nextStep != nil {
		step3.nextStep.Execute()
	}
}

// demo

func main() {
	steps := NewStep1(NewStep2(NewStep3(nil)))

	steps.Execute()
}

Output will be as below-

Execute all operation for Step1 processing

Execute all operation for Step2 processing

Execute all operation for Step3 processing

Examples

Let’s check a few examples of the Chain of Responsibility pattern implementation in Golang.

Example #1: Caching Data

In this example we have 3 caching mechanisms-

  1. CDN – for CSS and JS files
  2. Disk – for large data size
  3. Redis – for smaller data size

Let’s see how we can implement that using the Chain of Responsibility.

Data Struct

  • Create file “data.go”.
  • Define constants for “CSS”, “JAVASCRIPT”, or “DATA” type. 
  • Create struct “Data”.
  • Define fields – “dataType”, “key”, “value”.
  • Create method “NewData”, which creates a new struct of type “Data”.
  • Define methods as per requirement. Here we have get methods – “GetDataType”, “GetKey”, and “GetValue”. 
// data.go

package main

type DATA_TYPE int

const (
	DATA DATA_TYPE = iota
	JAVASCRIPT
	CSS
)

type Data struct {
	dataType DATA_TYPE
	key  string
	value string
}

func NewData(dataType DATA_TYPE, key string, value string) (data *Data) {
	data = &Data{}
	data.dataType = dataType
	data.key = key
	data.value = value
	return
}

func (data *Data) GetValue() (string) {
	return data.value
}

func (data *Data) GetKey() (string) {
	return data.key
}

func (data *Data) GetDataType() (DATA_TYPE) {
	return data.dataType
}

Cache Handler

  • Create file “cache_handler.go”.
  • Define interface “CacheHandler”.
  • Declare method “HandleRequest”.
// cache_handler.go

package main

type CacheHandler interface {
	HandleRequest(data Data)
}

CDN Cache Handler

  • Create file “cdn_cache_handler.go”.
  • Create struct “CdnCacheHandler”.
  • Declare a field “nextCacheHandler” of type “CacheHandler”. This will hold the information on the next step.
  • Create method “NewCdnCacheHandler”. This method accepts the next step as param, and sets that while creating a new struct.
  • Implement interface “CacheHandler”. Define the method “HandleRequest” as part of the interface implementation.
// cdn_cache_handler.go

package main

import "fmt"

type CdnCacheHandler struct {
	nextCacheHandler CacheHandler
}

func NewCdnCacheHandler(nextCacheHandler CacheHandler) (cdnCacheHandler *CdnCacheHandler) {
	cdnCacheHandler = &CdnCacheHandler{}
	cdnCacheHandler.nextCacheHandler = nextCacheHandler
	return
}

func (cdnCacheHandler *CdnCacheHandler) HandleRequest(data Data) {
	if data.GetDataType() == CSS || data.GetDataType() == JAVASCRIPT {
		fmt.Printf("Caching file '%v' in CDN\n", data.GetKey())
	} else if cdnCacheHandler.nextCacheHandler != nil {
		cdnCacheHandler.nextCacheHandler.HandleRequest(data)
	}
}

Redis Cache Handler

  • Create file “redis_cache_handler.go”.
  • Create struct “RedisCacheHandler”.
  • Declare a field “nextCacheHandler” of type “CacheHandler” to hold the information on the next step.
  • Create method “NewRedisCacheHandler” for creating a new struct.
  • Implement interface “CacheHandler”. Define the method “HandleRequest” as part of the interface implementation.
// redis_cache_handler.go

package main

import "fmt"

type RedisCacheHandler struct {
	nextCacheHandler CacheHandler
}

func NewRedisCacheHandler(nextCacheHandler CacheHandler) (redisCacheHandler *RedisCacheHandler) {
	redisCacheHandler = &RedisCacheHandler{}
	redisCacheHandler.nextCacheHandler = nextCacheHandler
	return
}

func (redisCacheHandler *RedisCacheHandler) HandleRequest(data Data) {
	if data.GetDataType() == DATA && len(data.GetValue()) <= 1024 {
		fmt.Printf("Caching data '%v' in Redis\n", data.GetKey())
	} else if redisCacheHandler.nextCacheHandler != nil {
		redisCacheHandler.nextCacheHandler.HandleRequest(data)
	}
}

Disk Cache Handler

  • Create file “disk_cache_handler.go”.
  • Create struct “DiskCacheHandler”.
  • Declare a field “nextCacheHandler” of type “CacheHandler”.
  • Create method “NewDiskCacheHandler” for creating a new struct and setting “nextCacheHandler”.
  • Define the method “HandleRequest” for the struct as part of the “CacheHandler” interface implementation.
// disk_cache_handler.go

package main

import "fmt"

type DiskCacheHandler struct {
	nextCacheHandler CacheHandler
}

func NewDiskCacheHandler(nextCacheHandler CacheHandler) (diskCacheHandler *DiskCacheHandler) {
	diskCacheHandler = &DiskCacheHandler{}
	diskCacheHandler.nextCacheHandler = nextCacheHandler
	return
}

func (diskCacheHandler *DiskCacheHandler) HandleRequest(data Data) {
	if data.GetDataType() == DATA && len(data.GetValue()) > 1024 {
		fmt.Printf("Caching data '%v' in Disk\n", data.GetKey())
	} else if diskCacheHandler.nextCacheHandler != nil {
		diskCacheHandler.nextCacheHandler.HandleRequest(data)
	}
}

Demo

Chain the handlers and then call the “HandleRequest” method for using the implementation.

// main.go

package main

func main() {
	cacheHandler := NewDiskCacheHandler(NewRedisCacheHandler(NewCdnCacheHandler(nil)))

	data := NewData(DATA, "key1", "ABC320489un3429rn29urn29r82n9jfdn2")
	cacheHandler.HandleRequest(*data)

	data = NewData(CSS, "key2", ".some-class{border: 1px solid red; margin: 10px}")
	cacheHandler.HandleRequest(*data)
}

Output

Following output will be generated.

Caching data 'key1' in Redis

Caching file 'key2' in CDN

Example #2: Interview

Let’s consider an interview process that has a few steps

  1. Phone Interview
  2. Technical Interview
  3. HR Interview
  4. Interview with the CEO

We can create separate structs for each interview and then chain those stucts to perform interviews one by one.

Interview Interface

  • Create file “interview.go”.
  • Define interface “Interview”.
  • Declare a method “Execute”.
// interview.go

package main

type Interview interface {
	Execute()
}

Phone Interview

  • Create file “phone_interview.go”.
  • Create struct “PhoneInterview”.
  • Declare field “nextInterview” of type “Interview” for the struct. This will hold the information on the next step.
  • Create method “NewPhoneInterview” creating and returning a new “PhoneInterview” struct. This method accepts a param of type “Interview” and sets that to the “nextInterview” field.
  • Implement interface “Interview”. Define the method “Execute” as part of the interface implementation. Ask all questions for phone interview and then call the next interview process.
// phone_interview.go

package main

import "fmt"

type PhoneInterview struct {
	nextInterview Interview
}

func NewPhoneInterview(nextInterview Interview) (phoneInterview *PhoneInterview) {
	phoneInterview = &PhoneInterview{}
	phoneInterview.nextInterview = nextInterview
	return
}

func (phoneInterview *PhoneInterview) Execute() {
	// Ask all questions for phone interview
	// Perform any other action required for phone interview
	fmt.Println("Ask phone interview questions")

	// Execute the next interview set while creating new struct
	if phoneInterview.nextInterview != nil {
		phoneInterview.nextInterview.Execute()
	}
}

Technical Interview

  • Create file “technical_interview.go”.
  • Create struct “TechnicalInterview”.
  • Declare the field “nextInterview” of type “Interview” for the struct.
  • Create method “NewTechnicalInterview” creating and returning a new “TechnicalInterview” struct.
  • Define the method “Execute” as part of the “Interview” interface implementation. Perform all operations required for the technical interview. Then call the next process at the end.
// technical_interview.go

package main

import "fmt"

type TechnicalInterview struct {
	nextInterview Interview
}

func NewTechnicalInterview(nextInterview Interview) (technicalInterview *TechnicalInterview) {
	technicalInterview = &TechnicalInterview{}
	technicalInterview.nextInterview = nextInterview
	return
}

func (technicalInterview *TechnicalInterview) Execute() {
	// Ask all questions for technical interview
	// Perform any other action required for technical interview
	fmt.Println("Ask technical interview questions")

	// Execute the next interview set while creating new struct
	if technicalInterview.nextInterview != nil {
		technicalInterview.nextInterview.Execute()
	}
}

HR Interview

  • Create file “hr_interview.go”.
  • Create struct “HrInterview”.
  • Declare the field “nextInterview” of type “Interview” for the struct.
  • Create method “NewHrInterview” creating and returning a new “HrInterview” struct.
  • Define the method “Execute” as part of the “Interview” interface implementation. Perform all operations required for the HR interview, then call the next process at the end.
// hr_interview.go

package main

import "fmt"

type HrInterview struct {
	nextInterview Interview
}

func NewHrInterview(nextInterview Interview) (hrInterview *HrInterview) {
	hrInterview = &HrInterview{}
	hrInterview.nextInterview = nextInterview
	return
}

func (hrInterview *HrInterview) Execute() {
	// Ask all questions for Hhr interview
	// Perform any other action required for Hhr interview
	fmt.Println("Ask HR interview questions")

	// Execute the next interview set while creating new struct
	if hrInterview.nextInterview != nil {
		hrInterview.nextInterview.Execute()
	}
}

CEO Interview

  • Create file “ceo_interview.go”.
  • Create struct “CeoInterview”.
  • Declare the field “nextInterview” of type “Interview” for the struct.
  • Create method “NewCeoInterview” creating and returning a new “CeoInterview” struct. Set the “nextInterview” value after initializing the struct.
  • Define the method “Execute” as part of the “Interview” interface implementation. Perform all operations required for the Ceo interview, call the next process at the end.
// ceo_interview.go

package main

import "fmt"

type CeoInterview struct {
	nextInterview Interview
}

func NewCeoInterview(nextInterview Interview) (ceoInterview *CeoInterview) {
	ceoInterview = &CeoInterview{}
	ceoInterview.nextInterview = nextInterview
	return
}

func (ceoInterview *CeoInterview) Execute() {
	// Ask all questions for ceo interview
	// Perform any other action required for ceo interview
	fmt.Println("Ask CEO interview questions")

	// Execute the next interview set while creating new struct
	if ceoInterview.nextInterview != nil {
		ceoInterview.nextInterview.Execute()
	}
}

Demo

Chain the struct calls and call the execute method after that. The process in each structs will be executed step by step.

We can chain the structs in any sequence to decide which step comes after which one.

// main.go

package main

func main() {
	interview := NewPhoneInterview(NewTechnicalInterview(NewHrInterview(NewCeoInterview(nil))))

	interview.Execute()
}

Output

Output will be as below

Ask phone interview questions

Ask technical interview questions

Ask HR interview questions

Ask CEO interview questions

Source Code

Use the following link to get the source code:

Other Code Implementations

Use the following links to check the Chain of Responsibility pattern implementation in other programming languages.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.