8

非常に大規模で非常にレガシーなプロジェクトをテスト可能にしようとしています。

ほとんどのコードで使用する、静的に利用可能なサービスが多数あります。問題は、これらをモックするのが難しいことです。以前はシングルトンでした。現在、それらは疑似シングルトンです。同じ静的インターフェイスですが、関数は切り替え可能なインスタンス オブジェクトに委任されます。このような:

class ServiceEveryoneNeeds
{
    public static IImplementation _implementation = new RealImplementation();

    public IEnumerable<FooBar> GetAllTheThings() { return _implementation.GetAllTheThings(); }
}

今私の単体テストで:

void MyTest()
{
    ServiceEveryoneNeeds._implementation = new MockImplementation();
}

ここまでは順調ですね。製品では、1 つの実装のみが必要です。ただし、テストは並行して実行され、別のモックが必要になる可能性があるため、次のようにしました。

class Dependencies
{
     //set this in prod to the real impl
     public static IImplementation _realImplementation;

     //unit tests set these
     [ThreadStatic]
     public static IImplementation _mock;

     public static IImplementation TheImplementation
     { get {return _realImplementation ?? _mock; } }

     public static void Cleanup() { _mock = null; }
}

その後:

class ServiceEveryoneNeeds
{
     static IImplementation GetImpl() { return Dependencies.TheImplementation; }

     public static IEnumerable<FooBar> GetAllTheThings() {return GetImpl().GetAllTheThings(); }

}

//and
void MyTest()
{
    Dependencies._mock = new BestMockEver();
    //test
    Dependencies.Cleanup();
}

これらのサービスを必要とするすべてのクラスにコンストラクターが注入するのは大規模なプロジェクトであるため、この方法を採用しました。同時に、これらはほとんどの機能が依存するコードベース内のユニバーサル サービスです。

このパターンは、依存関係を明示的にするコンストラクター注入とは対照的に、依存関係を隠すという意味で悪いことを理解しています。

ただし、利点は次のとおりです。
- 3 か月のリファクタリングを行ってから単体テストを行うのに対して、すぐに単体テストを開始できます。
- グローバルはまだありますが、以前よりも確実に改善されているようです。

私たちの依存関係はまだ暗黙的ですが、このアプローチは私たちが持っていたものよりも厳密に優れていると主張します. 隠れた依存関係は別として、これは適切な DI コンテナーを使用するよりも悪いことですか? どのような問題が発生しますか?

4

4 に答える 4

4

そのサービスロケーターは悪いです。しかし、あなたはすでにそれを知っています。コードベースが非常に大きい場合は、部分的な移行を開始してみませんか?シングルトンインスタンスをコンテナに登録し、コード内のクラスに触れるたびにコンストラクターがそれらを注入し始めます。次に、ほとんどの部品を(うまくいけば)動作状態のままにして、他のすべての場所でDIの利点を得ることができます。

理想的には、DIのない部品は時間の経過とともに収縮するはずです。そして、すぐにテストを開始できます。

于 2012-05-24T18:48:27.790 に答える
4

これはアンビエント コンテキストと呼ばれます。アンビエント コンテキストを正しく使用して実装すれば、問題はありません。アンビエント コンテキストを使用できる場合、いくつかの前提条件があります。

  1. なんらかの値を返す分野横断的な問題でなければなりません
  2. ローカルのデフォルトが必要です
  3. null割り当てられないことを確認する必要があります。(代わりにNull 実装を使用してください)

ロギングなどの値を返さない分野横断的な問題については、インターセプトを優先する必要があります。横断的な懸念事項ではない他の依存関係については、コンストラクター注入を行う必要があります。

ただし、実装にはいくつかの問題があります (null の割り当て、命名、デフォルトなし)。これを実装する方法は次のとおりです。

public class SomeCrossCuttingConcern
{
     private static ISomeCrossCuttingConcern default = new DefaultSomeCrossCuttingConcern();

     [ThreadStatic]
     private static ISomeCrossCuttingConcern current;

     public static ISomeCrossCuttingConcern Default
     { 
         get { return default; }
         set 
         { 
             if (value == null) 
                 throw new ArgumentNullException(); 
             default = value; 
         } 
     }

     public static ISomeCrossCuttingConcern Current
     { 
         get 
         { 
             if (current == null)
                 current = default; 
             return current; 
         }

         set 
         { 
             if (value == null) 
                 throw new ArgumentNullException(); 
             current = value; 
         } 
     }

     public static void ResetToDefault() { current = null; }
}

アンビエント コンテキストには、分野横断的な懸念のために API を汚染しないという利点があります。

しかし一方で、テストに関しては、テストが依存する可能性があります。たとえば、あるテスト用にモックをセットアップするのを忘れた場合、そのモックが以前に別のテストでセットアップされていれば、正しく実行されます。ただし、スタンドアロンまたは別の順序で実行すると失敗します。それはテストをより困難にします。

于 2012-05-24T23:54:16.343 に答える
1

あなたのしていることは悪くないと思います。コードベースをテスト可能にしようとしていますが、秘訣はそれを小さなステップで行うことです。Working Effectively With Legacy Codeを読むと、これと同じアドバイスが得られます。ただし、実行していることの欠点は、依存性注入の使用を開始すると、コードベースを再度リファクタリングする必要があることです。しかし、もっと重要なことは、多くのテスト コードを変更する必要があることです。

私はアレックスに同意します。アンビエント コンテキストを使用する代わりに、コンストラクター インジェクションを使用することをお勧めします。このためにコード ベース全体を直接リファクタリングする必要はありませんが、コンストラクター インジェクションはコール スタックを「バブル」させます。コードベース全体に多くの変更。

私は現在、レガシーコードベースに取り組んでおり、DI コンテナーを使用できません (苦痛)。それでも、可能な場合はコンストラクター注入を使用します。これは、一部の型で貧弱な依存性注入を使用する必要があることを意味します。これは、「コンストラクター注入バブル」を止めるために私が使用するトリックです。それでも、これはアンビエント コンテキストを使用するよりもはるかに優れています。貧乏人の DI は最適ではありませんが、それでも適切な単体テストを作成することができ、後でデフォルトのコンストラクターを分解するのがはるかに簡単になります。

于 2012-05-25T07:41:32.510 に答える
1

依存性注入と DI コンテナーの使用は、実際には別の作業ですが、一方が他方に自然につながります。DI コンテナーを使用するということは、コードに特定の構造があることを意味します。このような構造はおそらく読みやすく、隠れた依存関係についての深い知識がなくても作業しやすいため、より保守しやすくなります。

具象に依存しなくなったので、制御の反転の形式を実装しました。これはより良い設計であり、コードをよりテストしやすくするための良い出発点になると思います。このステップからすぐに価値が得られたようです。

暗黙的な依存関係よりも明示的な依存関係 (つまり、DI とアンビエント コンテキスト) を持つほうがよいでしょうか? はいと言いたいところですが、実際にはコストとメリットに依存します。メリットは、バグを導入するコスト、コードで発生する可能性のあるチャーンの量、デバッグの難しさ、保守担当者、期待寿命などによって異なります。

グローバル可変静的状態は常に悪いです。賢明な魂の中には、呼び出し中にグローバル サービスの実装を交換し、後で交換する必要があると判断する人もいるでしょう。後でクリーンアップしないと、これはひどくうまくいかない可能性があります。これはばかげた例かもしれませんが、このような意図しない副作用は常に悪いので、設計上完全に排除することをお勧めします。規律と用心深さでそれらを防ぐことはできますが、それはより困難です。

于 2012-05-24T18:45:25.190 に答える