概要
- 前回の続き
- 今回はインターフェースについて学習
- インターフェースは並行実行と並んで、Go言語の特徴的な(良い意味で)概念とのこと
参考
インターフェース
- Go言語で唯一の抽象型(実装を持たない型)で2つの特徴がある
- メソッドの集合: 実装するべきメソッドを定義する(Javaと同じ概念)
- 型:変数がインターフェースを基盤とする型を持つことで、任意の型の値を代入できる
-
var i interface{}
または、var i any
とかくことで、任意の値やメソッドを記憶できる。空インターフェースとよぶ。(tsのany型と同じ用途)
-
- インターフェースも型の一種。er(〜する人)で命名する
インターフェースの定義
type Stringer interface {
String() string //実装するメソッド
}
- Go言語が特別なのは、暗黙的なインターフェースである点
- 具象型では、インターフェースを実装することを宣言しない。(Javaでいうimplemntsをしない)
- インターフェースのメソッドを含めば、自動的に実装したことになる
- 静的言語と動的言語の両方を併せ持っている
- つまり、型による安全性と、型の柔軟性をもつ
暗黙的なインターフェースの例
// 具象型
type LogicProvider struct{}
// 具象型のメソッド
func(lp LogicProvider) Process(data string) string {
fmt.Println("具象型のメソッド:", data)
return data
}
// インターフェース
type Logic interface {
// インターフェースのメソッド定義
Process(data string) string
}
// 利用する型
type Client struct{
L Logic //インターフェースをメンバで定義
}
// 利用する型のメソッド
func (c Client) Program() {
data := "use-client"
// インターフェースのメソッド呼び出し
c.L.Process(data)
}
func main() {
// 実行時に、利用する側が、具象型を設定する
c := Client {
L: LogicProvider{},
}
c.Program()
}
- インターフェースを知っているのは、利用する側(Client)のみ。
- インターフェースを実装しているLogicProviderはそのことを知らない(暗黙的な実装)
- 利用する側(Client)がインターフェースを定義し、利用する機能を指定する。
- 上記の例で、インターフェースにメソッドを追加した場合、LogicProviderは、Logicインターフェースの具象型ではなくなるので、
- Clinentを作成する際の
L: LogicProvider{}
でエラーとなる。(=型の安全性) - 一方で、LogicProviderの定義ではエラーは発生しない。(=型の柔軟性)
- Clinentを作成する際の
埋め込みインターフェース
- 構造体と同様に、インターフェースにインターフェースを埋め込むこともできる
埋め込みインターフェース
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Closer() error
}
type ReadCloser interface {
Reader
Closer
}
インターフェースを受け取り、構造体を返す
- 関数の起動はインターフェースで行い、関数の出力は具体的な型にするというルール
- インターフェースを返さないようにする。(デカップリング、バージョン管理などの問題が生じる)
- ファクトリ関数を作る際も、返却する具象型の数分、関数をつくるべき。
- ただし、errorは例外らしい。(返却するerror自体がインターフェース)
nil
- インターフェースは型と値の両方がnilの場合にnilとなる。
nil
var s *string
fmt.Println(s == nil) // true
var i any // interfase{}でも同じ
fmt.Println(i == nil) // true
i = s // 型のポインタを渡している
fmt.Println(i == nil) // false: 値のポインタはnil、型のポインタは非nil
型アサーション
- インターフェース型の変数が具象型を持っているかを調べるのに、型アサーションを使用する。
-
インターフェース変数名.(具象型)
の形式
-
- Goでは型の扱いは慎重に行っているため、2つの型が同じ基底型をもっていても型アサーションはできない
- 安全に型アサーションを利用するために、カンマOkイディオムを利用する
型アサーション
type MyInt int
func main() {
var i any // 空インターフェース
var mine MyInt = 20
i = mine;
i2 := i.(MyInt) // 型アサーション
i3 := i.(string) // 具象型が違うのでパニック
i4 := i.(int) // 基底型は同じだが、実際の具象型はMyIntなのでパニック
// 型アサーションを利用する場合、カンマokイディオムで確認する
i5,ok := i.(int)
if !ok {
err := fmt.Errorf("iの型(値:%v)が想定外",i)
fmt.Println(err.Error())
os.Exit(1)
}
fmt.Println(i2,i3,i4,i5);
}
型Switch
- インターフェースが複数の型のいずれかの場合を判定するのに型switchを利用する
- switch文に
インターフェース型変数.(type)
を指定する
- switch文に
- 型switchの目的は、インターフェースを具体的な型にすることなので、シャドーイングが役に立つ
-
switch i := i.(type)
シャドーイング:対象の変数を同名の変数に代入する
-
型switch
func doTypeSwitch(i any) {
switch i := i.(type) {
case nil:
// ...
case int:
// ...
case string:
// ...
}
}
関数型インターフェースによる依存性の注入
- 関数もインターフェースを実装できる
関数のインターフェース実装
type Hnaler interface {
ServerHttp(http.ResponseWriter, *http.Request)
}
// 関数インターフェースの実装
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServerHttp(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
- Go言語は、上記、関数インターフェースや、暗黙のインターフェースの仕組みを使うことで、追加のライブラリ不要で、依存性の注入が簡潔に記載できる
DIの実装例(ログ記録アプリケーション)
- 1.ログ記録関数と、データ保存場所の作成
DI-1
// ログを記録する関数
func LogOutput(message string) {
fmt.Println(message)
}
// アプリのデータを保存する場所(SimpleDataStore)
type SimpleDataStore struct {
userData map[string]string
}
func (sds SimpleDataStore) UserNameForID(userID string) (string, bool) {
name, ok := sds.userData[userID]
return name,ok
}
// SimpleDataStoreのインスタンスを作成するファクトリ関数
func NewSimpleDataStore() SimpleDataStore {
return SimpleDataStore{
userData: map[string]string{
"1": "hase",
"2": "taro",
"3": "hana",
},
}
}
- 2.ビジネスロジック(ユーザを取得し、挨拶を行う)が使用するため、適合するインターフェースを作成する
- ユーザデータに上記の保存場所(SimpleDataStore)。また、その際にログ記録関数(LogOutput)を使用するが、上記を直接使用したくない。(データ保存やログ記録を別の仕組みで行うかもしれないので)
- そのため、ビジネスロジックが何に依存するかを説明するインターフェースを用意する
DI-2
// ビジネスロジックが依存するインターフェース
type DataStore interface {
UserNameForId(userID string)(string,bool)
}
type Logger interface {
Log(message string)
}
// LogOutputが上記Loggerインターフェースに適合するように関数型を定義
type LoggerAdapter func(message string)
func (lg LoggerAdapter) Log(message string) {
lg(message)
}
- 3.ビジネスロジックの実装
DI-3
// ビジネスロジック
// 定義したフィールドはどちらも具象型(LogOutput, SimpleDataStore)とは依存していない
type SimpleLogic struct {
l Logger
ds DataStore
}
func (sl SimpleLogic) SayHello(userID string) (string,error) {
sl.l.Log("SayHello(" + userID + ")")
name, ok := sl.ds.UserNameForId(userID)
if !ok {
return "", errors.New("不明なユーザ")
}
return name + "さんこんにちは", nil
}
func (sl SimpleLogic) SayGoodbye(userID string) (string,error) {
sl.l.Log("SayGoodbye(" + userID + ")")
name, ok := sl.ds.UserNameForId(userID)
if !ok {
return "", errors.New("不明なユーザ")
}
return name + "さんさようなら", nil
}
// ビジネスロジックのファクトリ関数
func NewSimpleLogic(l Logger, ds DataStore) SimpleLogic {
return SimpleLogic{
l: l,
ds: ds,
}
}
- 4.コントローラー(ビジネスロジックの利用側)の作成
DI-4
// ビジネスロジックから、コントローラーが利用するものを定義
type Logic interface {
SayHello(userID string) (string, error)
}
// コントローラーの作成
type Controller struct {
l Logger
logic Logic
}
func (c Controller) HandleGreeting(w http.ResponseWriter, r *http.Request) {
c.l.Log("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,
}
}
- 5.最後にmain関数で実行
DI-5
func main() {
l := LoggerAdapter(LogOutput)
ds := NewSimpleDataStore()
logic := NewSimpleLogic(l,ds)
c := NewController(l, logic)
http.HandleFunc("/hello", c.HandleGreeting)
http.ListenAndServe(":8080",nil)
}
以下で実行
- すべて具象型を設定しているのはmain関数内のみなので、実装を入れ替えたい場合、書きかえるのはmain関数のみになる
- 依存性注入によって、依存を外部化することができた
おわりに
- 暗黙的なインターフェース、依存性注入については、大切な観点なので、読み返し理解を深めていこうと思う。
- 暗黙的なインターフェースは便利な半面、インターフェースを実装(実現)してるのかわからないのは、不安に感じてしまう。
- DIについては参考書の例題が長くて、処理を追うのが大変だった。(ただでさえ、DI難しいのに)
- もうすこし簡単な例があればよかったかも。
- JavaのDIはフレームワーク依存だったのであまり深く考えなかったが、順序だってインターフェースを作成していき、最後のmain関数で依存性を注入する際、そのシンプルさと疎結合に感動できた。
- 自分でも使いこなせるようにがんばろう。。。