はじめに
プロダクトで利用する一部のデータは別プロダクト(サービス)との gRPC 通信を経由して取得しています.担当しているプロダクトで,今後問い合わせる先のサービスが変更されることから,サービスの切り替え対応のタスクに取り組みました.この際,円滑な切り替えを実現するために,Go の embedding を利用した方法をとったので,それを紹介します.
Embedding とは
Go には典型的なサブクラス化の概念はないが,構造体やインターフェースの中に型を embedding することで,実装を部分的に借りることができます.
Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface.
サブクラス化(継承)と異なる点としては,embed された型は外側の型のメソッドとして呼び出せますが,メソッドが呼び出された際のレシーバーは内側の型のものになります.
When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one.
type Inner struct {
Name string
}
func (i *Inner) Greet() {
fmt.Printf("Hi, %s\n", i.Name)
}
type Outer struct {
Name string
Inner
}
func main() {
outer := Outer{
Name: "Alice",
Inner: Inner{Name: "Bob"},
}
// outer.Greet() では Inner 構造体の Name が表示される
outer.Greet() // -> "Hi, Bob"
}
また,embedding した型は名前が衝突する問題が発生するため,以下のルールで解決されています.
- 同じ名前のフィールドやメソッドがある場合,より深いネストに入っているものが隠されます.
a field or method X hides any other item X in a more deeply nested part of the type.
...
func main() {
outer := Outer{
Name: "Alice",
Inner: Inner{Name: "Bob"},
}
// outer.Name では Outer 構造体の Name が表示される
// Inner 構造体の Name は同名のため隠されている
fmt.Println(outer.Name) // -> "Alice"
}
- 同じ名前のフィールドやメソッドが同じネストの深さにある場合,通常はエラーが返ります.しかし,その名前が型定義の外で一度も使われない場合は,問題になりません.
if the same name appears at the same nesting level, it is usually an error[, h]owever, if the duplicate name is never mentioned in the program outside the type definition, it is OK.
type FirstInner struct {
Name string
}
func (i *FirstInner) Greet() {
fmt.Printf("Hi, %s\n", i.Name)
}
type SecondInner struct {
Name string
}
func (i *SecondInner) Hello() {
fmt.Printf("Hi, %s\n", i.Name)
}
type Outer struct {
FirstInner
SecondInner
}
func main() {
outer := Outer{
FirstInner: FirstInner{Name: "Alice"},
SecondInner: SecondInner{Name: "Bob"},
}
// outer.Name が FirstInner.Name と SecondInner.Name のどちらかわからない
fmt.Println(outer.Name) // -> エラー:"ambiguous selector outer.Name"
// outer.Name が直接参照されないため,エラーにならない
outer.Greet() // -> "Hi, Alice"
outer.Hello() // -> "Hi, Bob"
}
サービスの切り替え実装について
プロダクトで利用しているサービスは元々以下のように実装されていました.
// Service ではプロダクトが利用するサービスが持つべき機能を定義
type Service interface {
GetUser()
UpdateUser()
}
// 既存の gRPC を利用するサービス
type ExistingService struct {
SomeClient proto.SomeServiceClient
}
// ExistingService は Service インターフェースのどちらのメソッドも実装している
func (s ExistingService) GetUser() {}
func (s ExistingService) UpdateUser() {}
// Service を返す関数
func NewService() Service {
return ExistingService{
SomeClient: proto.NewSomeServiceClient(),
}
}
そこで,以下のように切り替え先のサービスを別の構造体で定義することで,メソッド単位で利用するサービスを切り替えできるようになります.
...
// 新規の gRPC を利用するサービスで,embedding を利用した構造体
type ReplacingService struct {
SomeClient proto.SomeServiceV2Client // gRPC client が変わる想定
ExistingService
}
// ReplacingService は Service インターフェースの片方のメソッドしか実装していない
func (s ReplacingService) GetUser() {}
// Service を返す関数を,上記の追加に合わせて変更
func NewService() Service {
return ReplacingService{
SomeClient: proto.NewSomeServiceV2Client(),
ExistingService: ExistingService{
SomeClient: proto.NewSomeServiceClient(),
},
}
}
上記の場合,GetUser()
は新たに実装している ReplacingService
のメソッドとして呼ぶことができ,まだ実装できていない UpdateUser()
は ExistingService
のメソッドを呼ぶことができ,Service
インターフェースを満たしている状態を維持できます.
まとめ
Embedding を利用することでサービスの切り替えを DI などを用いずに実現することができました.サブクラス化とは違う挙動には気をつけなければなりませんが,とても強力な機能なので機会があれば是非利用しましょう!