面向接口编程的三个例子
背景
在用go操作mysql数据库时会经常见到类似下面的代码,空导入”go-sql-driver/mysql”
1 | import ( |
我之前对上面代码有点疑问:空导入有什么意义吗?
后面知道go在导入包时,会执行包中的init函数,所以上面的空导入会执行 github.com/go-sql-driver/mysql/driver.go 中的init函数来注册驱动
1 | func init() { |
再后来,见到好多次类似”注册模式”的写法,逐渐能从中体会到”面向接口编程”的思想。然后就多了一个好处:在做代码审计时,因为了解这种”业务套路”,所以更容易理解代码逻辑;
下面分享三个用到这种模式的例子,分别是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
4type Driver interface {
...
Open(name string) (Conn, error)
}database/sql/sql.go中提供”注册实例接口”,”驱动”可以调用Register函数注册。
1
2
3
4
5
6
7
8
9
10
11func 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
8func 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
12type 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
3func init() {
sql.Register("mysql", &MySQLDriver{})
}
swagger
是什么?
在审计 tidb-dashboard 项目时,关注到swagger。
tidb-dashboard用swagger来提供在线的api文档服务。
服务使用者
“服务使用者”,这里是tidb-dashboard
1
2
3func 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
6func 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
7type s struct{}
func (s *s) ReadDoc() string {
...
return tpl.String()
}注册接口
1
2
3func init() {
swag.Register(swag.Name, &s{})
}和第一个例子的区别
区别在于,这个例子中,”服务提供者”和”服务使用者”都是用户自己。
那么为什么不直接自己调自己,还经过”接口层”呢?
python flask扩展
是什么?
用户可以用flask框架的cors扩展来做跨域请求时的限制。
插件文档中的例子如下
1
2
3
4
5
6
7
8
9from 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相关的安全能力。
实现接口:cors_after_request函数是
AfterRequestCallable
类型具体的实现1
2
3
4
5
6
7
8
9
10def 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
11class 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库里自动注册服务,让库的使用者少写几行代码吗?
- 没有显式的”接口定义”,毕竟python没有接口关键字
总结
通过分析这三个”注册模式”的例子,我自己对”面向接口编程”有点感觉。后面感觉这种思想很基础、很常见、很实用,比如rpc、spring ioc容器、微服务的服务注册等都和这个”注册模式”很像。
如果你觉得疑惑,或者觉得我写得比较怪,推荐你找一个你熟悉的库自己分析一下。