依存性注入への段階的なアプローチ


12

私は、依存性注入を使用して、クラスをユニットテスト可能にすることに取り組んでいます。しかし、これらのクラスの一部には多くのクライアントがあり、依存関係を渡すためにそれらすべてをリファクタリングする準備がまだできていません。だから私は徐々にそれをやろうとしている。現時点ではデフォルトの依存関係を維持しますが、テストのためにそれらをオーバーライドできます。

私が考えているアプローチの1つは、すべての「新しい」呼び出しを独自のメソッドに移動することです。たとえば、

public MyObject createMyObject(args) {
  return new MyObject(args);
}

次に、単体テストでこのクラスをサブクラス化し、作成関数をオーバーライドして、代わりに偽のオブジェクトを作成します。

これは良いアプローチですか?欠点はありますか?

より一般的には、テストのために依存関係を置き換えることができる限り、ハードコードされた依存関係を使用しても大丈夫ですか?私は、コンストラクタで明示的にそれらを要求することが好ましいアプローチであることを知っており、最終的にそこに到達したいと思います。しかし、私はこれが良い最初のステップかどうか疑問に思っています。

私に発生した欠点の1つは、テストする必要がある実際のサブクラスがある場合、親クラス用に作成したテストサブクラスを再利用できないことです。実際のサブクラスごとにテストサブクラスを作成する必要があり、同じ作成関数をオーバーライドする必要があります。


なぜ今ユニットテストができないのか詳しく説明していただけますか?
オースティンサロネン

コード全体に多数の「新しいX」が散在しているほか、静的クラスとシングルトンの多くの用途があります。そのため、1つのクラスをテストしようとすると、実際にはそれらのクラス全体をテストしています。依存関係を分離し、それらを偽のオブジェクトに置き換えることができれば、テストをより細かく制御できます。
JW01

回答:


13

これは、開始するのに適した方法です。ことを注意最も重要なのは、ユニットテストを使用して、既存のコードをカバーすることです。テストが完了したら、自由リファクタリングして設計をさらに改善できます。

したがって、最初のポイントは、デザインをエレガントで光沢のあるものにすることではなく、コードユニットを最小限のリスクで変更してテスト可能にすることです。単体テストを使用しない場合、コードの破損を防ぐために、より慎重で慎重でなければなりません。これらの最初の変更は、場合によってはコードをより不格好にしたりいように見せたりすることもありますが、最初の単体テストを作成できるようになれば、最終的には思い描いている理想的な設計に向けてリファクタリングできるようになります。

このテーマに関する基本的な作業は、レガシーコードを効果的に使用することです。それは、あなたが上で示したトリックと、多くの、さらに多くの、いくつかの言語を使用して説明しています。


「テストが済んだら、より自由にリファクタリングできる」という発言だけです。テストがない場合は、リファクタリングせず、コードを変更するだけです。
オコド

@Slomojoあなたのポイントはわかりますが、テストなしで多くの安全なリファクタリングを行うことができます。特に、たとえばJavaのeclipseでは、重複したコードをメソッドに抽出し、すべての名前を変更し、クラスを移動し、フィールド、定数を抽出するなどの自動IDEレイタリングが可能ですなど
Alb

私は本当に、リファクタリングという用語の起源を引用しているだけです。リファクタリングとは、テストフレームワークによって回帰エラーから保護される改善されたパターンにコードを修正することです。もちろん、IDEベースのリファクタリングにより、ソフトウェアの「リファクタリング」ははるかに簡単になりましたが、ソフトウェアにテストがなく、コードを「リファクタリング」しているだけであれば、自己妄想を避ける方が好きです。もちろん、それは私が他の誰かに言うことではないかもしれません;)
オコド

1
@Alb、テストなしのリファクタリングは常により危険です。自動リファクタリングはそのリスクを確実に減少させますが、ゼロにはなりません。ツールが正しく処理できない可能性があるトリッキーなエッジケースが常にあります。より良い場合、結果はツールからのエラーメッセージですが、最悪の場合、静かに微妙なバグを引き起こす可能性があります。そのため、自動化されたツールを使用しても、できるだけ単純で安全な変更を維持することをお勧めします。
ペテルトレック

3

Guiceの人々は

@Inject
public void setX(.... X x) {
   this.x = x;
}

@Injectを定義に追加するだけでなく、すべてのプロパティに対して。これにより、それらを通常のBeanとして扱うことができます。これはnew、テスト中(およびプロパティを手動で設定する場合)および本番環境で@Inject する場合に使用できます。


3

この種の問題に直面した場合、クラスからテストへの各依存関係を識別し、それらを渡すことができる保護されたコンストラクターを作成します。これは、それぞれのセッターを作成するのと実質的に同じですが、コンストラクターで依存関係を宣言して、将来それらをインスタンス化することを忘れないようにします。

したがって、新しいユニットテスト可能クラスには2つのコンストラクタがあります。

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}

2

注入された依存関係を使用できるようになるまで、「getter create」イディオムを検討することをお勧めします。

例えば、

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

これにより、ゲッターを介してmyObjectを参照できるように内部的にリファクタリングできます。電話する必要はありませんnew。将来的に、すべてのクライアントが依存性注入を許可するように構成された場合、必要なことは作成コードを1か所(ゲッター)で削除することだけです。セッターを使用すると、必要に応じてモックオブジェクトを注入できます。

上記のコードは単なる例であり、内部状態を直接公開します。このアプローチがコードベースに適しているかどうかを慎重に検討する必要があります。

また、主要なリファクタリングを成功させるための便利なヒントが豊富に含まれている「レガシーコードを効果的に使用する」を読むことをお勧めします。


ありがとう。いくつかのケースでこのアプローチを検討しています。しかし、MyObjectのコンストラクターがクラス内からのみ使用可能なパラメーターを必要とする場合はどうしますか?または、クラスの存続期間中に複数のMyObjectを作成する必要がある場合 MyObjectFactoryを注入する必要がありますか?
JW01

@ JW01ゲッタークリエーターコードを設定して、クラスから内部状態を読み取り、それをそのまま適合させることができます。ライフタイム中に複数のインスタンスを作成することは、オーケストレーションするのが難しいでしょう。そのような状況に遭遇した場合、回避策ではなく、再設計することをお勧めします。ゲッターを複雑にしすぎないでください。ゲッターが首の周りの石臼になります。あなたの目的は、リファクタリングプロセスを簡単にすることです。難しすぎる場合は、別の場所に移動して修正します。
ゲイリーロウ

それはシングルトンです。シングルトンを使用しないでくださいO_O
GameDeveloper

@DarioOO実際、それは非常に貧弱なシングルトンです。より良い実装では、Josh Blochに従ってenumを使用します。シングルトンは有効な設計パターンであり、用途があります(高価な1回限りの作成など)。
ゲイリーロウ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.