[Go] Uber製DIフレームワークfxの利用法(基礎)
この記事について
この記事ではGoのDIフレームワークであるfxパッケージの基本的な使用法について記載します。
このパッケージはUber製で、Uberのサービスの様々な所で使われているようです。リフレクションを使うタイプでDIを実行するようです。Googleにもwireというものがあり、これはスター数が大きかったのですが、リリース自体はこちらの方が活発に見受けられます。また、fxのもっとコアな中身としてはdigというものがあるようです。
私がこのライブラリを触ったのは全くの気分で(強いて言えば私が一応最も知見のある…であろう.NETのフレームワーク群はDIを全面に押し出したような環境が多い印象があった為)、特に技術選定などに係るような深い理由は何もありません。
この記事自体が自分が学んでいる中での覚書みたいな内容ですが、大まかな基本的な使い方を書いているので、公式ドキュメントと合わせてご参考になれば幸いです。
ネットで探してみると海外リソース含めてコード例がWebを想定したパターンのものばかりだったのですが、私はそこまで詳しくないため、一般的なプログラミング(クラス表現など)のような形で示しています。
DIとは
「アプリケーションをコンテナ化し、オブジェクトが必要とする依存オブジェクトを外部から提供される仕組み」というのが私の今の理解です。
C#(.NET)のDependency Injectionパッケージで言うと、コンテナに色々なクラスの型を登録しておいて、アプリのクラスコンストラクタ引数には必要なクラスを書いておくと、それを設定するロジックを記述することなくアプリ実行時によしなに注入してくれるようなデザインパターン、と解釈しています。
このメリットとしては状況に応じた実装(オブジェクト)の差し替えやコード自体の結合度の削減などが挙げられるのではないかと考えられます。
(とはいえ、これはこれで割りかし設計プラクティスも必要なのかな、と(言語化できないのですが)ちょっと感じていたりしますが...このあたりの知見は私には少ないです。)
基本的な使い方
基本的な使い方は次の通りです。
-
fx.New()
のコンストラクタ引数にfx.Option
を指定する。指定するために対応したものにはfx.Provide()
,fx.Invoke()
,fx.Module()
などがある -
fx.Provide()
には提供するオブジェクトのファクトリ関数を指定する。生成のために必要な要素は全てコンテナに提供されなければならない -
fx.Invoke()
はアプリケーション実行前にやってほしいことを行う。呼び出しは順次的に実行される- これが1つもないと
Run
しても特に何も起こらない。その為、例えばエントリポイント的なオブジェクトを引数に取る空の関数を呼び出してあげる、みたいなことが必要
- これが1つもないと
-
fx.Lifecycle
を引数で呼び出すことで、fx.Hook
を用いたフックを追加できる。アプリケーションをRun
させた、または終了した時に行ってほしいフックを追加する -
fx.Module
でfx.Option
をひとまとめにできる
大まかな利用するための骨組みとしては以下の通りです。
func main() {
app := fx.New(
// must
fx.Provide(// something),
fx.Decorate(),
// must
fx.Invoke(// something),
)
app.Run()
}
以下はfxの基本的な機能を大体示すHello Worldの例です。
このコードでは、Person
構造体のファクトリを登録し、実行前にJhon
と名前が設定されるようにし、アプリケーションが実行されたらGreet()
関数を呼び出す例を示します。
記法の例
type Person struct {
Name string
}
func NewPerson(lc fx.Lifecycle) *Person {
person := &Person{Name: ""}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
person.Greet()
// ここでエラーを返すとアプリケーションが停止する
return nil
},
})
return person
}
func (p *Person) Greet() {
fmt.Printf("Hello, %s!\n", p.Name)
}
func main() {
app := fx.New(
fx.Provide(
NewPerson,
),
fx.Invoke(func(person *Person) {
person.Name = "Jhon"
}),
)
app.Run()
}
インターフェースを用いた注入の差し替え
次の例では関数の引数にインターフェースを指定することにより、注入されるものを設定によって変化させられることを示します。
このコードでは次のことを実行します。
-
IdGenerator
を実装する2つの構造体RandomIdGenerator
とUUIDGenerator
があります -
IdGenerator
をファクトリの引数に取る必要があるIdCard
があります -
fx.Annotate
とfx.As
を使い、注入されるオブジェクトがIdGenerator
であることをfx
に伝えます -
fx.Invoke
が走った際、注入した構造体の方のインスタンスが呼ばれます
これによってRandomIdGenerator
をUUIDGenerator
に差し替えたいってなったら、
コンテナの注入設定を変更すれば良い訳ですね。これがDIの魅力なのではと感じています。
インターフェースを利用する例
// IDを生成するインターフェース
type IdGenerator interface {
Generate() string
}
// IdGeneratorを実装する構造体1
type RandomIdGenerator struct{}
func NewRandomIdGenerator() *RandomIdGenerator {
return &RandomIdGenerator{}
}
func (r *RandomIdGenerator) Generate() string {
s := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return ""
}
for i := range b {
b[i] = s[int(b[i])%len(s)]
}
return string(b)
}
// IdGeneratorを実装する構造体2
type UUIDGenerator struct{}
func NewUUIDGenerator() *UUIDGenerator {
return &UUIDGenerator{}
}
func (u *UUIDGenerator) Generate() string {
return uuid.New().String()
}
// generatorを使ってIDを生成する構造体
type IdCard struct {
id string
}
func NewIdCard(generator IdGenerator) *IdCard {
return &IdCard{id: generator.Generate()}
}
func (u *IdCard) Id() string {
return u.id
}
func main() {
app := fx.New(
fx.Provide(
NewIdCard,
// (a)
// fx.Annotate(
// NewRandomIdGenerator,
// fx.As(new(IdGenerator)),
// ),
// (b)
fx.Annotate(
NewUUIDGenerator,
fx.As(new(IdGenerator)),
),
),
fx.Invoke(func(card *IdCard) {
fmt.Println(card.Id())
}),
)
app.Run()
}
フックを利用する例
fx.Hook
構造体のOnStart
やOnStop
はアプリケーションの開始に必要な処理を記述し、例えばサーバの起動が想定されていると思います。
func NewDatabase(lc fx.Lifecycle, logger Logger) *DatabaseImpl {
db := &DatabaseImpl{}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
if err := db.Conn(); err != nil {
logger.Error(err.Error())
return err
}
logger.Info("connected to database")
return nil
},
})
return db
}
以下のコード例では、アプリケーションの起動時にデータベースの接続チェックをするということを仮の内容で示します。
フックを利用する例
type Logger interface {
Info(msg string)
Error(msg string)
}
type LoggerImpl struct{}
func (l *LoggerImpl) Info(msg string) {
fmt.Println("INFO: " + msg)
}
func (l *LoggerImpl) Error(msg string) {
fmt.Println("ERROR: " + msg)
}
func NewLogger() *LoggerImpl {
return &LoggerImpl{}
}
type Database interface {
Conn() error
}
type DatabaseImpl struct{}
func (db *DatabaseImpl) Conn() error {
is_success := false
if is_success {
return nil
}
return fmt.Errorf("failed to connect to database")
}
func NewDatabase(lc fx.Lifecycle, logger Logger) *DatabaseImpl {
db := &DatabaseImpl{}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
if err := db.Conn(); err != nil {
logger.Error(err.Error())
return err
}
logger.Info("connected to database")
return nil
},
})
return db
}
func main() {
app := fx.New(
fx.Provide(
fx.Annotate(
NewLogger,
fx.As(new(Logger)),
),
fx.Annotate(
NewDatabase,
fx.As(new(Database)),
),
),
fx.Invoke(func(db Database) {}),
)
app.Run()
}
パラメータと結果
利用するファクトリにおいて、ある結果とどのオブジェクトを利用するかということについては、タグで表現します。この内group
というタグは特殊な意味を持っており、該当するものをスライスとして引数で呼び出せます。
これを設定するためには、fx.ResultTags
またはfx.ParamTags
が提供されています。
以下のコードでは、Movie
とMusic
の結果およびPlayer
の呼び出しをhobby
としてタグ付けし、実際に初期化時にMovie
とMusic
がHobbyer
として解釈されスライスとして引数に利用される例を示します。
スライスで呼ぶ例
type Hobbyer interface {
Play()
}
type Movie struct {
Name string
}
func NewMovie() *Movie {
return &Movie{Name: "インセプション"}
}
func (m *Movie) Play() {
fmt.Println("Playing movie: ", m.Name)
}
type Music struct {
Name string
}
func NewMusic() *Music {
return &Music{Name: "アジカン"}
}
func (m *Music) Play() {
fmt.Println("Playing music: ", m.Name)
}
type Player struct {
Hobbies []Hobbyer
}
func NewPlayer(hobbies []Hobbyer) *Player {
return &Player{Hobbies: hobbies}
}
func (p *Player) Play() {
for _, h := range p.Hobbies {
h.Play()
}
}
func main() {
app := fx.New(
fx.Provide(
fx.Annotate(
NewMovie,
fx.As(new(Hobbyer)),
fx.ResultTags(`group:"hobby"`),
),
fx.Annotate(
NewMusic,
fx.As(new(Hobbyer)),
fx.ResultTags(`group:"hobby"`),
),
fx.Annotate(
NewPlayer,
fx.ParamTags(`group:"hobby"`),
),
),
fx.Invoke(func(p *Player) {
p.Play()
}),
)
app.Run()
}
以下のコードは、どのオブジェクトを呼び出したいかを指定する方法を示しています。上のコードと大体同じですが、スライスではなく単独のHobbyer
インターフェースを引数にしています。ここでは、name
としてそれぞれmovie
、music
がタグ付けされており、実際にはmovie
のみが呼び出される例を示します。
Movieを呼び出す例
type Hobbyer interface {
Play()
}
type Movie struct {
Name string
}
func NewMovie() *Movie {
return &Movie{Name: "インセプション"}
}
func (m *Movie) Play() {
fmt.Println("Playing movie: ", m.Name)
}
type Music struct {
Name string
}
func NewMusic() *Music {
return &Music{Name: "アジカン"}
}
func (m *Music) Play() {
fmt.Println("Playing music: ", m.Name)
}
type Player struct {
hobby Hobbyer
}
func NewPlayer(hobby Hobbyer) *Player {
return &Player{hobby: hobby}
}
func (p *Player) Play() {
p.hobby.Play()
}
func main() {
app := fx.New(
fx.Provide(
fx.Annotate(
NewMovie,
fx.As(new(Hobbyer)),
fx.ResultTags(`name:"movie"`),
),
fx.Annotate(
NewMusic,
fx.As(new(Hobbyer)),
fx.ResultTags(`name:"music"`),
),
fx.Annotate(
NewPlayer,
fx.ParamTags(`name:"movie"`),
),
),
fx.Invoke(func(p *Player) {
p.Play()
}),
)
app.Run()
}
…とここまで書いておいてなのですが、この値と結果はfx.In
およびfx.Out
埋め込み構造体で表現できます。というより、どうもざっと公式ドキュメントを見た感じではこの方法が比較的Uberのおすすめらしいです。
type MovieResult struct {
fx.Out
Movie Hobbyer `name:"movie"`
}
type PlayerParams struct {
fx.In
Hobby Hobbyer `name:"movie"`
}
注意点として、アノテーションのAs
は使えず、フィールドは大文字(パブリック)でなければいけません。
以下は値・結果構造体を利用した、上記と等価なコードの例を示します。
値・結果構造体の利用例
type Hobbyer interface {
Play()
}
type Movie struct {
Name string
}
type MovieResult struct {
fx.Out
Movie Hobbyer `name:"movie"`
}
func NewMovie() MovieResult {
return MovieResult{Movie: &Movie{Name: "インセプション"}}
}
func (m *Movie) Play() {
fmt.Println("Playing movie: ", m.Name)
}
type Music struct {
Name string
}
type MusicResult struct {
fx.Out
Music Hobbyer `name:"music"`
}
func NewMusic() MusicResult {
return MusicResult{Music: &Music{Name: "アジカン"}}
}
func (m *Music) Play() {
fmt.Println("Playing music: ", m.Name)
}
type Player struct {
hobby Hobbyer
}
type PlayerParams struct {
fx.In
Hobby Hobbyer `name:"movie"`
}
func NewPlayer(params PlayerParams) *Player {
return &Player{hobby: params.Hobby}
}
func (p *Player) Play() {
p.hobby.Play()
}
func main() {
app := fx.New(
fx.Provide(
fx.Annotate(
NewMovie,
),
fx.Annotate(
NewMusic,
),
fx.Annotate(
NewPlayer,
),
),
fx.Invoke(func(p *Player) {
p.Play()
}),
)
app.Run()
}
デコレート
fx.Decorate
はライフサイクルとしてfx.Provide
とfx.Invoke
の間にある段階のもので、恐らくInvoke
以降に実際に使いたいオブジェクトの設定をしたい用としてあるのだと思います。
一応公式ドキュメントでは強制終了があることを示す為ですが、SQLの接続を試行する例の説明が載っていました。
注意点として、fx.Annotate
を付加できますが、注釈済みであれば修飾できません。
以下のコードではSportsPlayer
のSports
の部分だけを変更して、実際にそれがInvoke
で呼び出される例を示します。
fx.Decorate
type Sports struct {
Name string
}
func NewSports() *Sports {
return &Sports{Name: "Football"}
}
type Person struct {
Name string
}
func NewPerson() *Person {
return &Person{Name: "John"}
}
type SportsPlayer struct {
person *Person
sports *Sports
}
func NewSportsPlayer(p *Person, s *Sports) *SportsPlayer {
return &SportsPlayer{person: p, sports: s}
}
func (p *SportsPlayer) Say() {
fmt.Printf("I am %s, I like %s\n", p.person.Name, p.sports.Name)
}
func main() {
app := fx.New(
fx.Provide(
NewSports,
NewPerson,
NewSportsPlayer,
),
fx.Decorate(func(sp *SportsPlayer) *SportsPlayer {
sp.sports = &Sports{Name: "Basketball"}
return sp
}),
fx.Invoke(func(p *Person, s *Sports, player *SportsPlayer) {
// Person: John, Sports: Football
// I am John, I like Basketball
fmt.Printf("Person: %s, Sports: %s\n", p.Name, s.Name)
player.Say()
}),
)
app.Run()
}
おわりに
以上基本的な部分にはなりますが、現在の機能などに関して私なりの解釈を記載しました。
少しでも皆さんのご参考になれば幸いです。
Discussion