面向接口编程的三个例子

背景

在用go操作mysql数据库时会经常见到类似下面的代码,空导入”go-sql-driver/mysql”

1
2
3
4
5
6
7
8
9
10
import (
"database/sql"
"time"

_ "github.com/go-sql-driver/mysql" // 空导入
)

// ...

db, err := sql.Open("mysql", "user:password@/dbname")

我之前对上面代码有点疑问:空导入有什么意义吗?

后面知道go在导入包时,会执行包中的init函数,所以上面的空导入会执行 github.com/go-sql-driver/mysql/driver.go 中的init函数来注册驱动

1
2
3
func init() {
sql.Register("mysql", &MySQLDriver{})
}

再后来,见到好多次类似”注册模式”的写法,逐渐能从中体会到”面向接口编程”的思想。然后就多了一个好处:在做代码审计时,因为了解这种”业务套路”,所以更容易理解代码逻辑;

下面分享三个用到这种模式的例子,分别是go sql库、go swagger库、python flask库

分析

  • 这种”注册模式”是什么?

    包含有三个角色:

    • 接口层:定义接口、提供”注册实例接口”、提供”获取服务”或者”功能接口”
    • 服务提供者:实现接口、注册”接口实现”
    • 服务使用者:调用”获取服务接口”

    这个结论是我根据三个例子总结出来的,下面来具体看看三个角色的功能

go sql

  • 是什么?

    https://github.com/go-sql-driver/mysql 文档中的例子,用户可以使用驱动名获取到”包含驱动实例的对象”

    1
    db, err := sql.Open("mysql", "user:password@/dbname") // db中包含驱动实现

    这里”用户”就是”服务使用者”。

    “接口层”是”database/sql”库,它在 database/sql/driver/driver.go 文件中定义了接口,”驱动”需要实现下面的Open方法

    1
    2
    3
    4
    type Driver interface {
    ...
    Open(name string) (Conn, error)
    }

    database/sql/sql.go中提供”注册实例接口”,”驱动”可以调用Register函数注册。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func Register(name string, driver driver.Driver) {
    driversMu.Lock()
    defer driversMu.Unlock()
    if driver == nil {
    panic("sql: Register driver is nil")
    }
    if _, dup := drivers[name]; dup {
    panic("sql: Register called twice for driver " + name)
    }
    drivers[name] = driver
    }

    database/sql/sql.go中提供”获取服务接口”,”用户”可以调用Open函数获取DB实例

    1
    2
    3
    4
    5
    6
    7
    8
    func Open(driverName, dataSourceName string) (*DB, error) {
    driversMu.RLock()
    driveri, ok := drivers[driverName]
    driversMu.RUnlock()
    ...

    return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
    }

    “服务提供者”就是”驱动”,这里就是 github.com/go-sql-driver/mysql

    它实现了Driver接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type MySQLDriver struct{}
    ...
    func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
    cfg, err := ParseDSN(dsn)
    if err != nil {
    return nil, err
    }
    c := &connector{
    cfg: cfg,
    }
    return c.Connect(context.Background())
    }

    并且调用Register方法注册”接口实现”,代码见github.com/go-sql-driver/mysql/driver.go

    1
    2
    3
    func init() {
    sql.Register("mysql", &MySQLDriver{})
    }

swagger

  • 是什么?

    在审计 tidb-dashboard 项目时,关注到swagger

    tidb-dashboard用swagger来提供在线的api文档服务。

  • 服务使用者

    “服务使用者”,这里是tidb-dashboard

    tidb-dashboard

    1
    2
    3
    func Handler() http.Handler {
    return httpSwagger.Handler()
    }

    如果跟进方法,就会看到调用swag.ReadDoc()方法,这个方法就是”接口层”提供的”获取服务接口”。

  • 接口层

    “接口层”就是swagger库,在 swagger.go 文件中

    定义接口

    1
    2
    3
    4
    // Swagger is an interface to read swagger document.
    type Swagger interface {
    ReadDoc() string
    }

    提供”注册实例接口”

    1
    2
    3
    4
    5
    // Register registers swagger for given name.
    func Register(name string, swagger Swagger) {
    ...
    swags[name] = swagger
    }

    提供”获取服务接口”

    1
    2
    3
    4
    5
    6
    func ReadDoc(optionalName ...string) (string, error) {
    ...
    swag, ok := swags[name]
    ...
    return swag.ReadDoc(), nil
    }
  • 服务提供者

    “服务提供者”这里是用户自己。这里用法有点特殊,tidb-dashboard仓库中没有”服务实现”相关代码,在编译tidb-dashboard项目时会生成代码。

    生成后的代码我放在了gist

    可以看到它实现了Swagger接口

    1
    2
    3
    4
    5
    6
    7
    type s struct{}

    func (s *s) ReadDoc() string {
    ...

    return tpl.String()
    }

    注册接口

    1
    2
    3
    func init() {
    swag.Register(swag.Name, &s{})
    }
  • 和第一个例子的区别

    区别在于,这个例子中,”服务提供者”和”服务使用者”都是用户自己。

    那么为什么不直接自己调自己,还经过”接口层”呢?

python flask扩展

  • 是什么?

    用户可以用flask框架的cors扩展来做跨域请求时的限制。

    插件文档中的例子如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from flask import Flask
    from flask_cors import CORS

    app = Flask(__name__)
    CORS(app)

    @app.route("/")
    def helloWorld():
    return "Hello, cross-origin-world!"

    这里”服务使用者”不需要找”接口层”要”服务实例”

  • 接口层

    flask框架中定义了一个函数AfterRequestCallable类型

    提供”注册实例接口”,如下,只是把函数放进了列表中

    1
    2
    3
    4
    5
    @setupmethod
    def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
    ..
    self.after_request_funcs.setdefault(None, []).append(f)
    return f
  • 服务提供者

    cors插件在flask框架基础上,提供了cors相关的安全能力。

    flask_cors/extension.py

    实现接口:cors_after_request函数是AfterRequestCallable类型具体的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def make_after_request_function(resources):
    def cors_after_request(resp):
    ...
    normalized_path = unquote_plus(request.path)
    for res_regex, res_options in resources:
    if try_match(normalized_path, res_regex):
    ...
    ...
    return resp
    return cors_after_request

    注册接口:在CORS实例化时,会注册实例提供安全能力

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class CORS(object):
    ...

    def __init__(self, app=None, **kwargs):
    ...
    self.init_app(app, **kwargs)

    def init_app(self, app, **kwargs):
    ...
    cors_after_request = make_after_request_function(resources)
    app.after_request(cors_after_request) # 有点注册一个中间件的感觉
  • 和前面两个例子的区别

    细品的话,可以看到这个例子和前两个例子又有很多不同:

    • 没有显式的”接口定义”,毕竟python没有接口关键字
      • 注册的是”函数”,而不是”对象”
    • “接口层”不提供”获取服务”,”服务使用者”也不需要”获取服务”
    • 由”服务使用者”注册”接口实例”,而不是”服务提供者”注册接口实例

    可以想一想为什么会有这些区别,能把这些区别”修改”回去吗?比如如果我是cors扩展库作者,我就不能在cors库里自动注册服务,让库的使用者少写几行代码吗?

总结

通过分析这三个”注册模式”的例子,我自己对”面向接口编程”有点感觉。后面感觉这种思想很基础、很常见、很实用,比如rpc、spring ioc容器、微服务的服务注册等都和这个”注册模式”很像。

如果你觉得疑惑,或者觉得我写得比较怪,推荐你找一个你熟悉的库自己分析一下。