純粋な関数に暗黙的に依存しているのは悪いことですか(特にテストの場合)?


8

タイトルを少し拡張するために、他の関数またはクラスが依存する純粋な関数を明示的に宣言する(つまり注入する)必要があるかどうかについて、結論を出そうとしています。

それらが要求せずに純粋な関数を使用する場合、テスト可能性が低く、設計が悪い特定のコードはありますか?私は単純なものから純粋な機能の任意の種類のために、問題の結論に取得したいと思い、ネイティブ関数(例えばmax()min()-言語に関係なく)今度は他の純粋な機能に暗黙的に依存してもよいことを習慣に、より複雑なもの。

通常の懸念は、一部のコードが依存関係を直接使用するだけの場合、それを単独でテストすることができない、つまり、あなたが黙って持ってきたものすべてを同時にテストすることです。しかし、これは、小さな関数ごとに実行する必要がある場合、かなりのボイラープレートを追加するので、これがまだ純粋な関数に当てはまるかどうか、なぜか、またはなぜそうでないのかと思います。


2
私はこの質問を理解していません。関数がDI目的で純粋である場合、何が問題になりますか?
Telastyn 2017年

したがって、サイズ、推移的な依存関係などに関係なく、純粋な関数、純粋でない関数、およびクラスをすべて同じように注入することを提案します。それについて詳しく説明してもらえますか?
DanielM 2017年

いいえ、私はあなたが何を求めているのか理解していません。関数が何かに注入されることはめったになく、そうである場合、消費者はそれらの純度を保証できません。
Telastyn 2017年

1
すべての機能を注入できますが、多くの場合、テストでこれらの機能を模擬しても意味がありません。つまり、これらの機能を注入しても意味がありません。たとえば、演算子は基本的に静的関数であるため、+演算子を挿入してテストで模擬することができます。それが価値がない限り、あなたはそれをしないでしょう。
user2180613 2017年

1
オペレーターも私の心を超えていました。それは確かに不便でしょうが、私が見つけようとしていた点は、何を注入し、何を注入しないかをどのように決定するかです。つまり、価値があるときとないときの判断方法。@Goyoの回答に触発されて、私は今、良い基準はシステムの本質的な部分ではないすべてのものを注入し、システムとその依存関係を疎結合に保つことだと思います。対照的に、システムが非常にまとまりのあるシステムのアイデンティティの一部であるものを使用する(したがって、注入しない)。
DanielM

回答:


10

いいえ、悪くありません

作成するテストは、特定のクラスまたは関数がどのように実装されるかを気にする必要はありません。むしろ、それらがどのように正確に実装されているかに関係なく、期待どおりの結果を確実に生成する必要があります。

例として、次のクラスを考えます。

Coord2d{
    float x, y;

    ///Will round to the nearest whole number
    Coord2d Round();
}

「ラウンド」関数をテストして、関数が実際にどのように実装されているかに関係なく、期待どおりの結果が返されることを確認します。おそらく、次のようなテストを作成します。

Coord2d testValue(0.6, 0.1)
testValueRounded = testValue.Round()
CHECK( testValueRounded.x == 1.0 )
CHECK( testValueRounded.y == 0.0 )

テストの観点からは、Coord2d :: Round()関数が期待どおりの結果を返す限り、その実装方法は気にしません。

場合によっては、純粋な関数を注入することは、クラスをよりテスト可能または拡張可能にするための本当に素晴らしい方法である可能性があります。

ただし、上記のCoord2d :: Round()関数などのほとんどの場合、min / max / floor / ceil / trunc関数を挿入する必要はありません。この関数は、値を最も近い整数に丸めるように設計されています。作成するテストでは、関数がこれを実行することを確認する必要があります。

最後に、クラス/関数の実装が依存するコードをテストする場合は、依存関係のテストを記述するだけでテストできます。

たとえば、Coord2d :: Round()関数が次のように実装された場合...

Coord2d Round(){
    return Coord2d( floor(x + 0.5f),  floor(y + 0.5f))
}

「floor」機能をテストする場合は、別の単体テストで行うことができます。

CHECK( floor (1.436543) == 1.0)
CHECK( floor (134.936) == 134.0)

あなたが言ったすべては、純粋な関数を参照しますよね?もしそうなら、それは純粋でないものやクラスとどのような違いがありますか?つまり、すべてをハードコーディングして同じロジックを適用することができます(「依存しているものはすべてテスト済み」)。そして別の質問:純粋な関数をインジェクトすることがいつ素晴らしいのか、そしてその理由を詳しく説明してください。ありがとうございました!
DanielM 2017年

3
@DanielM絶対に-単体テストは意味のある動作の分離されたユニットをカバーする必要があります-「ユニット」の動作の妥当性が何かの最大値を返すことに厳密に依存している場合、「最大値」を抽象化することは役に立たず、実際に有用性が低下しますテストも。一方、一部のプロバイダーから来るものの「最大」を返す場合、プロバイダーのスタブは便利です。プロバイダーの動作は、このユニットの意図された動作に直接関係しておらず、その正確性を確認できるためです。他の場所。覚えておいてください-"ユニット"!= "クラス"。
Ant P

2
しかし、結局のところ、この種のことを考えすぎることは、通常、逆効果です。これは、テストファーストが独自のものになる傾向があるところです。テストする分離された動作を定義してから、テストを記述し、何を注入するか、何を注入しないかを決定します。
Ant P

4

され、暗黙的に純粋な機能の悪いに応じて、

テストの観点から-いいえ、ただし純粋な関数の場合のみ、関数が同じ入力に対して常に同じ出力を返す場合。

明示的に注入された関数を使用するユニットをどのようにテストしますか?
mocked(fake)関数を挿入し、その関数が正しい引数で呼び出されたことを確認します。

しかし、純粋な関数があるため、常に同じ引数に対して同じ結果を返します。入力引数のチェックは、出力結果のチェックと同じです。

純粋な関数を使用すると、追加の依存関係/状態を構成する必要はありません。

追加の利点として、より良いカプセル化が得られ、ユニットの実際の動作をテストします。
そして、テストの観点から非常に重要です-テストを変更せずにユニットの内部コードを自由にリファクタリングできます-たとえば、純粋な関数の引数をもう1つ追加することを決定します-明示的に注入された関数(テストでモック化)を使用する必要がありますテスト構成を変更します。暗黙的に使用される関数では何もしません。

純粋な関数を注入する必要がある状況を想像できます-消費者にユニットの動作を変更するように提案したい場合です。

public decimal CalculateTotalPrice(Order order, Func<Customer, decimal> calculateDiscount)
{
    // Do some calculation based on order data
    var totalPrice = order.Lines.Sum(line => line.Price);

    // Then
    var discountAmount = calculateDiscount(order.Customer);

    return totalPrice - discountAmount;
}

上記のメソッドの場合、消費者が割引計算を変更できることを期待しているため、CalcualteTotalPriceメソッドの単体テストからロジックのテストを除外する必要があります。


関数を注入しても、関数呼び出しをテストする必要があるわけではありません。実際には、モックに渡されたときにSUTが(監視可能な状態に関して)正しいことを行うと断言します。SUTインターフェースの一部ではない依存関係を動いているOTOHは、私の目には奇妙です。
モニカへの危害を停止する

@Goyo、特定の入力に対してのみ期待値を返すようにモックを構成します。したがって、モックは正しい入力引数が提供されたことを「保護」します。
Fabio

4

SOLID OOプログラミングの理想的な世界では、すべての外部動作を注入します。実際には、常にいくつかの単純な(またはそれほど単純ではない)関数を直接使用します。

一連の数値の最大値を計算している場合、max関数を注入して単体テストでそれを模擬することは多分やり過ぎでしょう。通常、特定の実装からコードを分離することを気にせずmax、分離してテストします。

グラフで最小コストのパスを見つけるような複雑なことをしている場合は、具体的な実装を注入して柔軟性を高め、ユニットテストで模擬してパフォーマンスを向上させることをお勧めします。この場合、パス検索アルゴリズムを使用するコードからパス検索アルゴリズムを分離する作業に値するかもしれません。

「純粋な関数の種類」に対する答えはあり得ません。どこに線を引くかを決定する必要があります。この決定には多くの要因が関係しています。結局、デカップリングがあなたに与える問題に対する利益を重み付けしなければなりません、そしてそれはあなたとあなたの文脈に依存します。

コメントを見ると、クラスの注入(実際にはオブジェクトの注入)と関数の違いについて尋ねられます。違いはありません。言語機能だけが異なって見えます。抽象的観点から見ると、関数を呼び出すことは、いくつかのオブジェクトのメソッドを呼び出すことと同じであり、オブジェクトを注入する(またはしない)のと同じ理由で関数を注入(またはしない)します。その動作を呼び出しコードから切り離して、動作のさまざまな実装を使用し、使用する実装を別の場所で決定する機能。

したがって、基本的に、依存性注入に有効であると判断した基準はすべて、依存関係がオブジェクトまたは関数であるかに関係なく使用できます。言語によってはニュアンスがあるかもしれません(つまり、Javaを使用している場合は問題ありません)。


3
注入はオブジェクト指向プログラミングとは何の関係もありません。
フランクヒルマン2017年

@FrankHilemanベター?抽象化はインスタンス化できないため、抽象化に依存するには依存性注入が必要です。
モニカへの危害を停止する

実践に関わる最も重要な要素の大まかなリストを用意しておくことは間違いなく有益です。これに関連して、max()(他のものとは対照的に)との結合を気にしないのはなぜですか?
DanielM

SOLIDは「理想的」ではなく、オブジェクト指向プログラミングとは何の関係もありません。
フランクヒルマン、2017年

@FrankHileman SOLIDはOOPとは何の関係もありませんか?5つの原則は、オブジェクト指向クラス設計の原則と呼ばれる章で、2000年の論文でRobert Martinによって提案されましたか?
NickL 2017年

3

純粋な関数は、その性質上、クラスのテスト容易性には影響しません。

  • それらの出力(またはエラー/例外)は入力にのみ依存します。
  • それらの出力は世界の状態に依存しません。
  • 彼らの操作は世界の状態を変更しません。

これは、それらがクラスの制御下にあるプライベートメソッドとほぼ同じレルムにあり、インスタンスの現在の状態にも依存しないという追加のボーナス(つまり「this」)があることを意味します。ユーザークラスは入力を完全に制御するため、出力を「制御」します。

あなたが与えた例、max()とmin()は決定論的です。ただし、random()およびcurrentTime()はプロシージャであり、純粋な関数ではありません(たとえば、帯域外の世界の状態に依存/変更する)。これらの最後の2つは、テストを可能にするために注入する必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.