Notes: Go Design Patterns - Chain of Responsibility

Notes: Go Design Patterns - Chain of Responsibility

This blog implements chain of responsibility design pattern using the middleware concept

ยท

4 min read

Table of contents

No heading

No headings in the article.

Chain of responsibility pattern decouples request and receiver. It adds a chain of actions between request and receiver which may modify the request for the next step or make some checks or stop the request altogether. This is a behavioral pattern.

The best example of this pattern is middleware layers in our web development frameworks, so I'll use that only as an example.

Our middleware as an object of the chain has the responsibility of taking requests and sending a response for the next middleware. Next middleware can also be defined with each middleware. So, our middleware interface can look something like this:

type middleware interface {
    handle(request, response)
    setNext(middleware)
}

Now, let's use a logger middleware that logs all the requests on the console. Its concrete implementation can look something like this:

type accessLogger struct {
    next middleware
}

func (aL *accessLogger) handle(req request, w response) {
    log.Printf("Method: %v, URL Path: %v, Remote Address: %v, Time: %v\n", req.Method, req.URL.Path, req.RemoteAddr, time.Now().UTC())

    aL.next.handle(req, w)
}

func (aL *accessLogger) setNext(next middleware) {
    aL.next = next
}

It implements both interface methods to handle requests and add the next middleware in the chain.

Now, in the same way, we can add other middleware in the chain and use them to work on our request before sending it to the final receiver.

In the below example, we have created a GET API http://localhost:8090/getUser/{id}. This API takes auth_token as header and id as path param. Here we have created a dummy data:

const (
    ID         = 101
    NAME       = "Rick Sanchez"
    AGE        = 727
    ADDRESS    = "Earth Dimension C-137"
    AUTH_TOKEN = "WUBBALUBBADUBDUB"
)

Now, start the API and try with the following requests and see the responses. You'll be able to track the role of each middleware:

  1. Invalid auth_token:

    curl --location --request GET 'http://localhost:8090/getUser/102' \
    --header 'auth_token: WUBBALUBBADUBDU'
    
  2. Invalid user:

    curl --location --request GET 'http://localhost:8090/getUser/102' \
    --header 'auth_token: WUBBALUBBADUBDUB'
    
  3. All valid:
    curl --location --request GET 'http://localhost:8090/getUser/101' \
    --header 'auth_token: WUBBALUBBADUBDUB'
    
    Complete working code:
package main

import (
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "time"
)

// fake user info
// add json data for more versatility
// or add db for more practical example
const (
    ID         = 101
    NAME       = "Rick Sanchez"
    AGE        = 727
    ADDRESS    = "Earth Dimension C-137"
    AUTH_TOKEN = "WUBBALUBBADUBDUB"
)

// middleware interface
type middleware interface {
    handle(request, response)
    setNext(middleware)
}

// request, response type
type request *http.Request
type response *http.ResponseWriter

// handlers implementation
// accessLogger middleware
type accessLogger struct {
    next middleware
}

func (aL *accessLogger) handle(req request, w response) {
    log.Printf("Method: %v, URL Path: %v, Remote Address: %v, Time: %v\n", req.Method, req.URL.Path, req.RemoteAddr, time.Now().UTC())

    aL.next.handle(req, w)
}

func (aL *accessLogger) setNext(next middleware) {
    aL.next = next
}

type authCheck struct {
    next middleware
}

// verifying auth_token
func (aC *authCheck) handle(req request, w response) {
    authToken := req.Header.Get("auth_token")

    if authToken == AUTH_TOKEN {
        aC.next.handle(req, w)
    } else {
        http.Error(*w, "invalid auth token!", http.StatusUnauthorized)
        *w = nil // TODO: let me know proper approach
    }
}

func (aC *authCheck) setNext(next middleware) {
    aC.next = next
}

type dataValidation struct {
    next middleware
}

// data validation
// check if id is a valid number
func (dV *dataValidation) handle(req request, w response) {
    userId := strings.TrimPrefix(req.URL.Path, "/getUser/")

    intId, err := strconv.Atoi(userId)
    if err != nil {
        http.Error(*w, "user id should be a number!", http.StatusAccepted)
        *w = nil
        return
    }

    if intId != ID {
        http.Error(*w, "user doesn't exist!", http.StatusOK)
        *w = nil
    }
}

func (dV *dataValidation) setNext(next middleware) {
    dV.next = next
}

func getUserHandler(middlewares []middleware) func(http.ResponseWriter, *http.Request) {
    // should be set in main func
    return func(w http.ResponseWriter, req *http.Request) {
        for i := 0; i < (len(middlewares) - 1); i++ {
            middlewares[i].setNext(middlewares[i+1])
        }

        middlewares[0].handle(req, &w)

        if w != nil {
            fmt.Fprintf(w, "[User Details]\nName: %v, Age: %v, Address: %v", NAME, AGE, ADDRESS)
        }
    }
}

func main() {
    // middlewares to be called sequentially
    middlewares := []middleware{&accessLogger{}, &authCheck{}, &dataValidation{}}

    // equivalent to "/getUser/:id"
    http.HandleFunc("/getUser/", getUserHandler(middlewares))

    http.ListenAndServe(":8090", nil)
}
ย