美文网首页
源码学啥子嘛?接口、组合

源码学啥子嘛?接口、组合

作者: 谢小路 | 来源:发表于2019-12-26 14:38 被阅读0次
    WX20191226-141700.png

    大家好,我叫谢伟,是一名程序员。

    今天的主题:面向接口、组合编程。

    作为程序员,都希望编写通用、可扩展的代码,通常这些知识靠的都是依靠设计模式进行指导开发。比如说面向对象的特性:封装、抽象、多态、继承。

    要编写更通用的代码,一方面需要靠足够时间砸出来,一方面也需要自己实践摸索。编写代码过程中要时刻在脑中形成清单:

    • 编写可读的代码
    • 编写符合设计模式的代码

    在 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...,具体的实现散布在各种库中。

    io.png

    这种做法有什么好处?分层(或者说是隔离)。

    • 上游层和下游层通过接口进行关联,但两层之间没有相互依赖
    • 上游层使用接口描述,稳定,不会轻易改动
    • 下游层侧重实现,需求变更,更改对应的实现即可

    这么说,有点抽象,找个具体的例子: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))
    }
    
    

    维护了一致的风格。

    结果:

    WX20191226-141700.png

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

    下课!

    相关文章

      网友评论

          本文标题:源码学啥子嘛?接口、组合

          本文链接:https://www.haomeiwen.com/subject/ynokoctx.html