Go Program Pattern 05: Decorations

分享

Go is a statically typed compiled language designed to be concise and efficient. While Go is not a purely object-oriented language, we can still use design patterns to improve code readability and maintainability. Today, I will introduce a common design pattern: the Decorator pattern.

What is the Decorator Pattern?

The Decorator pattern is a design pattern that allows us to dynamically add behavior to an object at runtime without altering its implementation. This is achieved by creating a wrapper or decorator containing the original object and providing an enhanced interface to add new behavior.

In Go, we can use functions as decorators because Go supports higher-order functions, which means functions can be passed as parameters and returned as values.

An Example

To better understand the Decorator pattern, let's see how we can implement it in Go through an example.

First, we define a function type Foo and a decorator type FooDecorator:

type Foo func(string) string

type FooDecorator func(Foo) Foo

Then, we can create a decorator that takes a function of type Foo and returns a new function of type Foo which adds some behavior before and after calling the original function:

func WithLog(decorated Foo) Foo {
    return func(s string) string {
        fmt.Println("Before calling the decorated function")
        result := decorated(s)
        fmt.Println("After calling the decorated function")
        return result
    }
}

Now, we can create a Foo function and enhance it using the decorator:

func main() {
    foo := func(s string) string {
        fmt.Println("Foo function called")
        return s
    }

    foo = WithLog(foo)

    foo("Hello, world!")
}

In this example, we create a Foo function and use the WithLog decorator to enhance it. When we call the enhanced function, it first prints a message, then calls the original Foo function, and finally prints another message.

This is the Decorator pattern in Go. By using decorators, we can dynamically add new behavior without modifying the original function.

Next, let's look at an example related to handling HTTP requests. First, we'll start with a simple HTTP server code:

package main

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

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithServerHeader()")
        w.Header().Set("Server", "HelloServer v0.0.1")
        h(w, r)
    }
}

func hello(w http.ResponseWriter, r *http.Request) {
    log.Printf("Received Request %s from %s\n", r.URL.Path, r.RemoteAddr)
    fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
    http.HandleFunc("/v1/hello", WithServerHeader(hello))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

In this code, we use the Decorator pattern. The WithServerHeader() function acts as a decorator that takes an http.HandlerFunc and returns a modified version. This example is relatively simple, as we only add a response header using WithServerHeader(). However, we can create many more functions like this, such as writing authentication cookies, checking authentication cookies, and logging.

package main

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

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithServerHeader()")
        w.Header().Set("Server", "HelloServer v0.0.1")
        h(w, r)
    }
}

func WithAuthCookie(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithAuthCookie()")
        cookie := &http.Cookie{Name: "Auth", Value: "Pass", Path: "/"}
        http.SetCookie(w, cookie)
        h(w, r)
    }
}

func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithBasicAuth()")
        cookie, err := r.Cookie("Auth")
        if err != nil || cookie.Value != "Pass" {
            w.WriteHeader(http.StatusForbidden)
            return
        }
        h(w, r)
    }
}

func WithDebugLog(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithDebugLog")
        r.ParseForm()
        log.Println(r.Form)
        log.Println("path", r.URL.Path)
        log.Println("scheme", r.URL.Scheme)
        log.Println(r.Form["url_long"])
        for k, v := range r.Form {
            log.Println("key:", k)
            log.Println("val:", strings.Join(v, ""))
        }
        h(w, r)
    }
}

func hello(w http.ResponseWriter, r *http.Request) {
    log.Printf("Received Request %s from %s\n", r.URL.Path, r.RemoteAddr)
    fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
    http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
    http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello)))
    http.HandleFunc("/v3/hello", WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

Pipeline of Multiple Decorators

When using multiple decorators, the code can become less visually appealing as we need to nest functions layer by layer. However, we can refactor the code to make it cleaner. To do this, we first write a utility function that iterates through and calls each decorator:

type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc

func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
    for i := range decors {
        d := decors[len(decors)-1-i] // iterate in reverse
        h = d(h)
    }
    return h
}

Then, we can use it like this:

http.HandleFunc("/v4/hello", Handler(hello,
                WithServerHeader, WithBasicAuth, WithDebugLog))

Conclusion

In this article, I demonstrated the Decorator pattern using two examples. However, since Go does not support annotations as a syntactic sugar, using decorators can be cumbersome. Nevertheless, the concept is still important, and we can apply this thinking to write higher-quality code in our daily development.

Read More

  • Go Program Pattern 01: Functional Options Pattern
  • Go Program Pattern 02: Implementing Class Inheritance and Method Overriding through Composition
  • Go Program Pattern 03: Inversion of Control
  • Go Program Pattern 04: Map-Reduce
  • Go Program Pattern 05: Decorations

阅读更多

那些意外变成特性的 Bug:当代码漏洞改写计算史

那些意外变成特性的 Bug:当代码漏洞改写计算史

那些意外变成特性的 Bug:当代码漏洞改写计算史. 比起草台班子将错就错的神话,面对异常数据时的观测定力才是真的 社交网络上常年流传着一类爽文:某家公司的程序员把代码写崩了,结果不仅没被开除,这个 Bug 反而成了拳头产品,妥妥的爽文主角剧本。 最常被搬出来的有三个:Audio Hijack 写错试用期拯救公。

By huizhou92