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

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


## go-extpointsをインストール
普通に `go install` するだけ。

```shell-session
$ go install github.com/progrium/go-extpoints
```


## 試してみる
使ったソースは [こちら](https://github.com/goldeneggg/geggg-go-extpoint-example)
ざっくり、こういう流れで実装していくことになる。
※（以降の記述では、公式で"extension point"とか"component"と言及されている物については分かりやすいように"プラグイン"という言葉を使う）

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

### `extpoints`サブパッケージ下に、プラグインのinterfaceを作る
* `extpoints`( __この名前でなければならない__ )サブパッケージを作成し、そこに提供する機能のinterfaceを定義する。例として事前処理・主処理・事後処理、という形の処理を行うinterfaceを定義してみる。

```go: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のようなクライアント・ソースは存在しないよね。
    * なので、今回のケースでは冗長とは思いつつ個人的に「今後のベターなお作法」となる気がしているこの形にしておいた。

```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-extpoints/template.go](https://github.com/progrium/go-extpoints/blob/master/template.go)  

```bash
$ go generate
extpoints: Processing file extpoints/interfaces.go
extpoints: Found interfaces: []string{"Hoge"}
extpoints: Writing file extpoints/extpoints.go
```

* 生成された`extpoints/extpoints.go`のpublicな関数・メソッドを中心に主要機能を抜粋して内容を見てみると、プラグインの参照・追加・削除を行う処理が定義されている。

```go: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` を順に呼び出すだけのクライアントを実装してみる。

```go: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
  }
}
```

* この段階で実行しても、まだプラグインを登録していないので何も起こらない。

```bash
$ go run *.go
Required name argument

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

### プラグインのinterfaceの実装を書く
* ポイントは、定義したinterfaceを実装する事 __と、`init` 関数内でメタAPIの`Register`メソッドを呼んでその処理の登録を行う__ 事。
    * （`Register` __関数__ の方（`func Register`）は、後の例で使う場面が出てくるがココではスルー）

```go: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 "劇的！ビフォーアフター"
}
```

* 実装した型名を引数で指定して実行すると、登録したプラグインの処理が呼び出される。

```bash
% go run *.go gekiteki
Registered object:  劇的！ビフォーアフター
ビフォー
匠の劇的な技！
アフター
```

* 別のプラグイン "xyz" を追加してみて（ソースは省略）実行するとこうなる

```bash
% 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`関数__ が使える。この関数はこのような "外部プロジェクトで定義したプラグインを登録する" 為に用意されていた関数だった、というわけ。

```go: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`する。

```bash
$ go install github.com/goldeneggg/additionalextpoint
```

* 追加したプラグインを使いたいクライアントプログラム側では __`import _ PACKAGE`するだけで良い。__

```go:main.go
package main

import (
  "fmt"
  "os"

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

  // 追加したいプラグインのパッケージをブランク識別子付きでimport
  _ "github.com/goldeneggg/additionalextpoint"
)
:
:
```

* 外部プラグインの名前 "additional" を指定して実行 → OK。

```bash
% go run *.go additional
Registered object:  I am additional
Registered object:  劇的！ビフォーアフター
Registered object:  I am xyz
Additional Before
Additional Run
Additional After
```