Chain of Responsibilities design pattern is also known as "pipeline" in modern era of software engineering design. It is done by passing the responsibilities from one execution set to another until the chain is completed. Each block has its own way to source input, identify handler (both failed and next), and its own execution codes.
Here is a Chain of Responsibilities in Go:
package main
import (
"fmt"
"time"
)
/*****************************************************************************
* Chain Handler Structure *
*****************************************************************************/
type Handler struct {
Data interface{}
Next func(super *Handler)
}
func (h *Handler) Handle() {
if h.Next == nil {
return
}
go h.Next(h)
}
/*****************************************************************************
* List of Processes and Data Structure *
*****************************************************************************/
type DispenserData struct {
Balance int
Thousand int
Hundred int
}
func Process1(value int) {
h := &Handler{
Data: &DispenserData{
Balance: value,
},
Next: Process2,
}
h.Handle()
}
func Process2(h *Handler) {
if h == nil {
// fatal error. Ghost handler.
return
}
data, ok := h.Data.(*DispenserData)
if !ok {
h.Next = OutProcessError
h.Handle()
return
}
for {
if data.Balance < 1000 {
break
}
data.Balance -= 1000
data.Thousand++
}
// next process block
h.Next = Process3
h.Handle()
}
func Process3(h *Handler) {
if h == nil {
// fatal error. Ghost handler.
return
}
data, ok := h.Data.(*DispenserData)
if !ok {
h.Next = OutProcessError
h.Handle()
return
}
for {
if data.Balance < 100 {
break
}
data.Balance -= 100
data.Hundred++
}
// next process block
h.Next = OutProcessPassed
if data.Balance != 0 {
h.Next = OutProcessFailed
}
h.Handle()
}
func OutProcessError(h *Handler) {
if h == nil {
// fatal error. Ghost handler.
return
}
fmt.Printf("Error: missing parameters\n")
// next process block
h.Next = OutProcessEnd
h.Handle()
}
func OutProcessFailed(h *Handler) {
if h == nil {
// fatal error. Ghost handler.
return
}
data, ok := h.Data.(*DispenserData)
if !ok {
h.Next = OutProcessError
h.Handle()
return
}
fmt.Printf("Error: unable to sort remaining balance: %v\n",
data.Balance)
// next process block
h.Next = OutProcessEnd
h.Handle()
}
func OutProcessPassed(h *Handler) {
if h == nil {
// fatal error. Ghost handler.
return
}
data, ok := h.Data.(*DispenserData)
if !ok {
h.Next = OutProcessError
h.Handle()
return
}
fmt.Printf("Total: %v 1000 notes, %v 100 notes\n",
data.Thousand,
data.Hundred)
// next process block
h.Next = OutProcessEnd
h.Handle()
}
func OutProcessEnd(h *Handler) {
fmt.Printf("Press CTRL + C to exit\n")
}
/*****************************************************************************
* Client Request *
*****************************************************************************/
func main() {
Process1(1520)
<-time.After(10 * time.Second)
fmt.Printf("timeout the program. Bye!\n")
}
// Output:
// Error: unable to sort remaining balance: 20
// Press CTRL + C to exit
// timeout the program. Bye!
Generally, the chain happens when each processes are split from one another and then determine the next process "on-the-fly". Notice that the client is able to run Process1
asynchronously to the point where we need to set a timeout for the entire execution.
In the Process1
, it "gathers" the input and creates the first handler object for chaining. Then it identifies the next process and call the handle.
Notice Process3
and OutProcessFailed
are able to determine the next step based on the result it processes. Hence, the subsequent happy steps are no longer in the chain.