LoginSignup
0
0

[Go]インターフェース

Last updated at Posted at 2023-08-27

概要

  • 前回の続き
  • 今回はインターフェースについて学習
    • インターフェースは並行実行と並んで、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の定義ではエラーは発生しない。(=型の柔軟性)

埋め込みインターフェース

  • 構造体と同様に、インターフェースにインターフェースを埋め込むこともできる
埋め込みインターフェース
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 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)
}

以下で実行

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

  • すべて具象型を設定しているのはmain関数内のみなので、実装を入れ替えたい場合、書きかえるのはmain関数のみになる
  • 依存性注入によって、依存を外部化することができた

おわりに

  • 暗黙的なインターフェース、依存性注入については、大切な観点なので、読み返し理解を深めていこうと思う。
  • 暗黙的なインターフェースは便利な半面、インターフェースを実装(実現)してるのかわからないのは、不安に感じてしまう。
  • DIについては参考書の例題が長くて、処理を追うのが大変だった。(ただでさえ、DI難しいのに)
    • もうすこし簡単な例があればよかったかも。
  • JavaのDIはフレームワーク依存だったのであまり深く考えなかったが、順序だってインターフェースを作成していき、最後のmain関数で依存性を注入する際、そのシンプルさと疎結合に感動できた。
    • 自分でも使いこなせるようにがんばろう。。。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0