リフレクション
Reflection
golangチャレンジ:構造体
xを受け取り、内部にあるすべての文字列フィールドに対してfnを呼び出す関数walk(x interface{}, fn func(string))を記述します。難易度:再帰的に。
これを行うには、リフレクション(reflection)を使用する必要があります。
コンピューティングにおけるリフレクションは、プログラムが、特にタイプを通じて、独自の構造を調べる能力です。それは一種のメタプログラミングです。また、混乱の元にもなります。
「インターフェース(interface)」とは何ですか?
interface)」とは何ですか?Goでは、string、intなどの既知の型や、BankAccountなどの独自の型で機能する関数の点で、タイプの安全性を提供してきました。
つまり、自由な値(ドキュメント)を取得し、間違った型を関数に渡そうとするとコンパイラーが文句を言います。
コンパイル時に型がわからない関数を書きたいというシナリオに出くわすかもしれません。
Goでは、これを any 型と考えることができる型interface{}で回避できます。
したがって、walk(x interface{}, fn func(string))は、xの任意の値を受け入れます。
では、すべてに「インターフェース」を使用し、本当に柔軟な機能を持たないのはなぜでしょうか?
「インターフェース
interface」をとる関数のユーザーとして、タイプの安全性を失います。タイプstringのFoo.barを関数に渡すつもりでしたが、代わりにintであるFoo.bazを渡した場合はどうなりますか?コンパイラーは間違いを通知できません。また、関数に渡すことが許可されている what もわかりません。たとえば関数がUserServiceをとることを知ることは非常に便利です。そのような関数の書き方として、渡された anything を検査して、型が何であり、それで何ができるのかを理解する必要があります。これは、リフレクション(reflection)を使用して行われます。これは非常に不格好で読みにくい場合があり、実行時にチェックを行う必要があるため一般的にパフォーマンスが低下します。
つまり、本当に必要な場合にのみ、リフレクションを使用してください。
ポリモーフィック関数が必要な場合は、ユーザーが関数を機能させるために必要なメソッドを実装している場合に、ユーザーが複数の型で関数を使用できるように、(interfaceではなく混乱を防ぐために)の周囲で設計できるかどうか検討してください。
私たちの機能は、さまざまなことを処理できる必要があります。いつものように、サポートしたい新しいものごとにテストを作成し、完了するまでリファクタリングを繰り返すというアプローチをとります。
最初にテストを書く
文字列フィールドが(x)に含まれている構造体で関数を呼び出す必要があります。 次に、渡された関数(fn)をスパイして、呼び出されているかどうかを確認できます。
どの文字列が
walkによってfnに渡されたかを格納する文字列のスライス(got)を格納したいと思います。多くの場合、前の章では、関数/メソッドの呼び出しをスパイするために専用の型を作成しましたが、この場合は、gotを閉じるfnの匿名関数を渡すだけです。最も単純な「ハッピー」パスを取得するために、文字列型の
Nameフィールドを持つ匿名のstructを使用します。最後に、
walkをxとスパイで呼び出し、今のところgotの長さを確認するだけです。非常に基本的な動作が得られたら、アサーションでより具体的になります。
テストを実行してみます
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
walkを定義する必要があります
テストを再試行してください
成功させるのに十分なコードを書く
このパスを作成するために、任意の文字列でスパイを呼び出すことができます。
これでテストに合格するはずです。 次に行う必要があるのは、fnの呼び出し対象をより具体的にアサートすることです。
最初にテストを書く
次のコードを既存のテストに追加して、fnに渡された文字列が正しいことを確認します
テストを実行してみます
成功させるのに十分なコードを書く
このコードは 非常に安全でナイーブ ですが、「赤」(テスト失敗)にあるときの目標は、可能な限り最小限のコードを記述することです。次に、懸念に対処するためのテストをさらに記述します。
リフレクションを使用してxを確認し、そのプロパティを確認する必要があります。
reflect packageには、指定された変数のValueを返す関数ValueOfがあります。これには、次の行で使用するフィールドなど、値を検査する方法があります。
次に、渡された値について非常に楽観的な仮定を行います。
最初で唯一のフィールドを見て、パニックを引き起こすフィールドがまったくない場合があります。
次に、基になる値を文字列として返す
String()を呼び出しますが、フィールドが文字列以外の場合は間違っていることがわかります。
リファクタリング♪
私たちのコードは単純なケースに合格していますが、コードには多くの欠点があることを知っています。
さまざまな値を渡し、fnが呼び出された文字列の配列をチェックするいくつかのテストを作成します。
新しいシナリオのテストを続行しやすくするために、テストをテーブルベースのテストにリファクタリングする必要があります。
これで、シナリオを簡単に追加して、複数の文字列フィールドがある場合にどうなるかを確認できます。
最初にテストを書く
次のシナリオをcasesに追加します。
テストを実行してみます
成功させるのに十分なコードを書く
valには、値のフィールド数を返すメソッドNumFieldがあります。 これにより、フィールドを反復処理し、テストに合格したfnを呼び出すことができます。
リファクタリング♪
ここにコードを改善する明白なリファクターがあるようには見えないので、続けましょう。
walkの次の欠点は、すべてのフィールドがstringであると想定していることです。 このシナリオのテストを書いてみましょう。
最初にテストを書く
次のケースを追加
テストを実行してみます
成功させるのに十分なコードを書く
フィールドのタイプがstringであることを確認する必要があります。
そのKindをチェックすることでそれを行うことができます。
リファクタリング♪
繰り返しになりますが、コードは今のところ十分に妥当です。
次のシナリオは、「フラット」な「構造体」でない場合はどうなるのでしょうか。 言い換えると、いくつかのネストされたフィールドを持つstructがあるとどうなりますか?
最初にテストを書く
私たちは匿名構造体構文を使用して、テストのためにアドホックに型を宣言しているので、そのように続けることができます
しかし、内部の匿名構造体を取得すると、構文が少し乱雑になることがわかります。構文を改善するために作成する提案があります。
このシナリオの既知のタイプを作成してこれをリファクタリングし、テストで参照してみましょう。 私たちのテストのコードの一部がテストの外にあるという点で少し間接的ですが、読者は初期化を見て、structの構造を推測できるはずです。
次の型宣言をテストファイルのどこかに追加します。
これをケースに追加して、以前よりもはるかに明確に読み取ることができます。
テストを実行してみます
問題は、型の階層の最初のレベルのフィールドでのみ反復していることです。
成功させるのに十分なコードを書く
解決策は非常に簡単です。そのKindをもう一度調べ、それが「構造体struct」である場合は、その内部の「構造体struct」でもう一度walkを呼び出すだけです。
リファクタリング♪
同じ値の比較を複数回行う場合、「一般的に」switchにリファクタリングすると、読みやすさが向上し、コードの拡張が容易になります。
渡された構造体の値がポインターの場合はどうなりますか?
最初にテストを書く
このケースを追加
テストを実行してみます
成功させるのに十分なコードを書く
ポインターValueでNumFieldを使用することはできません。Elem()を使用する前に、基になる値を抽出する必要があります。
リファクタリング♪
与えられた interface{}から reflect.Valueを関数に抽出する責任をカプセル化しましょう。
これは実際には more コードを追加しますが、抽象化レベルは適切だと思います。
検査できるように、
xのreflect.Valueを取得します。方法は気にしません。フィールドを反復処理し、そのタイプに応じて必要なことをすべて実行します。
次に、スライスをカバーする必要があります。
最初にテストを書く
テストを実行してみます
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
これは以前のポインターシナリオに似ています。reflect.Valueで NumFieldを呼び出そうとしていますが、構造体ではないため、これはありません。
成功させるのに十分なコードを書く
リファクタリング♪
これは機能しますが、不幸です。心配はいりません。 テストに裏打ちされた実際のコードがあるので、好きなように自由に変更することができます。
少し抽象的に考えると、どちらかでwalkと呼びたい
構造体の各フィールド
スライス内の各 thing
現時点でのコードはこれを実行しますが、十分に反映していません。 最初に、それがスライス(残りのコードの実行を停止するための return付き)であるかどうかを確認し、そうでない場合は構造体であると想定します。
コードを書き直して、代わりにタイプ first を確認してから作業を行います。
格好良く!
構造体またはスライスの場合は、それぞれの値に対してwalkを呼び出してその値を繰り返し処理します。 それ以外の場合、reflect.Stringであれば、fnを呼び出すことができます。
それでも、私にはそれがより良いものになり得るような気がします。フィールド/値を反復してwalkを呼び出すという操作の繰り返しがありますが、概念的には同じです。
valueがreflect.Stringの場合、通常のようにfnを呼び出すだけです。
それ以外の場合、switchはタイプに応じて2つのものを抽出します
いくつのフィールドがありますか
Valueを抽出する方法(FieldまたはIndex)
それらを決定したら、getField関数の結果を使用して、numberOfValuesが walkを呼び出して反復できるようにします。
これで完了です。配列の処理は簡単です。
最初にテストを書く
ケースに追加
テストを実行してみます
成功させるのに十分なコードを書く
配列はスライスと同じように処理できるため、コンマを使用してケースに追加するだけです。
次に処理するタイプは mapです。
最初にテストを書く
テストを実行してみます
成功させるのに十分なコードを書く
ここでも少し抽象的に考えると、mapはstructに非常に似ていることがわかります。これは、コンパイル時にキーが不明であるということだけです。
ただし、設計により、インデックスからマップから値を取得することはできません。これは key によってのみ行われるため、抽象化を壊します。
リファクタリング♪
今の気分はどうですか?
当初は素晴らしい抽象化のように思われたかもしれませんが、コードは少し不安定に感じられます。
これで問題ありません! リファクタリングは道のりであり、間違いを犯すこともあります。 TDDの主要なポイントは、これらのことを試す自由を私たちに与えることです。
テストに裏打ちされた小さなステップを踏むことによって、これは決して不可逆的な状況ではありません。 リファクタリング前の状態に戻しましょう。
walkを導入しました。これは、valからreflect.Valueを抽出するだけでよいように、switch内のwalkへの呼び出しを乾燥させます。
最後の問題
Goのマップは順序を保証するものではないことに注意してください。したがって、fnの呼び出しは特定の順序で行われると断言するため、テストが失敗することがあります。
これを修正するには、マップを含むアサーションを、順序を気にしない新しいテストに移動する必要があります。
assertContainsの定義方法は次のとおりです
次に処理したい型はchanです。
最初にテストを書く
テストを実行してみます
成功させるのに十分なコードを書く
Recv()で閉じられるまで、チャネルを通じて送信されたすべての値を反復処理できます
次に処理するタイプはfuncです。
最初にテストを書く
テストを実行してみます
成功させるのに十分なコードを書く
このシナリオでは、引数のない関数はあまり意味がありません。ただし、任意の戻り値を許可する必要があります。
まとめ
reflectパッケージのいくつかの概念を導入しました。任意のデータ構造をたどるために再帰を使用しました。
振り返ってみると、悪いリファクタリングをしましたが、それについてはあまり動揺はありません。テストを反復的に行うことで、それほど大したことではありません。
これは、リフレクションの小さな側面だけをカバーしています。GOブログでは、詳細を網羅した優れた記事を掲載しています。
リフレクションについて理解したので、使用しないように最善を尽くしてください。
最終更新
役に立ちましたか?