Go 语言模块化编程 - 写在 go module 出现之后

yufei       6 年, 3 月 前       1497

这篇文章,是受 Go module 启发,其实,不管有没有出现 go module ,这篇文章的思想仍然值得推荐

「 模块化编程 」 或 「 插件化 」是当下最流行的编程思维,这两种思维的高级表现就是当下最炙手可热的 「 中间件 」 编程

本章节,我们就来讲讲 Go 语言中模块化编程的一个新思维 - 「 驱动模式 」( driver pattern )

## 编写模块化的程序

模块化编程意味着将抽象与实现分离。通常,我们的程序是在特定技术的基础上构建的,并且我们意识到在保持所有功能可用并且正常工作的同时,它可以很容易地被其它东西所代替

理想很美好,现实很残酷,比如我们使用 ThinkPHP 开发的应用能无缝的切换到 CI 上吗? 不能

此时你可能正在对自己说:「 我仅仅只需要一种模块化方式来选择任何这些实现,同时应用程序的其余部分则可以编写通用代码 」

从某些方面说,你正在寻找的是 「 驱动模式 」

「 驱动模式 」 是一种可拔插和可替换的模块

古老的好用的驱动模式

「 插件化 」 应该是最古老又好用的模块化编程模式了,「 插件化 」 的本质是利用 「 机制 」( mechanism ) 来扩展程序的功能集

而最新的 「 驱动模式 」则专注于通过 「 合约 」( contract ) 提供一个严格的环境来绑定到其它代码

对于 「 驱动模式 」,合约是唯一能够于驱动程序交互的媒介,而具体的实现特性,则由驱动程序自己决定

大家可能对 「 机制 」 和 「 合约 」 有点不太理解吧

「 机制 」 就是宿主程序本身提供了一种插件化约定,当需要某个功能时,通过调用插件来完成。某些方面说宿主程序会判断插件是否提供了某个功能,如果提供了,则调用,没提供则不调用,可以有选择的遗地。

「 合约 」 则是宿主程序只负责提供约定和调用函数,而具体的实现,则由驱动程序自己决定,这时候,宿主程序调用的仍然是自己的接口,但自己的接口调用的,却是驱动程序提供的实现。某些方面说,驱动程序要完备的实现宿主程序提供的约定,没有选择的遗地。

仍然不太理解,没关系,看完这篇文章你就懂了

Go 语言中的驱动模式

Go 语言能够使用驱动模式,得益于它特殊接口机制,这个机制就是

「 只要一个 structstruct 的匿名成员定义了接口的所有方法,那么这个 struct 就是实现了该接口,而不用显示声明」

文件和目录结构

  1. 首先,我们需要在 driver 包内有一个驱动注册器
  2. 然后,我们需要创建一个 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!
目前尚无回复
简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.