ホーム プログラミング言語 golang go reflect Go 言語インジェクション ライブラリ: 依存性インジェクション






Go 言語インジェクション ライブラリ: 依存性インジェクション




 
 
injectを紹介する前に、「依存性注入」と「制御の反転」という2つの概念を簡単に紹介しましょう。

 

通常の状況では、関数またはメソッドの呼び出しは、アクティブで直接的な動作です。関数を呼び出す前に、呼び出される関数の名前、パラメーターの種類などを明確に把握する必要があります。

いわゆる制御の反転とは、この能動的な動作を間接的な動作に変えることです。関数やオブジェクトを直接呼び出す代わりに、フレームワーク コードを使用して間接的な呼び出しと初期化を行います。この動作は「制御の反転」と呼ばれます。制御の反転の概念をうまく説明してください。

依存関係の注入は、制御の反転を実現する方法です。制御の反転が設計アイデアである場合、依存関係の注入はこのアイデアの実装です。制御の反転は、パラメーターまたはインスタンスを注入することによって実現されます。特別な説明がなければ、依存関係の注入と制御の反転は別のものであると考えてよいでしょう。

制御の反転の価値は分離にあります。制御の反転では、コードを死ぬほど書く必要はありません。制御の反転のフレームワーク コードは、構成を読み取り、オブジェクトを動的に構築できます。これは、特にJava Springフレームワークに反映されています。

練習を注入する

inject は、依存関係注入の Go 言語実装です。実行時にパラメーターを挿入し、メソッドを呼び出すことができます。これは、Martini フレームワーク (Go 言語のよく知られた Web フレームワーク) の基本コアです。

具体的な実装を紹介する前に、文字列型の関数名を使用して関数を呼び出す方法について考えてみましょう。 Go 言語にはクラス名から直接オブジェクトを構築できる Java の Class.forName メソッドがないため、この方法は実現できませんが、map を使って文字列から関数へのマッピングを実装する方法が考えられます。コードは次のとおりです。

 func fl() {
    println("fl")
}
func f2() {
    println("f2")
}
funcs := make(map[string]func())
funcs["fl"] = fl
funcs["f2"] = fl
funcs["fl"]()
funcs["f2"]() 

しかし、これには欠点があり、マップの Value 型は func() として記述されており、パラメータと戻り値の型が異なる関数は汎用的に使用できません。この問題は、マップの値をインターフェイスの空のインターフェイス タイプとして定義することで解決できますが、型アサーションまたはリフレクションを使用して実装する必要があります。型アサーションにより、値は と等価になり、戻ります。リフレクションは実現可能な方法。

inject パッケージは、リフレクションによって関数注入呼び出しを実装します。例を見てみましょう。

 package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type S1 interface{}
type S2 interface{}

func Format(name string, company S1, level S2, age int) {
    fmt.Printf("名前=%s, 会社=%s, レベル=%s, 年齢=%d!\n", name, company, level, age)
}
func main() {
    //インスタンスの作成をコントロール
    inj := inject.New()
    //実引数の注入
    inj.Map("tom")
    inj.MapTo("tencent", (*S1)(nil))
    inj.MapTo("T4", (*S2)(nil))
    inj.Map(23)
    //関数の逆転呼び出し
    inj.Invoke(Format)
} 

操作の結果は次のようになります。

name = tom, company=tencent, level=T4, age = 23!

inject はパラメータの注入と関数の呼び出しという一般的な関数を提供し、inject.New() は関数の注入と呼び出しを実現するために使用されるコントロール インスタンスの作成に相当することがわかります。 inject パッケージは関数の注入を提供するだけでなく、構造体型の注入も実装します。サンプル コードは次のとおりです。

package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type S1 interface{}
type S2 interface{}
Staff struct {
    name    String `inject`
    company S1     `inject`
    level   S2     `inject`
    age     int    `inject`
}

func() {
    //注入されたインスタンスを作成する
    s := Staff{}
    //インスタンスの作成を制御する
    inj := inject.New()
    //注入値の初期化
    inj.Map("tom")
    inj.MapTo("google", (*S1)(nil))
    inj.MapTo("T4", (*S2)(nil))
    inj.Map(23)
    //構造体への注入の実現
    inj.Apply(&s)
    //結果の出力
    fmt.Printf("s = %v\n", s)
} 

操作の結果は次のようになります。

s = {tom google T4 23}

inject は構造タイプに対する一般的な注入メソッドを提供することがわかります。ここまではマクロレベルでinjectで何ができるのかを理解したところですが、次はソースコード実装の観点からinjectを分析していきます。

注入原理解析

inject パッケージには、inject.go ファイルと inject_test.go ファイルの 2 つのファイルしかありません。ここでは、inject.go ファイルだけに注目する必要があります。

inject.go は短く簡潔で、コメントと空白行を含むコードはわずか 157 行で、以下に示すように、コードには親インターフェイスと 3 つのサブインターフェイスを含む 4 つのインターフェイスが定義されています。

type Injector interface {
    Applicator
    Invoker
    TypeMapper
    SetParent(Injector)
}

type Applicator interface {
    Apply(interface{}) error
}

type Invoker interface {
    Invoke(interface{}) ([]reflect.Value, error)
}

type TypeMapper interface {
    Map(interface{}) TypeMapper
    MapTo(interface{}, interface{}) TypeMapper
    Get(reflect.Type) reflect.Value
}

Injector インターフェイスは、Applicator、Invoker、および TypeMapper インターフェイスの親インターフェイスであるため、Injector インターフェイスを実装する型は、Applicator、Invoker、および TypeMapper インターフェイスも実装する必要があります。

  • Applicator インターフェイスは、構造体の注入に使用される apply メンバーのみを指定します。
  • Invoker インターフェイスは、呼び出し先を実行するために使用される Invoke メンバーのみを指定します。
  • TypeMapper インターフェイスは 3 つのメンバーを指定します。Map と MapTo はどちらもパラメーターの挿入に使用されますが、使用方法が異なります。Get は、呼び出し時に挿入されたパラメーターを取得するために使用されます。

さらに、Injector は、親 Injector を設定するために使用される SetParent の動作も指定します。実際、これは継承を見つけることと同じです。つまり、注入されたパラメータを Get メソッドで取得すると親に戻り、パラメータが見つかるか nil で終了するまで再帰的な処理となります。

type injector struct {
    values map[reflect.Type]reflect.Value
    parent Injector
}

func InterfaceOf(value interface{}) reflect.Type {
    t := reflect.TypeOf(value)

    for t.Kind() == reflect.Ptr {
        t = t.Elem()
    }

    if t.Kind() != reflect.Interface {
        panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
    }

    return t
}

func New() Injector {
    return &injector{
        values: make(map[reflect.Type]reflect.Value),
    }
}

インジェクターは、inject パッケージで定義されている唯一の構造体であり、すべての操作は 2 つのメンバー値と親を持つインジェクター構造体に基づいて実行されます。 Values は、注入されたパラメータを保存するために使用されます。これは、reflect.Type をキーとして、reflect.Value を値として持つマップです。これを理解すると、Map と MapTo を理解するのに役立ちます。

New メソッドはインジェクター構造体を初期化するために使用され、インジェクター構造体へのポインターを返しますが、この戻り値は Injector インターフェースによってラップされます。

InterfaceOf メソッドの実装コードは数行だけですが、これはインジェクターの中核です。 InterfaceOf メソッドのパラメータは、インターフェイス タイプへのポインタである必要があります。そうでない場合は、パニックが発生します。 InterfaceOf メソッドの戻り値の型は、reflect.Type であり、インジェクターのメンバー値は、reflect.Type 型をキーとしたマップであることを覚えておく必要があります。このメソッドの機能は実際にはパラメータの型を取得することだけであり、その値は気にしません。

サンプルコードは次のとおりです。

package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type SpecialString interface{}

func main() {
    fmt.Println(inject.InterfaceOf((*interface{})(nil)))
    fmt.Println(inject.InterfaceOf((*SpecialString)(nil)))
} 

操作の結果は次のようになります。

interface {}
main.SpecialString

InterfaceOf メソッドは、具体的にどのような値が格納されているかに関係なく、パラメーターの型を取得するために使用されます。

 func (i *injector) Map(val interface{}) TypeMapper {
    i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
    return i
}

func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
    i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
    return i
}

func (i *injector) Get(t reflect.Type) reflect.Value {
    val := i.values[t]
    if !val.IsValid() && i.parent != nil {
        val = i.parent.Get(t)
    }
    return val
}

func (i *injector) SetParent(parent Injector) {
    i.parent = parent
} 

Map メソッドと MapTo メソッドは両方ともパラメータを注入するために使用され、パラメータはインジェクタのメンバー値に格納されます。これら 2 つのメソッドの機能はまったく同じです。唯一の違いは、Map メソッドはパラメーター値自体の型をキーとして使用し、MapTo メソッドには特定の型をキーとして指定できる追加パラメーターがあることです。ただし、MapTo メソッドの 2 番目のパラメーター ifacePtr は、インターフェイス ポインター型である必要があります。これは、ifacePtr が最終的に InterfaceOf メソッドのパラメーターとして使用されるためです。

MapTo メソッドがあるのはなぜですか?注入されたパラメータは型をキーとしてマップに格納されるため、関数内に同じ型のパラメータが複数ある場合、Map の実行後に注入されたパラメータが、Map Injected パラメータを介して渡された以前のパラメータを上書きすることが考えられます。 。

SetParent メソッドは、インジェクターの親インジェクターを指定するために使用されます。 Get メソッドは、reflect.Type を通じてインジェクターの値メンバーから対応する値を取得します。無効な値が見つかるか返されるまで、親が設定されているかどうかを確認し、最終的に Get メソッドの戻り値は次のようになります。 IsValid メソッドによって検証されます。

サンプルコードは次のとおりです。

package main

import (
    "fmt"
    "reflect"
    "github.com/codegangsta/inject"
)

type SpecialString interface{}

func main() {
    inj := inject.New()
    inj.Map("IT基礎")
    inj.MapTo("Golang", (*SpecialString)(nil))
    inj.Map(20)
    fmt.Println("文字列は有効ですか?", inj.Get(reflect.TypeOf("Go言語入門チュートリアル")).IsValid())
    fmt.Println("特殊文字列は有効ですか?", inj.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid())
    fmt.Println("int は有効ですか?", inj.Get(reflect.TypeOf(18)).IsValid())
    fmt.Println("[]byte は有効ですか?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())
    inj2 := inject.New()
    inj2.Map([]byte("test"))
    inj.SetParent(inj2)
    fmt.Println("[]byte は有効ですか?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())
} 

 

上記の例を通して、SetParent がどのような動作をするかがわかると思いますが、オブジェクト指向の検索チェーンとよく似ています。

 func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
    t := reflect.TypeOf(f)

    var in = make([]reflect.Value, t.NumIn()) // tがFuncの種類でない場合はパニックを起こす
    for i := 0; i < t.NumIn(); i++ {
        argType := t.In(i)
        val := inj.Get(argType)
        if !val.IsValid() {
            return nil, fmt.Errorf("%vタイプの値が見つかりませんでした", argType)
        }
        in[i] = val
    }
    return reflect.ValueOf(f).Call(in), nil
} 

Invoke メソッドは、関数を動的に実行するために使用されます。もちろん、Invoke を通じて実行される関数は、注入されたパラメータを取り出し、reflect パッケージの Call メソッドを通じて呼び出すため、実行前に Map または MapTo を通じてパラメータを注入することもできます。 。 Invoke によって受け取られるパラメータ f はインターフェイス型ですが、f の基になる型は func でなければなりません。そうしないとパニックが発生します。

package main

import(
    "fmt"
    "github.com/codegangsta/inject"
)

特殊文字列インターフェース{}

func Say(name string、gender SpecialString、age int){
    fmt.Printf("私の名前は%sです、性別は%sです、年齢は%d歳です!\ n"、name、gender、age)
}

func main(){
    inj:= inject.New()
    inj.Map("Yamada")
    inj.MapTo("男"、(*SpecialString)(nil))
    inj2:= inject.New()
    inj2.Map(25)
    inj.SetParent(inj2)
    inj.Invoke(Say)
} 

操作の結果は次のようになります。

私の名前はYamadaです , 性別は男です, 年齢は25歳です!

上記の例で、SpecialString インターフェイスを性別パラメータの型として定義せず、名前と性別の両方を文字列型として定義した場合、性別が名前の値をオーバーライドします。

 func (inj *injector) Apply(val interface{}) error {
    v := reflect.ValueOf(val)

    for v.Kind() == reflect.Ptr {
        v = v.Elem()
    }

    if v.Kind() != reflect.Struct {
        return nil
    }

    t := v.Type()

    for i := 0; i < v.NumField(); i++ {
        f := v.Field(i)
        structField := t.Field(i)
        if f.CanSet() && structField.Tag == "inject" {
            ft := f.Type()
            v := inj.Get(ft)
            if !v.IsValid() {
                return fmt.Errorf("%vの値は見つかりませんでした", ft)
            }
            f.Set(v)
        }
    }
    return nil
} 

apply メソッドは構造体のフィールドを注入するために使用され、パラメーターは構造体の基礎となる型へのポインターです。注入可能であることの前提条件は、フィールドがエクスポートされる必要があり (つまり、フィールド名が大文字で始まること)、このフィールドのタグが`inject`に設定されているということです。

サンプルコードは次のとおりです。

 package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type SpecialString interface{}

type TestStruct struct {
    Name   string `inject`
    Nick   []byte
    Gender SpecialString `inject`
    uid    int           `inject`
    Age    int           `inject`
}

func main() {
    s := TestStruct{}
    inj := inject.New()
    inj.Map("Yamada")
    inj.MapTo("男", (*SpecialString)(nil))
    inj2 := inject.New()
    inj2.Map(26)
    inj.SetParent(inj2)
    inj.Apply(&s)
    fmt.Println("s.Name =", s.Name)
    fmt.Println("s.Gender =", s.Gender)
    fmt.Println("s.Age =", s.Age)
} 

操作の結果は次のようになります。

s.Name = Yamada
s.Gender = 男
s.Age = 26

 

「 Go 言語インジェクション ライブラリ: 依存性インジェクション」についてわかりやすく解説!絶対に観るべきベスト2動画

justforfunc #29: コードレビューでの依存性注入
Go (Golang) での依存性注入