LoginSignup
13
13

More than 5 years have passed since last update.

pluggableな機能追加を実現してくれる go-extpoints を使ってみた

Posted at
  • go generateを調べたり試したりしてみて、「これを使って柔軟且つpluggableに機能拡張/追加出来る仕組み作れないかな?」と考えていたところ、既にそれっぽい事を実現してくれているパッケージがあった。
  • こちらの progrium/go-extpoints
  • 使ってみたら、個人的にドストライクなツボを抑えてくれていたので、お試しメモとして残しておく。
  • なお、go generateを使用するので、goのバージョンは1.4(以上)が前提。

go-extpointsをインストール

普通に go install するだけ。

$ go install github.com/progrium/go-extpoints

試してみる

使ったソースは こちら
ざっくり、こういう流れで実装していくことになる。
※(以降の記述では、公式で"extension point"とか"component"と言及されている物については分かりやすいように"プラグイン"という言葉を使う)

  1. extpointsサブパッケージ下に、プラグインのinterfaceを作る
  2. //go:generate だけ行うgoファイルを作る(cliアプリなら mainパッケージ下 とかに)
  3. go generate する
  4. プラグインを使用するクライアント処理を書く(cliアプリなら mainパッケージ下 とかに)
  5. プラグインのinterfaceの実装を書く(cliアプリなら mainパッケージ下 とかに、作りたい数だけ作る)

extpointsサブパッケージ下に、プラグインのinterfaceを作る

  • extpoints( この名前でなければならない )サブパッケージを作成し、そこに提供する機能のinterfaceを定義する。例として事前処理・主処理・事後処理、という形の処理を行うinterfaceを定義してみる。
extpoints/interfaces.go
package extpoints

// 提供する機能のinterface群を定義する(1つ or 複数)

type Hoge interface {
  Before(args []string) error // 事前処理
  Run(args []string) error    // 主処理
  After(args []string) error  // 事後処理
  String() string
}

//go:generateだけ行うgoファイルを作る

  • go generateお決まりのコメントフォーマット//go:generate <実行したいコマンド>を記述。コマンドにはgo-extpointsコマンドを指定する。
  • 今回のような単純なcliアプリなら main.go の中に//go:generateも書いちゃえばいいよね?と一瞬思った 。
    • が、プラグインのinterfaceとその具象実装 "だけ" を公開するようなプロジェクトを作りたいケースとかもありそう。
    • そういうケースだとmain.goのようなクライアント・ソースは存在しないよね。
    • なので、今回のケースでは冗長とは思いつつ個人的に「今後のベターなお作法」となる気がしているこの形にしておいた。
extensions.go
//go:generate go-extpoints
package main

go generateする

  • go generateすると上記extensions.goが処理される
  • extpoints/interfaces.goで定義したinterfaceをパースし、結果がextpointsサブパッケージ下にextpoints/extpoints.goというgoファイルで生成される
$ go generate
extpoints: Processing file extpoints/interfaces.go
extpoints: Found interfaces: []string{"Hoge"}
extpoints: Writing file extpoints/extpoints.go
  • 生成されたextpoints/extpoints.goのpublicな関数・メソッドを中心に主要機能を抜粋して内容を見てみると、プラグインの参照・追加・削除を行う処理が定義されている。
generateで生成されたextpoints/extpoints.go
// generated by go-extpoints -- DO NOT EDIT
package extpoints

:

// プラグインを登録する関数
func Register(component interface{}, name string) []string {
  :
}

// プラグインを削除する関数
func Unregister(name string) []string {
  :
}


// Hoge

// 定義したinterface(の複数形) の変数をexportしている
// 実体は "該当interfaceをextensionPoint型でラップしたオブジェクト" になっている。
// この変数がgo-exportsのキモで、プラグインを利用するクライアント側は、この変数経由でメタAPIを呼び出して追加・削除・実行等を行う
var Hoges = &hogeExt{
  newExtensionPoint(new(Hoge)),  // newExtensinonPointの処理内容は本家のソースを参照
}
type hogeExt struct {
  *extensionPoint
}

// プラグインを登録するメソッド
func (ep *hogeExt) Register(component Hoge, name string) bool {
  :
}

// プラグインを削除するメソッド
func (ep *hogeExt) Unregister(name string) bool {
  :
}

// 登録時のnameをキーに、プラグインのオブジェクトを取得するメソッド
func (ep *hogeExt) Lookup(name string) (Hoge, bool) {
  :
}

// 登録済のプラグインオブジェクト全てを、nameをキーにしたmap型で取得するメソッド
func (ep *hogeExt) All() map[string]Hoge {
  :
}

プラグインを使用するクライアント処理を書く

  • Before, Run, After を順に呼び出すだけのクライアントを実装してみる。
main.go
package main

import (
  "fmt"
  "os"

  "github.com/goldeneggg/geggg-go-extpoint-example/extpoints"
)

var (
  hoges = extpoints.Hoges  // extpoints.Hogesは、generateで生成したメタAPIアクセス用の変数
)

func main() {
  var s int
  defer os.Exit(s)

  // 登録済のプラグインを All メソッドで確認
  // プラグイン未実装の初期状態では何も出力されない
  for _, registered := range hoges.All() {
    fmt.Println("Registered object: ", registered)
  }

  args := os.Args[1:]
  if len(args) < 1 {
    fmt.Fprintln(os.Stderr, "Required name argument")
    s = 1
    return
  }

  // 登録済のHoge型のプラグインを Lookup メソッドで取得
  // nameには引数で指定した内容を使う
  // プラグイン未実装の初期状態では、okがfalseとなり終了
  // (するはずだが、本家の処理でnilチェックが漏れてる箇所があって期待通りには動かない。pull-reqするか。。。)
  name := args[0]
  hoge, ok := hoges.Lookup(name)
  if !ok {
    fmt.Fprintf(os.Stderr, "Name %s is not registered\n", name)
    s = 1
    return
  }

  // Before, Run, Afterを順次実行
  if err := hoge.Before(args); err != nil {
    fmt.Fprintln(os.Stderr, "Before error", err)
    s = 1
    return
  }

  if err := hoge.Run(args); err != nil {
    fmt.Fprintln(os.Stderr, "Run error", err)
    s = 1
    return
  }

  if err := hoge.After(args); err != nil {
    fmt.Fprintln(os.Stderr, "After error", err)
    s = 1
    return
  }
}
  • この段階で実行しても、まだプラグインを登録していないので何も起こらない。
$ go run *.go
Required name argument

$ go run *.go nothing
 # (本来ならココで "Name nothing is not registered" が出力されて欲しいが、、、)

プラグインのinterfaceの実装を書く

  • ポイントは、定義したinterfaceを実装する事 と、init 関数内でメタAPIのRegisterメソッドを呼んでその処理の登録を行う 事。
    • Register 関数 の方(func Register)は、後の例で使う場面が出てくるがココではスルー)
gekiteki.go
package main

import "fmt"

func init() {
  // main.goで定義した hoges 変数経由でメタAPIの Register メソッドを実行してプラグインを登録する
  // 第1引数に具象型(の参照)を、第2引数で名前を指定する(省略時は型と同じ名前で登録される)
  hoges.Register(new(gekiteki), "")
}

type gekiteki struct{}

var _ extpoints.Hoge = new(gekiteki)

func (g *gekiteki) Before(args []string) error {
  fmt.Println("ビフォー")
}

func (g *gekiteki) Run(args []string) error {
  fmt.Println("匠の劇的な技!")
}

func (g *gekiteki) After(args []string) error {
  fmt.Println("アフター")
}

func (g *gekiteki) String() string {
  return "劇的!ビフォーアフター"
}
  • 実装した型名を引数で指定して実行すると、登録したプラグインの処理が呼び出される。
% go run *.go gekiteki
Registered object:  劇的!ビフォーアフター
ビフォー
匠の劇的な技!
アフター
  • 別のプラグイン "xyz" を追加してみて(ソースは省略)実行するとこうなる
% go run *.go xyz
Registered object:  劇的!ビフォーアフター
Registered object:  I am xyz
Exec Before
Exec Run
Exec After

こんな具合に、一度クライアント側を実装してしまえば、以降はプラグインのinterfaceを実装した型を追加するだけで機能追加が容易に行える( という開発の流れを促進してくれる )のが go-extpoints。
上記は「引数で処理を切り替える」というよくあるCMD SUBCMD形式の実装例だが、こうしたアプリの実装にgo-extpointsは向いていると思う。

外部のプロジェクトで定義したプラグインを使いたい

例えば additional という 別プロジェクト(パッケージ)で実装された同じinterfaceのプラグインを、既存クライアントから使いたい というケースが出てきそう。
先ほど作ったHogeインタフェースを使って、こういうケースでの対応を試してみる。

  • 新たに作成するプラグイン追加用プロジェクト(パッケージ)側で、既存interface Hoge を実装した型を作る。
  • 既存と同様に、そのソースのinit関数内でRegisterを実行してプラグインを登録する。したい。。
    • しかし、 既存プロジェクトで使っていたメタAPIアクセス用変数(hoges)はこのパッケージからは見えなくてRegisterを呼び出せない。
  • ここで、 既存プロジェクトのextpointsパッケージ直下で、インスタンスを経由せず直接実行する為に定義されたRegister関数 が使える。この関数はこのような "外部プロジェクトで定義したプラグインを登録する" 為に用意されていた関数だった、というわけ。
additionalextpoint/additional.go
package additionalextpoint

import (
  "fmt"

  "github.com/goldeneggg/geggg-go-extpoint-example/extpoints"
)

func init() {
  // Register "関数" を使ってプラグインを登録
  extpoints.Register(new(additional), "")
}

type additional struct{}

var _ extpoints.Hoge = new(additional)

func (a *additional) Before(args []string) error {
  fmt.Println("Additional Before")
  return nil
}

func (a *additional) Run(args []string) error {
  fmt.Println("Additional Run")
  return nil
}

func (a *additional) After(args []string) error {
  fmt.Println("Additional After")
  return nil
}

func (a *additional) String() string {
  return "I am additional"
}
  • リモートに存在するプロジェクトであれば、まずgo install PACKAGEする。
$ go install github.com/goldeneggg/additionalextpoint
  • 追加したプラグインを使いたいクライアントプログラム側では import _ PACKAGEするだけで良い。
main.go
package main

import (
  "fmt"
  "os"

  "github.com/goldeneggg/geggg-go-extpoint-example/extpoints"

  // 追加したいプラグインのパッケージをブランク識別子付きでimport
  _ "github.com/goldeneggg/additionalextpoint"
)
:
:
  • 外部プラグインの名前 "additional" を指定して実行 → OK。
% go run *.go additional
Registered object:  I am additional
Registered object:  劇的!ビフォーアフター
Registered object:  I am xyz
Additional Before
Additional Run
Additional After
13
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13