帰ってきた「しっぽのさきっちょ」

インスタンスの生成と Functional Options パターン — プログラミング言語 Go

Go言語のFunctional Option Pattern - Qiita」を参考にして今回も自分用の覚え書きとして書いておく。

Go 言語には C++ や Java 等にある class 宣言がない。 つまりインスタンス(instance)生成時の構築子(constructor)もない。 ではどうやってインスタンスを生成するのか。

たとえば以下のような型を考える。

package ui

import (
	"io"
)

// UI is Command line user interface
type UI struct {
	reader      io.Reader
	writer      io.Writer
	errorWriter io.Writer
}

ui.UI 型のインスタンスを生成するにはいくつかの方法がある。

まずは new() 関数を使う方法。

u := new(ui.UI)

new() 関数でインスタンスを生成する場合は必ずゼロ値で初期化される。 ui.UI の場合は reader, writer, errorWriter の各フィールドには nil がセットされる。 しかし io.Reader および io.Writer は nil のまま使用すると panic になるため何らかの初期値を与える必要がある。

2番目は複合リテラル(composite literals)で記述する方法。

u := UI{reader: os.Stdin, writer: os.Stdout, errorWriter: os.Stderr}

この方法であれば各フィールドに初期値を与えることができる。 ただしフィールドがパッケージの外からは不可視の場合は(普通そうするよね)この手は使えない。

そこで,3番目の方法として構築子に相当する関数を考える。

package ui

import (
	"bytes"
	"io"
	"io/ioutil"
)

// UI is Command line user interface
type UI struct {
	reader      io.Reader
	writer      io.Writer
	errorWriter io.Writer
}

// New returns a new UI instance
func New(r io.Reader, w, e io.Writer) *UI {
	if r == nil {
		r = ioutil.NopCloser(bytes.NewReader(nil))
	}
	if w == nil {
		w = ioutil.Discard
	}
	if e == nil {
		e = ioutil.Discard
	}
	return &UI{reader: r, writer: w, errorWriter: e}
}

こうすれば

u := ui.New(os.Stdin, os.Stdout, os.Stderr)

と記述することでパッケージ外でも初期化済みのインスタンスを生成できる。 また

u := ui.New(nil, nil, nil)

と無効な値(nil)を引数に指定した場合でもフィールドには(nil ではなく)安全な値がセットされる。

この方法の問題点は引数に必ず何らかの値をセットしなければならないことだ(Go 言語にはデフォルト引数(default argument)のような仕組みはない)。 たとえば errorWriter は既定では使わないことが分かっていてもインスタンス生成時には

u := ui.New(os.Stdin, os.Stdout, nil)

などとしなければならない。 また

// NewWithoutErr returns a new UI instance
func NewWithoutErr(r io.Reader, w io.Writer) *UI {
	return New(r, w, nil)
}

などと構築子を別途増やす手もあるが,それでは有効なフィールドの組み合わせが増えると関数の管理が煩雑になってしまう。

そこで4番目の方法。 構築子の引数に初期値をセットするのではなく,初期化関数をセットするのである。 この初期化関数の型を

//Option is function value of functional options
type Option func(*UI)

と定義する1。 すると構築子は

//Option is function value of functional options
type Option func(*UI)

// New returns a new UI instance
func New(opts ...Option) *UI {
	u := &UI{reader: ioutil.NopCloser(bytes.NewReader(nil)), writer: ioutil.Discard, errorWriter: ioutil.Discard}
	for _, opt := range opts {
		opt(u)
	}
	return u
}

と記述することができる。

さらにフィールドごとに Option 関数を返す関数も定義する(これらの関数を用意することで ui パッケージを利用するユーザから関数閉包(closure)を隠蔽できる)。

//Reader returns closure as type Option
func Reader(r io.Reader) Option {
    return func(u *UI) {
        if r != nil {
            u.reader = r
        }

    }
}

//Writer returns closure as type Option
func Writer(w io.Writer) Option {
    return func(u *UI) {
        if w != nil {
            u.writer = w
        }
    }
}

//ErrorWriter returns closure as type Option
func ErrorWriter(e io.Writer) Option {
    return func(u *UI) {
        if e != nil {
            u.errorWriter = e
        }
    }
}

こうしておけばインスタンス生成時の記述は

u := ui.New(ui.Reader(os.Stdin), ui.Writer(os.Stdout))

などと初期化の必要なフィールドのみ引数で指定でき,かつコードの見た目も分かりやすくできる。 このようなプログラミング・パターンを “Functional Options” と呼ぶようである。

ブックマーク

参考図書

photo
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan Brian W. Kernighan 柴田 芳樹
丸善出版 2016-06-20
評価

スターティングGo言語 (CodeZine BOOKS) Go言語によるWebアプリケーション開発 Kotlinスタートブック -新しいAndroidプログラミング Docker実戦活用ガイド グッド・マス ギークのための数・論理・計算機科学

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。

reviewed by Spiegel on 2016-07-13 (powered by G-Tools)


  1. これを自己参照関数(self-referential function)と呼ぶそうだ。 “Self-referential functions and the design of options” には自己参照関数の様々なバリエーションが紹介されている。この記事ではもっとも簡単な構造のみ紹介している。 [return]