源碼學啥子嘛?接口、組合

大家好,我叫謝偉,是一名程序員。

今天的主題:面向接口、組合編程。

作爲程序員,都希望編寫通用、可擴展的代碼,通常這些知識靠的都是依靠設計模式進行指導開發。比如說面向對象的特性:封裝、抽象、多態、繼承。

要編寫更通用的代碼,一方面需要靠足夠時間砸出來,一方面也需要自己實踐摸索。編寫代碼過程中要時刻在腦中形成清單:

  • 編寫可讀的代碼
  • 編寫符合設計模式的代碼

在 Go 中如何編寫更通用的代碼?

一是接口,二是組合。

Go 中沒有繼承的概念,摒除了”繼承“可能導致層級過多的弊端,轉而推薦使用組合的形式,達到”繼承“的效果。

舉個簡單的示例:


type Languager interface {
    Can(string) string
}

type Someone struct {
    Language string
}

func (s Someone) Can(name string) string {
    return fmt.Sprintf("%s can program with %s", name, s.Language)
}

func Program(L Languager, name string) {
    log.Println(L.Can(name))
}


func main() {
    b := Someone{Language: "go"}
    Program(b, "謝謝")

}
>>2019/12/26 11:10:55 謝謝 can program with go

定義了一個接口:Languager 具備 Can 這個方法, Someone 結構體存在 Can 這個方法(參數、返回值一致),我們就說:Someone 實現了 Languager 接口。

接口是一系列“協議”的組合,描述其具備的抽象的能力,具體的實現依靠的是結構體具體的方法。

type OtherOne struct {
    Speaker string
}

func (o OtherOne) Can(name string) string {
    return fmt.Sprintf("%s can speake %s", name, o.Speaker)
}

func main(){
    b := Someone{Language: "go"}
    Program(b, "謝謝")
    o := OtherOne{Speaker: "English"}
    Program(o, "不客氣")
}
>>2019/12/26 11:24:39 謝謝 can program with go
>>2019/12/26 11:24:39 不客氣 can speake English

Someone 真實的方法(Can)是描述在"編程"層面的,OtherOne 真實的方法(Can)是描述其在"語言"層面的。但都是一種能力的描述,兩者都實現了 Languager 接口。

聚焦在“編程”層面的示例,編程語言有多種,那麼你覺得是設計比較全而統一的接口好?還是設計職責單一的接口好?

選擇職責單一的設計方法

有句話怎麼說的來着?什麼都想要,什麼都得不到。

type Gopher interface {
    Program(string) string
}

type Student struct {
    Name string
}

func (S Student) Program(language string) string {
    return fmt.Sprintf("%s 會寫 %s,叫他 Gopher。", S.Name, language)
}

func Go(body Gopher) {
    log.Println(body.Program("Go"))
}

type PHPer interface {
    Do(string) string
}

type Teacher struct {
    Name string
}

func (T Teacher) Do(language string) string {
    return fmt.Sprintf("%s 會教 %s,叫他 PHPer。", T.Name, language)
}

func Php(body PHPer) {
    log.Println(body.Do("Php"))
}

type Pythoner interface {
    Run(string) string
}

type Roommate struct {
    Name string
}

func (R Roommate) Run(language string) string {
    return fmt.Sprintf("%s 會學 %s,叫她 Pythoner。", R.Name, language)
}

func Python(body Pythoner) {
    log.Println(body.Run("Python"))
}

func main(){
    s := Student{Name: "謝小路"}
    t := Teacher{Name: "謝小人"}
    r := Roommate{Name: "謝小甲"}

    Go(s)
    Php(t)
    Python(r)
}
>>2019/12/26 12:19:36 謝小路 會寫 Go,叫他 Gopher。
>>2019/12/26 12:19:36 謝小人 會教 Php,叫他 PHPer。
>>2019/12/26 12:19:36 謝小甲 會學 Python,叫她 Pythoner。

多種能力的組合:

type Gopher interface {
    Program(string) string
}

type Student struct {
    Name string
}

func (S Student) Program(language string) string {
    return fmt.Sprintf("%s 會寫 %s,叫他 Gopher。", S.Name, language)
}

func (S Student) Run(language string) string {
    return fmt.Sprintf("%s 也會寫 %s", S.Name, language)
}

func Go(body Gopher) {
    log.Println(body.Program("Go"))
}

type PHPer interface {
    Do(string) string
}

type Teacher struct {
    Name string
}

func (T Teacher) Do(language string) string {
    return fmt.Sprintf("%s 會教 %s,叫他 PHPer。", T.Name, language)
}

func Php(body PHPer) {
    log.Println(body.Do("Php"))
}

type Pythoner interface {
    Run(string) string
}

type Roommate struct {
    Name string
}

func (R Roommate) Run(language string) string {
    return fmt.Sprintf("%s 會學 %s,叫她 Pythoner。", R.Name, language)
}

func Python(body Pythoner) {
    log.Println(body.Run("Python"))
}

type AwesomeDeveloper interface {
    Gopher
    Pythoner
}

func Development(a AwesomeDeveloper) {
    log.Println(a.Program("go"))
    log.Println(a.Run("python"))
}

func main(){

    s := Student{Name: "謝小路"}
    t := Teacher{Name: "謝小人"}
    r := Roommate{Name: "謝小甲"}

    Go(s)
    Php(t)
    Python(r)

    Development(s)
}

>>2019/12/26 12:24:31 謝小路 會寫 Go,叫他 Gopher。
>>2019/12/26 12:24:31 謝小人 會教 Php,叫他 PHPer。
>>2019/12/26 12:24:31 謝小甲 會學 Python,叫她 Pythoner。
>>2019/12/26 12:24:31 謝小路 會寫 go,叫他 Gopher。
>>2019/12/26 12:24:31 謝小路 也會寫 python

單一職責的設計方法,可以進行組合,創造出更多的“能力”,比如會兩種及以上的編程語言,示例中 AwesomeDeveloper.

可以看出:接口是一堆協議,描述其能力,不實現,接口可以被多個結構體實現,同一個結構體也可以實現多個接口。

內置庫中可以看到諸多的使用接口的示例,比如 io 庫,定義:Reader、Writer、Closer、Seeker...,具體的實現散佈在各種庫中。

這種做法有什麼好處?分層(或者說是隔離)。

  • 上游層和下游層通過接口進行關聯,但兩層之間沒有相互依賴
  • 上游層使用接口描述,穩定,不會輕易改動
  • 下游層側重實現,需求變更,更改對應的實現即可

這麼說,有點抽象,找個具體的例子:go-elasticsearch

大家都知道 elasticsearch 是開源的搜索引擎,對外暴露的是豐富的 RESTful 接口,多豐富呢?上百個吧。那麼如果要編寫個客戶端庫,面對如此多的 RESTful 接口,一方面需要考慮的是如何進行組織,一方面考慮的是如何應對 elasticsearch 本身的不斷迭代帶來的 API 接口變動。

調用 RESTful API , 無外乎這麼幾個動作:

  • 構造請求參數:比如 URL、HEADER、Method 等
  • 發起網絡請求:比如 http.Get
  • 組織響應信息: Response

基於此,官方源代碼在其中進行了接口設計:

// 描述其 Do 能力
type Request interface {
    Do(ctx context.Context, transport Transport) (*Response, error)
}

// 描述其 Perform 能力
type Transport interface {
    Perform(*http.Request) (*http.Response, error)
}

//  自定義的響應信息
type Response struct {
    StatusCode int
    Header     http.Header
    Body       io.ReadCloser
}

官方還劃分爲三層組織代碼結構:

1. esapi API 接口層

這一層主要做的事是:組織所有 API 請求參數、響應等。但實際上並沒有真實的發起網絡請求,而只是借用了Transport 接口的能力。

抽取其中一個接口查看下源代碼:

curl http://localhost:9200/_cat/health

>>1577337625 05:20:25 es-clustername green 3 3 24 11 0 0 0 0 - 100.0%

具體的源碼:esapi/api.cat.health.go

type CatHealth func(o ...func(*CatHealthRequest)) (*Response, error)

type CatHealthRequest struct {
    ...
}

func (r CatHealthRequest) Do(ctx context.Context, transport Transport) (*Response, error) {
    var (
        method string
        path   strings.Builder
        params map[string]string
    )

    method = "GET"

    path.Grow(len("/_cat/health"))
    path.WriteString("/_cat/health")

    params = make(map[string]string)

    if r.Format != "" {
        params["format"] = r.Format
    }

    if len(r.H) > 0 {
        params["h"] = strings.Join(r.H, ",")
    }

    if r.Help != nil {
        params["help"] = strconv.FormatBool(*r.Help)
    }

    if len(r.S) > 0 {
        params["s"] = strings.Join(r.S, ",")
    }

    if r.Time != "" {
        params["time"] = r.Time
    }

    if r.Ts != nil {
        params["ts"] = strconv.FormatBool(*r.Ts)
    }

    if r.V != nil {
        params["v"] = strconv.FormatBool(*r.V)
    }

    if r.Pretty {
        params["pretty"] = "true"
    }

    if r.Human {
        params["human"] = "true"
    }

    if r.ErrorTrace {
        params["error_trace"] = "true"
    }

    if len(r.FilterPath) > 0 {
        params["filter_path"] = strings.Join(r.FilterPath, ",")
    }

    req, err := newRequest(method, path.String(), nil)
    if err != nil {
        return nil, err
    }

    if len(params) > 0 {
        q := req.URL.Query()
        for k, v := range params {
            q.Set(k, v)
        }
        req.URL.RawQuery = q.Encode()
    }

    if len(r.Header) > 0 {
        if len(req.Header) == 0 {
            req.Header = r.Header
        } else {
            for k, vv := range r.Header {
                for _, v := range vv {
                    req.Header.Add(k, v)
                }
            }
        }
    }

    if ctx != nil {
        req = req.WithContext(ctx)
    }

    res, err := transport.Perform(req)
    if err != nil {
        return nil, err
    }

    response := Response{
        StatusCode: res.StatusCode,
        Body:       res.Body,
        Header:     res.Header,
    }

    return &response, nil
}

其中 Do 方法看上去很長,其實只在做這三件事:

  • 組織請求參數
  • 發起請求
  • 組織響應信息

其中發起請求步驟,只是借用了 Transport 的 Perform 能力,得出的 res, 進行重新組織成自定義的 Response。

那麼肯定有地方要真實的實現 Transport 的 Perform 能力,才能真實的發起網絡請求。

最後所有 RESTful 請求進行組合:esapi/api._.go

type API struct {
    Cat        *Cat
    Cluster    *Cluster
    Indices    *Indices
    ...
}

type Cat struct {
    Aliases      CatAliases
    Allocation   CatAllocation
    Count        CatCount
    Fielddata    CatFielddata
    Health       CatHealth
    ...
}

func New(t Transport) *API {
    return &API{
        Bulk:                                          newBulkFunc(t),
        ...
}

2. estransport 層

這層主要描述連接、傳輸的能力。即和 es 集羣連接的設置和真實的發起網絡請求的實現。

type Interface interface {
    Perform(*http.Request) (*http.Response, error)
}

type Client struct {
    ...
    transport http.RoundTripper
    ...
}

func (c *Client) Perform(req *http.Request) (*http.Response, error) {
        ...
        start := time.Now().UTC()
        res, err = c.transport.RoundTrip(req)
        dur := time.Since(start)
        ...

    
}

沒錯,真實的發起網絡請求的靠的是 http.RoundTripper,實際上 http.RoundTripper 也是個接口。

type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}

初始化 client 的時候,使用了默認的 http.RoundTripper 實現方案:http.DefaultTransport

func New(cfg Config) *Client {
    if cfg.Transport == nil {
        cfg.Transport = http.DefaultTransport
    }
    ...
}

這樣 定義的 Client 既實現了 Interface 接口,又實現了 Transport 接口。雖然兩者描述的能力一模一樣。

那麼這兩層之間本身沒什麼依賴,那麼如何交互呢?

func (r CatHealthRequest) Do(ctx context.Context, transport Transport) (*Response, error)

每個請求的 Do 方法接受 Transport 參數,實例化 estransport 層的 client, 將實例化的 client 作爲參數傳給 Do 方法即可。但兩者本身之間無耦合關係。

3. elasticsearch 層

定義上游 client 層。這層 esapi 層的 API 和 estransport 層的 Interface 組合起來。

type Client struct {
    *esapi.API // Embeds the API methods
    Transport  estransport.Interface
}

func NewClient(cfg Config) (*Client, error) {
    ...
    tp := estransport.New(estransport.Config{
        ...

        Transport:          cfg.Transport,
        ...
    })

    client := &Client{Transport: tp, API: esapi.New(tp)}
}

爲什麼這樣啊?明明 esapi 層和 estransport 層就可以完成任務啊?

簡單的說:esapi 和 estransport 配合使用的方式,最後的調用結果像這樣:

            req := esapi.IndexRequest{
                Index:      "test",
                DocumentID: strconv.Itoa(i + 1),
                Body:       strings.NewReader(b.String()),
                Refresh:    "true",
            }

            // Perform the request with the client.
            res, err := req.Do(context.Background(), es)

而具有了elasticsearch 層之後,調用的方式像這樣:

    es, err := elasticsearch.NewDefaultClient()
    es.Cat.Health()

簡單的說:上游暴露給用戶的信息更少,方便其使用,不讓用戶知道關於實現的更多細節,推薦使用第二種方式。

其實這種實現方式也簡單:就是將 Resquest 的 Do 方法再封裝一層,整成函數的類型.

type CatHealth func(o ...func(*CatHealthRequest)) (*Response, error)

func newCatHealthFunc(t Transport) CatHealth {
    return func(o ...func(*CatHealthRequest)) (*Response, error) {
        var r = CatHealthRequest{}
        for _, f := range o {
            f(&r)
        }
        return r.Do(r.ctx, t)
    }
}

type Cat struct {
    ...
    Health       CatHealth
    ...

}

基於此 elasticsearch 三層模型大概就是這樣,其實內部還大量的使用了面向接口、組合的編程思想。讀者可以根據源碼去探討研究。

看完就結束了嗎?

不,我要借鑑相似的思想,自己實現一個,於是有了這個項目:cartooncharts ,js 的具體實現查看:chart.xkcd

下游層:側重在細節實現層面

定義接口: charts.go

type ChartsInterface interface {
    Plot(t Transport) func(w http.ResponseWriter, r *http.Request)
    Save(string, Transport) bool
    Render(t Transport) func(w http.ResponseWriter, r *http.Request)
}

type Transport interface {
    Execute(w http.ResponseWriter, r *http.Request, v interface{})
    Read(name string) ([]byte, error)
}

某種類型的圖表實現:

type BarRequest struct {
    WithTitle
    WithXLabel
    WithYLabel
    WithDataCollection
    WithOption
}

func (bar BarRequest) Plot(t Transport) func(w http.ResponseWriter, r *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        v := struct {
            Type      string
            Interface BarRequest
        }{
            Type:      barStackedType,
            Interface: bar,
        }
        t.Execute(w, r, v)
    }
}

沒有具體的實現,只是借用了 Transport 的 Execute 的能力。

傳輸層:側重在模板渲染層面

type Template struct {
    Path string
}

func (T Template) Read(name string) ([]byte, error) {
    box := packr.New(name, T.Path)
    b, e := box.Find(name)
    if e != nil {
        log.Println("template read fail", e.Error())
        return nil, e
    }
    return b, nil
}

func (T Template) Execute(w http.ResponseWriter, r *http.Request, v interface{}) {
    t := template.New("")
    text, e := T.Read("plot.html")
    if e != nil {
        log.Println("template read fail")
        return
    }
    tt, e := t.Parse(string(text))
    if e != nil {
        log.Println("template parse fail")
        return
    }
    tt.Execute(w, v)
}
type Interface interface {
    Execute(w http.ResponseWriter, r *http.Request, v interface{})
    Read(name string) ([]byte, error)
}

type ChartsTransport struct {
    Template Interface
    Charts   *cartoon.Charts
}

func (C ChartsTransport) Execute(w http.ResponseWriter, r *http.Request, v interface{}) {
    C.Template.Execute(w, r, v)
}
func (C ChartsTransport) Read(name string) ([]byte, error) {
    return C.Template.Read(name)
}

func NewChartsTransport() *ChartsTransport {
    t := Template{Path: "./template"}
    return &ChartsTransport{
        Template: t,
        Charts:   cartoon.NewCharts(t),
    }
}

上游層:簡潔的對外暴露層

type CartoonCharts struct {
    *cartoontransport.ChartsTransport
}

func NewCartoonCharts() *CartoonCharts {
    return &CartoonCharts{cartoontransport.NewChartsTransport()}
}

示例:

package main

import (
    "github.com/wuxiaoxiaoshen/cartooncharts"
    "log"
    "net/http"
)

var charts *cartooncharts.CartoonCharts

func init() {
    charts = cartooncharts.NewCartoonCharts()
}

func ExampleBar() {
    bar := charts.Charts.Bar("github stars VS patron number",
        charts.Charts.Bar.WithDataLabels([]interface{}{"github stars", "patrons"}),
        charts.Charts.Bar.WithDataDataSets("", []interface{}{100, 2}),
        charts.Charts.Bar.WithOptions("yTickCount", 2),
    )
    http.HandleFunc("/bar", bar)
}
func ExampleXY() {
    type point struct {
        X interface{} `json:"x"`
        Y interface{} `json:"y"`
    }
    xy := charts.Charts.XY("Pokemon farms",
        charts.Charts.XY.WithXLabel("Coodinate"),
        charts.Charts.XY.WithYLabel("Count"),
        charts.Charts.XY.WithDataDataSets("Pikachu", []interface{}{point{3, 10}, point{4, 122}, point{10, 100}, point{1, 2}, point{2, 4}}),
        charts.Charts.XY.WithDataDataSets("Squirtle", []interface{}{point{3, 122}, point{4, 212}, point{-3, 100}, point{1, 1}, point{1.5, 12}}),
        charts.Charts.XY.WithOptions("xTickCount", 5),
        charts.Charts.XY.WithOptions("yTickCount", 5),
        charts.Charts.XY.WithOptions("legendPosition", "chartXkcd.config.positionType.upRight"),
        charts.Charts.XY.WithOptions("showLine", false),
        charts.Charts.XY.WithOptions("timeFormat", "undefined"),
        charts.Charts.XY.WithOptions("dotSize", 1),
    )
    http.HandleFunc("/xy", xy)
}
func ExampleStackedBar() {
    stackedBar := charts.Charts.StackedBar("Issues and PR Submissions",
        charts.Charts.StackedBar.WithXLabel("Month"),
        charts.Charts.StackedBar.WithYLabel("Count"),
        charts.Charts.StackedBar.WithDataLabels([]interface{}{"Jan", "Feb", "Mar", "April", "May"}),
        charts.Charts.StackedBar.WithDataDataSets("Issues", []interface{}{12, 19, 11, 29, 17}),
        charts.Charts.StackedBar.WithDataDataSets("PRs", []interface{}{3, 5, 2, 4, 1}),
        charts.Charts.StackedBar.WithDataDataSets("Merges", []interface{}{2, 3, 0, 1, 1}),
    )
    http.HandleFunc("/stackedBar", stackedBar)
}
func ExampleLine() {
    line := charts.Charts.Line("Monthly income of an indie developer",
        charts.Charts.Line.WithXLabel("Month"),
        charts.Charts.Line.WithYLabel("$ Dollars"),
        charts.Charts.Line.WithDataLabels([]interface{}{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}),
        charts.Charts.Line.WithDataDataSets("Plan", []interface{}{30, 70, 200, 300, 500, 800, 1500, 2900, 5000, 8000}),
        charts.Charts.Line.WithDataDataSets("Reality", []interface{}{0, 1, 30, 70, 80, 100, 50, 80, 40, 150}),
        charts.Charts.Line.WithOptions("yTickCount", 3),
        charts.Charts.Line.WithOptions("legendPosition", "chartXkcd.config.positionType.upLeft"),
    )
    http.HandleFunc("/line", line)

}
func ExamplePie() {
    pie := charts.Charts.Pie("What Tim made of",
        charts.Charts.Pie.WithDataLabels([]interface{}{"a", "b", "e", "f", "g"}),
        charts.Charts.Pie.WithDataDataSets("", []interface{}{500, 200, 80, 90, 100}),
        charts.Charts.Pie.WithOptions("innerRadius", 0.5),
        charts.Charts.Pie.WithOptions("legendPosition", "chartXkcd.config.positionType.upRight"),
    )
    http.HandleFunc("/pie", pie)
}
func ExampleRadar() {
    radar := charts.Charts.Radar("Letters in random words",
        charts.Charts.Radar.WithDataLabels([]interface{}{"c", "h", "a", "r", "t"}),
        charts.Charts.Radar.WithDataDataSets("ccharrrt", []interface{}{2, 1, 1, 3, 1}),
        charts.Charts.Radar.WithDataDataSets("chhaart", []interface{}{1, 2, 2, 1, 1}),
        charts.Charts.Radar.WithOptions("showLegend", true),
        charts.Charts.Radar.WithOptions("dotSize", 0.8),
        charts.Charts.Radar.WithOptions("showLabels", true),
        charts.Charts.Radar.WithOptions("legendPosition", "chartXkcd.config.positionType.upRight"),
    )
    http.HandleFunc("/radar", radar)
}

func main() {

    ExampleBar()
    ExampleXY()
    ExampleStackedBar()
    ExampleLine()
    ExamplePie()
    ExampleRadar()
    log.Fatal(http.ListenAndServe(":9090", nil))
}

維護了一致的風格。

結果:

參考:https://github.com/wuxiaoxiaoshen/cartooncharts

下課!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章