見出し画像

Dependency injection, Interfaces and Type Assertions, Function Type/interface | Go日記: 『Learning Go』

Use Type Assertions and Type Switches Sparingly

前記事の続きで interface{} と Type Assersions のあたりを読んだ。

Use Type Assertions and Type Switches Sparingly

Bodner, Jon. Learning Go (p.226). O'Reilly Media. Kindle 版.

Sparingly は「倹約して」「倹しく」「乏しく」という意味の副詞。

The Go Playground でサンプルコードをいろいろいじってみて、この言い方に尽きると思う。reflection パッケージの内容まではまだ学習が及んでいない。

package main
import (
	"fmt"
)
type MyInt int
func main() {
	var mine MyInt
	mine = 20
	var i interface{}
	i = mine + 1
	j := i.(MyInt) + 1
	s := "Ho Ho Ho"
	foo(mine) // Got MyInt 20
	foo(i)    // Got MyInt 21
	foo(j)    // Got MyInt 22
	foo(s)    // Another Ho Ho Ho
	i, ok := i.(int)
	if ok {
		fmt.Println(i.(int) + 1)
	} else {
		err := fmt.Errorf("Error: Unexpected type for %v", i)
		fmt.Println(err) // Error: Unexpected type for 0
	}
}
func foo(x interface{}) {
	switch x.(type) {
	case MyInt:
		fmt.Println("Got MyInt", x)
	case int:
		fmt.Println("Got Int", x)
	default:
		fmt.Println("Another", x)
	}
}

Function Type --- interface

Function Types Are a Bridge to Interfaces

... Go allows methods on any user-defined type, including user-defined function types. This sounds like an academic corner case, but they are actually very useful. They allow functions to implement interfaces.

Go はどんなユーザ定義型に対してもメソッドの実装を許している。これにはユーザ定義関数型も含まれる。これはアカデミックな世界のコーナーケース(特殊なケース)のようだけど、実はとても便利。この言語仕様のおかげで関数にインターフェイスを実装することができる。

Bodner, Jon. Learning Go (p.230). O'Reilly Media. Kindle 版.

書籍内では HTTP Handler の例が紹介されている。下記は自分なりに商品情報を云々する場面を想像しながら書いてみたサンプルコード。

package main
import (
	"fmt"
)
type ItemInfo struct {
	Name string
	Desc string
}
func (i ItemInfo) String() string {
	return fmt.Sprintf("ItemName: %v, Desc: %v", i.Name, i.Desc)
}
type ItemInfoGetter interface {
	GetItemInfo(itemId string) ItemInfo
}
type ItemInfoGetterFunc func(itemId string) ItemInfo
func (getter ItemInfoGetterFunc) GetItemInfo(itemId string) ItemInfo {
	return getter(itemId)
}
func pickup(itemId string, getter ItemInfoGetter) ItemInfo {
	return getter.GetItemInfo(itemId)
}
func main() {
	var getter ItemInfoGetterFunc
	getter = func(itemId string) ItemInfo {
		var info ItemInfo
		if itemId == "" {
			return info
		}
		info.Name = fmt.Sprintf("item %v", itemId)
		info.Desc = "...description..."
		return info
	}
	fmt.Println(pickup("", getter))    // ItemName: , Desc:
	fmt.Println(pickup("100", getter)) // ItemName: item 100, Desc: ...description...
}

商品ID itemId が既知の情報だとして、商品情報を得る方法と商品IDを直接に結び付けない(言い換えれば、方法と情報を分離する)ということが求められる場合に有効だと思う。サンプル内では ItemInfoGetterFunc というユーザ定義型関数しか用意していないが、「商品情報を得る方法」がこの他にも複数あったりする場合などが考えられる。ItemInfoGetter インターフェイスに向けて(main内で)どんな関数をバインドするかを選択し、pickup 関数に渡している。pickup 関数では ItemInfoGetter インターフェイスを満たしている関数から 、やはり ItemInfoGetter インターフェイスで定義されている GetItemInfo メソッドを呼び出しているが、これがまさに現時点で実行させたい ItemInfoGetterFunc というユーザ定義型関数に従って実装された関数である、という造りになっている。

ちなみに『Learning Go』で見かけた HTTP Handler の例は『プログラミング言語Go』の7.7でもやはり interface の解説のために取り上げられていた。

DI (Dependency injection)

One of the surprising benefits of Go’s implicit interfaces is that they make dependency injection an excellent way to decouple your code. While developers in other languages often use large, complicated frameworks to inject their dependencies, the truth is that it is easy to implement dependency injection in Go without any additional libraries.

Go の暗黙のインターフェイスがもたらす驚くべきベネフィットのひとつは、依存性注入 DI, dependency injection がコードを分離するための優れた方法になることです。 他の言語の開発者は、依存性を注入するために大規模で複雑なフレームワークを使用することがよくありますが、実際には、追加のライブラリがなくても Go で依存性注入を実装するのは簡単です。

Bodner, Jon. Learning Go (pp.231-232). O'Reilly Media. Kindle 版.

書籍で簡単な具体例を用意してくれていたので読みながら写経し理解を進めた。

書籍からの引用も交えながら解説の順番どおりにコーディングしてみた。

package main
import (
       "errors"
       "fmt"
       "net/http"
)
func LogOutput(message string) {
       fmt.Println(message)
}
type SimpleDataStore struct {
       userData map[string]string
}
func (sds SimpleDataStore) UserNameForID(userID string) (string, bool) {
       name, ok := sds.userData[userID]
       return name, ok
}
func NewSimpleDataStore() SimpleDataStore {
       return SimpleDataStore{
               userData: map[string]string{
                       "1": "Fred",
                       "2": "Mary",
                       "3": "Pat",
               },
       }
}
// we might want to use a different logger or data store later.
type DataStore interface {
       UserNameForID(userID string) (string, bool)
}
type Logger interface {
       Log(message string)
}
// To make our LogOutputfunction meet this interface,
// we define a function type with a method on it.
type LoggerAdapter func(message string)
func (lg LoggerAdapter) Log(message string) {
       lg(message)
}
// business logic
type SimpleLogic struct {
       l  Logger    // depends on interface(not cocrete type)
       ds DataStore // depends on interface(not cocrete type)
       // and they're unexported.
       /*
               This means they can only be accessed by code within the same package as SimpleLogic.
               Bodner, Jon. Learning Go (p.235). O'Reilly Media. Kindle 版.
       */
}
func (sl SimpleLogic) SayHello(userID string) (string, error) {
       sl.l.Log("in SayHello for " + userID)
       name, ok := sl.ds.UserNameForID(userID)
       if !ok {
               return "", errors.New("unknown user")
       }
       return "Hello, " + name, nil
}
func (sl SimpleLogic) SayGoodbye(userID string) (string, error) {
       sl.l.Log("in SayGoodbye for " + userID)
       name, ok := sl.ds.UserNameForID(userID)
       if !ok {
               return "", errors.New("unknown user")
       }
       return "Goodbye, " + name, nil
}
func NewSimpleLogic(l Logger, ds DataStore) SimpleLogic {
       return SimpleLogic{
               l:  l,
               ds: ds,
       }
}
/*
Now we get to our API. We’re only going to have a single endpoint, /hello, which says hello to the person whose user ID is supplied.
Bodner, Jon. Learning Go (p.235). O'Reilly Media. Kindle 版.
*/
type Logic interface {
       SayHello(userID string) (string, error)
       /*
               This method is available on our SimpleLogic struct, but once again, the concrete type is not aware of the interface.
               Furthermore, the other method on SimpleLogic, SayGoodbye, is not in the interface because our controller doesn’t care about it.
               Bodner, Jon. Learning Go (p.235). O'Reilly Media. Kindle 版.
       */
}
/*
The interface is owned by the client code, so its method set is customized to the needs of the client code:
Bodner, Jon. Learning Go (pp.235-236). O'Reilly Media. Kindle 版.
*/
type Controller struct {
       l     Logger
       logic Logic
}
func (c Controller) HandleGreeting(w http.ResponseWriter, r *http.Request) {
       c.l.Log("In SayHello")
       userID := r.URL.Query().Get("user_id")
       message, err := c.logic.SayHello(userID)
       if err != nil {
               w.WriteHeader(http.StatusBadRequest)
               w.Write([]byte(err.Error()))
               return
       }
       w.Write([]byte(message))
}
func NewController(l Logger, logic Logic) Controller {
       return Controller{
               l:     l,
               logic: logic,
       }
}
func main() {
       l := LoggerAdapter(LogOutput)
       ds := NewSimpleDataStore()
       logic := NewSimpleLogic(l, ds)
       c := NewController(l, logic)
       http.HandleFunc("/hello", c.HandleGreeting)
       http.ListenAndServe(":8080", nil)
}

これを go build してできたバイナリーをコンソールで実行し、ブラウザで 

http://localhost:8080/hello?user_id=1

にアクセスすると、

Hello, Fred

と表示され、また、例えば

http://localhost:8080/hello?user_id=100

などとアクセスすると、

unknown user

と表示される。

この操作の間、コンソールには

In SayHello
in SayHello for 1
In SayHello
in SayHello for 100
In SayHello
in SayHello for 1

このようなログが印字される。

***

main 関数だけが具象型がどのように配置されるかを知っているのがポイント。違った実装に切り替えたい場合は main 関数の中のパッチングをいじれば良い。依存性注入 dependency injection を介して依存関係を外部化している。依存関係を外部化することでコードの進化に必要な変更を制限しているのが味噌。

しかし手作業でDIを実装していくのは手間でもあるので自動化のためのライブラリーとして Wire が紹介されている。

SN

この記事が気に入ったらサポートをしてみませんか?