構造体、メソッド、インターフェース
Structs, methods & interfaces
高さと幅を指定して長方形の周囲を計算するために、いくつかのジオメトリコードが必要だとします。 Perimeter(width float64, height float64)
関数を記述できます。 ここで、 float64
は 123.45
のような浮動小数点数用です。
テスト駆動開発(TDD)のサイクルはもうお馴染みのものになっているはずです。
最初にテストを書く
新しいフォーマット文字列に注目してください。 f
は float64
用で、 .2
は小数点以下2桁を出力することを意味します。
テストを実行してみます
./shapes_test.go:6:9: undefined: Perimeter
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
Results in shapes_test.go:10: got 0.00 want 40.00
.
成功させるのに十分なコードを書く
これまでのところ、とても簡単です。長方形の面積を返す Area(width, height float64)
と呼ばれる関数を作成しましょう。
TDDサイクルに従って、自分で試してください。
おそらく、あなたはこのようなテストで終わるはずでしょう
そして、このようなコード
リファクタリング♪
私たちのコードはその役割を果たしますが、四角形について明示的なものは何も含まれていません。不注意な開発者は、三角形の幅と高さを間違った答えを返すことに気付かずにこれらの関数に提供しようとする場合があります。
RectangleArea
のように、より具体的な名前を関数に付けることができます。より適切なソリューションは、この概念をカプセル化するRectangle
と呼ばれる独自の型を定義することです。
structを使用して単純なタイプを作成できます。構造体は、データを保存できるフィールドの名前付きコレクションです。
このような構造体を宣言します
では、プレーンなfloat64
ではなく、Rectangle
を使用するようにテストをリファクタリングしましょう。
修正を試みる前に必ずテストを実行してください。 次のような有用なエラーが表示されるはずです。
myStruct.field
の構文で構造体のフィールドにアクセスできます。
2つの関数を変更してテストを修正します。
Rectangle
を関数に渡すと、意図がより明確に伝わるが、構造体を使用することで得られるメリットが増えることに同意していただければ幸いです。
次の要件は、サークルのArea
関数を記述することです。
最初にテストを書く
ご覧のとおり、 f
は g
に置き換えられています。f
を使用すると、正確な10進数を知るのが難しい場合があります。g
を使用すると、エラーメッセージで完全な10進数が表示されます。(fmt options).
テストを実行してみます
./shapes_test.go:28:13: undefined: Circle
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
Circle
タイプを定義する必要があります。
もう一度テストを実行してみてください
./shapes_test.go:29:14: cannot use circle (type Circle) as type Rectangle in argument to Area
一部のプログラミング言語では、次のようなことができます。
しかし、Goではできません
./shapes.go:20:32: Area redeclared in this block
2つの選択肢があります。
同じ名前の関数を異なる
packages
で宣言することができます。新しいパッケージでArea(Circle)
を作成することはできますが、ここではやりすぎだと感じます。代わりに、新しく定義した型にメソッドを定義できます。
メソッドとは?
これまでは functions のみを記述してきましたが、いくつかのメソッドを使用しています。 t.Errorf
を呼び出すときは、t
(testing.T
)のインスタンスでメソッドErrorf
を呼び出しています。
メソッドは、レシーバーを持つ関数です。 メソッド宣言は、識別子(メソッド名)をメソッドにバインドし、メソッドをレシーバーの基本タイプに関連付けます。
メソッドは関数と非常に似ていますが、特定のタイプのインスタンスで呼び出すことによって呼び出されます。 Area(rectangle)
など、好きな場所で関数を呼び出すことができる場所では、「もの」のメソッドのみを呼び出すことができます。
例が役立つので、まずテストを変更して、代わりにメソッドを呼び出し、次にコードを修正しましょう。
テストを実行しようとすると、
タイプCircleにはフィールドまたはメソッドエリアがありません(type Circle has no field or method Area)
ここでコンパイラがどれほど優れているかを繰り返し説明します。時間をかけてゆっくりと表示されるエラーメッセージを読むことは非常に重要です。それは長期的には役立ちます。
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
タイプにいくつかのメソッドを追加しましょう
メソッドを宣言するための構文は、関数とほとんど同じです。 これは、メソッドが非常に似ているためです。 唯一の違いは、メソッドレシーバー func (receiverName ReceiverType) MethodName(args)
の構文です。
そのタイプの変数でメソッドが呼び出されると、 receiverName
変数を介してそのデータへの参照が取得されます。他の多くのプログラミング言語では、これは暗黙的に行われ、 this
を介してレシーバーにアクセスします。
Goの慣例では、レシーバー変数をタイプの最初の文字にします。
テストを再実行しようとすると、テストがコンパイルされ、失敗した出力がいくつか表示されます。
成功させるのに十分なコードを書く
新しいメソッドを修正して、長方形のテストに成功させましょう
テストを再実行すると、四角形テストはパスするはずですが、円はまだ失敗しているはずです。
サークルの Area
関数を渡すために、math
パッケージから Pi
定数を借ります(インポートすることを忘れないでください\)。
リファクタリング♪
テストに重複があります。
やりたいことは、shapes
のコレクションを取得し、それらの Area()
メソッドを呼び出して、結果を確認することだけです。
Rectangle
と Circle
の両方を渡すことができるある種の checkArea
関数を記述できるようにしたいが、形状ではないものを渡そうとするとコンパイルに失敗します。
Goでは、この意図をインターフェースで体系化できます。
インターフェイスは、Goなどの静的型付き言語で非常に強力な概念です。 これにより、さまざまな型で使用できる関数を作成し、高度に分離されたコードを作成できます。まだタイプセーフを維持しています。
テストをリファクタリングしてこれを紹介しましょう。
他の演習と同様にヘルパー関数を作成していますが、今回はShape
が渡されるように要求しています。これを形状ではないもので呼び出そうとすると、コンパイルされません。
どのようにして何かが形になりますか? Shape
がインターフェース宣言を使用しているものをGoに伝えるだけです
Rectangle
と Circle
で行ったように新しい type
を作成していますが、今回はstruct
ではなく interface
です。
これをコードに追加すると、テストに合格します。
ちょ待って、なぜ?
これは、他のほとんどのプログラミング言語のインターフェースとはかなり異なります。通常、My type Foo implements interface Bar
と言うコードを書く必要があります。
しかし、私たちの場合
Rectangle
にはArea
というメソッドがあり、float64
を返すため、Shape
インターフェースを満たしますCircle
にはArea
というメソッドがあり、float64
を返すため、Shape
インターフェースを満たしますstring
にはそのようなメソッドがないため、インターフェースを満たしていませんなど
Goでは、インターフェースの解決は暗黙的です。 渡したタイプがインターフェースが要求するものと一致する場合、それはコンパイルされます。
切り離し(Decoupling)
ヘルパーが形状が Rectangle
、Circle
、または Triangle
のどちらであるかを気にする必要がないことに注意してください。 インターフェースを宣言することにより、ヘルパーは具象型から切り離(Decoupling)され、その機能を実行するために必要なメソッドのみを持ちます。
インターフェイスを使用して必要なもののみを宣言するこの種のアプローチは、ソフトウェア設計において非常に重要であり、後のセクションでより詳細に説明します。
さらにリファクタリング
構造体についてある程度理解できたので、「テーブル駆動テスト」を紹介します。
テーブル駆動テストは、同じ方法でテストできるテストケースのリストを作成する場合に役立ちます。
ここでの唯一の新しい構文は、「匿名の構造体」areaTests
を作成することです。 2つのフィールド、 shape
と want
で []struct
を使用して、構造体のスライスを宣言しています。次に、スライスをケースで埋めます。
次に、構造体フィールドを使用してテストを実行し、他のスライスと同じようにそれらを繰り返します。
開発者が新しい形状を導入し、 Area
を実装してテストケースに追加するのが非常に簡単であることを確認できます。 さらに、Area
でバグが見つかった場合、修正する前に新しいテストケースを追加して実行するのは非常に簡単です。
テーブルベースのテストは、ツールボックスの優れた項目になる可能性がありますが、テストで余分なノイズが必要であることを確認してください。 インターフェースのさまざまな実装をテストしたい場合、または関数に渡されるデータに、テストを必要とするさまざまな要件がたくさんある場合、それらは非常に適しています。
別の形状を追加してテストすることで、これらすべてを実証してみましょう。三角形含めて。
最初にテストを書く
新しい形状の新しいテストを追加するのはとても簡単です。リストに{Triangle{12, 6}, 36.0},
を追加するだけです。
テストを実行してみます
忘れずに、テストを実行し続けて、コンパイラーに解決策を導きましょう。
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
./shapes_test.go:25:4: undefined: Triangle
三角形はまだ定義していません
再試行
Triangleは Area()
メソッドがないため、形状として使用できないので、テストを機能させるために空の実装を追加します
最後にコードがコンパイルされ、エラーが発生します
shapes_test.go:31: got 0.00 want 36.00
成功させるのに十分なコードを書く
そして、テストは成功です!
リファクタリング♪
繰り返しになりますが、実装は問題ありませんが、テストでは多少の改善が見込めます。
これを見直すと
すべての数値が何を表しているのかすぐには明確ではなく、テストを簡単に理解できるようにする必要があります。
ここまでは、 MyStruct{val1、val2}
構造体のインスタンスを作成するための構文だけを示してきましたが、オプションでフィールドに名前を付けることができます。
それがどのように見えるか見てみましょう
例によるテスト駆動開発 で、Mr. Kent Beckは、いくつかのテストをある程度までリファクタリングして評価します。
テストは、それが真実の主張であるかのように、より明確に私たちに話しかけます。一連の操作ではありません
今度は、少なくともケースのリストのテストで、形状とその領域について真実を主張します。
テスト出力が役立つことを確認する
以前にTriangle
を実装していて、失敗したテストがあったことを覚えていますか?
shapes_test.go:31: got 0.00 want 36.00
と表示されました。
これがTriangle
に関連していることはわかっていましたが、それを扱っているだけでしたが、表の20のケースのいずれかでバグがシステムに侵入した場合はどうなりますか? 開発者はどのケースが失敗したかをどのようにして知るのでしょうか? これは開発者にとって素晴らしい経験ではありません。
実際に失敗したケースを見つけるために、ケースを手動で調べる必要があります。
エラーメッセージを %#v got %.2f want %.2f
に変更できます。 %#v
形式の文字列は、フィールドの値を含む構造体を出力するため、開発者はテストされているプロパティを一目で確認できます。
テストケースを読みやすくするために、 want
フィールドの名前をhasArea
のようなわかりやすい名前に変更できます。
テーブル駆動テストの最後のヒントは、 t.Run
を使用してテストケースに名前を付けることです。
各ケースを t.Run
でラップすることで、ケースの名前が出力されるため、失敗時のテスト出力がより明確になります。
また、 go test -run TestArea/Rectangle
を使用して、テーブル内で特定のテストを実行できます。
これを捉えた最終テストコードは次のとおりです
まとめ
これはより基本的な数学の問題の解決策を繰り返し、テストによって動機付けされた新しい言語機能を学習する、よりTDDの実践でした。
構造体を宣言して独自のデータ型を作成し、関連するデータをまとめてコードの意図を明確にする
さまざまなタイプで使用できる関数を定義できるようにインターフェイスを宣言する (parametric polymorphism)
データ型に機能を追加したり、インターフェースを実装したりできるようにメソッドを追加する
アサーションをより明確にし、スイートを拡張および保守しやすくするためのテーブルベースのテスト
これは重要な章でした。私たちは今、独自の型を定義し始めているからです。 Goのような静的に型付けされた言語では、理解しやすく、つなぎ合わせてテストできるソフトウェアを構築するために、独自の型を設計できることが不可欠です。
インターフェイスは、システムの他の部分から複雑さを隠すための優れたツールです。私たちの場合、テストヘルパーは、それがアサートしている正確な形状を知る必要はなく、その領域を尋ねる
方法を知るだけでした。
Goに慣れるにつれて、インターフェースと標準ライブラリの本当の強みを理解し始めることができます。 everywhere
で使用される標準ライブラリで定義されたインターフェイスについて学び、独自のタイプに対してそれらを実装することにより、多くの優れた機能を非常に迅速に再利用できます。
最終更新