这篇文章,是受 Go module
启发,其实,不管有没有出现 go module
,这篇文章的思想仍然值得推荐
「 模块化编程 」 或 「 插件化 」是当下最流行的编程思维,这两种思维的高级表现就是当下最炙手可热的 「 中间件 」 编程
本章节,我们就来讲讲 Go
语言中模块化编程的一个新思维 - 「 驱动模式 」( driver pattern
)
## 编写模块化的程序
模块化编程意味着将抽象与实现分离。通常,我们的程序是在特定技术的基础上构建的,并且我们意识到在保持所有功能可用并且正常工作的同时,它可以很容易地被其它东西所代替
理想很美好,现实很残酷,比如我们使用
ThinkPHP
开发的应用能无缝的切换到CI
上吗? 不能
此时你可能正在对自己说:「 我仅仅只需要一种模块化方式来选择任何这些实现,同时应用程序的其余部分则可以编写通用代码 」
从某些方面说,你正在寻找的是 「 驱动模式 」
「 驱动模式 」 是一种可拔插和可替换的模块
古老的好用的驱动模式
「 插件化 」 应该是最古老又好用的模块化编程模式了,「 插件化 」 的本质是利用 「 机制 」( mechanism ) 来扩展程序的功能集
而最新的 「 驱动模式 」则专注于通过 「 合约 」( contract ) 提供一个严格的环境来绑定到其它代码
对于 「 驱动模式 」,合约是唯一能够于驱动程序交互的媒介,而具体的实现特性,则由驱动程序自己决定
大家可能对 「 机制 」 和 「 合约 」 有点不太理解吧
「 机制 」 就是宿主程序本身提供了一种插件化约定,当需要某个功能时,通过调用插件来完成。某些方面说宿主程序会判断插件是否提供了某个功能,如果提供了,则调用,没提供则不调用,可以有选择的遗地。
「 合约 」 则是宿主程序只负责提供约定和调用函数,而具体的实现,则由驱动程序自己决定,这时候,宿主程序调用的仍然是自己的接口,但自己的接口调用的,却是驱动程序提供的实现。某些方面说,驱动程序要完备的实现宿主程序提供的约定,没有选择的遗地。
仍然不太理解,没关系,看完这篇文章你就懂了
Go 语言中的驱动模式
Go 语言能够使用驱动模式,得益于它特殊接口机制,这个机制就是
「 只要一个 struct
或 struct
的匿名成员定义了接口的所有方法,那么这个 struct
就是实现了该接口,而不用显示声明」
文件和目录结构
- 首先,我们需要在
driver
包内有一个驱动注册器 - 然后,我们需要创建一个
drivers
包,包含了分组的各个驱动程序,每个分组都有一个register
子包,用于简化应用程序其余部分的导入过程并设置构建约束
大概的目录结构如下
. ├── driver │ └── registry.go └── drivers └── group ├── group.go ├── driver1 │ └── driver1.go ├── driver2 │ └── driver2.go └── register
一切都是 「 约定 」
不要感到太意外,强制执行 「 合约 」 的方式就是使用接口 interface
我们假设我们想要编写一个可以利用多个打印后端的示例应用程序,这些打印程序都有一个通用的接口
type Printer interface { Open(dest string) error Print([]byte) (n int, err error) Close() error }
驱动程序注册
某些时候,基本上,我们需要从名称 ( 即字符串 ) 中检索驱动程序实现,这意味着我们的驱动程序需要使用别名向注册器来声明自己的存在
注意:下面的代码已经被简化了,并且,加入你错用了类型,反射包可能会引发一个异常 ( panic )
driver/registry.go
package driver import ( "reflect" "sync" ) var registry struct { contracts sync.Map drivers sync.Map } // 在一个注册器中将一个 「 合约 」绑定到一个分组名称上, // 如果 「 合约 」 不是一个接口,将会引发异常 func Declare(group string, contract interface{}) { if reflect.TypeOf(contract).Elem().Kind() != reflect.Interface { panic("Contract is not an Interface for driver group " + group) } registry.contracts.Store(group, contract) } // 加载并获取一个驱动程序,如果不能获取驱动程序,将引发一个异常 func Load(group, name string) interface{} { fqn := fullQualifiedName(group, name) driver, ok := registry.drivers.Load(fqn) if !ok { panic("Unknown driver " + fqn) } return driver } // 使用给定的名称,将一个驱动程序注册到一个已经注册了的分组上 // 当分组不存在或该驱动程序没有实现分组合约时,将会引发一个异常 func Register(group, name string, driver interface{}) { fqn := fullQualifiedName(group, name) contract, ok := registry.contracts.Load(group) if !ok { panic("Unknown driver group " + group) } if !reflect.TypeOf(driver).Implements(reflect.TypeOf(contract.Elem()) { panic("Unsatisfied contract for driver " + fqn) } registry.drivers.Store(fqn, driver) } func fullQualifiedName(group, name string) string { return group + ":" + name }
声明一个驱动程序分组
接下来,我们声明下我们的驱动程序分组实现了 Printer
接口
package printer import "repo/user/project/driver" func init() { driver.Declare("printer", (*Printer)(nil)) } type Printer interface { Open(dest string) error Print([]byte) (n int, err error) Close() error }
编写驱动程序
我们首先实现一个写入控制台的驱动程序
package console import ( "fmt" "repo/user/project/driver" ) func init() { driver.Register("printer", "console", &Console{}) } type Console struct{} func (c *Console) Open(string) error { return nil } func (c *Console) Print(buf []byte) (int, error) { return fmt.Print(buf) } func (c *Console) Close() error { return nil }
然后我们实现一个写入文件的驱动程序
package file import ( "os" "repo/user/project/driver" ) func init() { driver.Register("printer", "file", &File{}) } type File struct { dest *os.File } func (f *File) Open(dest string) (err error) { f.dest, err = os.Create(dest) return } func (f *File) Print(buf []byte) (int, error) { return f.dest.Write(buf) } func (f *File) Close() error { return f.dest.Close() }
编译时引用驱动程序
我们希望避免构建那些与目标操作系统的不相关驱动程序,为生产环境排除不稳定的驱动程序或构建最小的二进制文件
幸运的是,Go 语言已经提供了 构建限制,提供了所有必要的资料来精心构建细粒度的跨平台的应用程序
例如,为我们的驱动程序分组下的 register
添加以下代码
// +build !exclude_driver_console package register import _ "repo/user/project/drivers/printer/console"
然后,我们就可以在构建时传递 -tags = exclude_driver_console
传递给构建链,则不会构建此驱动程序
使用驱动程序
使用驱动程序很简单,只需按正确的顺序导入 register
包和驱动程序分组
package main import ( "flag" "repo/user/project/driver" "repo/user/project/drivers/printer" _ "repo/user/project/drivers/printer/register" ) func main() { driverName := flag.String("driver", "console", "Printing driver") flag.Parse() printer := driver.Load("printer", *driverName).(printer.Printer) printer.Open("out") printer.Print([]byte("Hello world!")) printer.Close() }
运行结果如下
$ go run main.go --driver=console Hello world! $ go run main.go --driver=file $ cat out Hello world!