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-
- CDN – for CSS and JS files
- Disk – for large data size
- 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 CDNn", 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 Redisn", 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 Diskn", 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
- Phone Interview
- Technical Interview
- HR Interview
- 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.