あなたがそれらの問題を抱えているとき、彼らは解決が得意な問題を解決するためにテクニックを使うべきです。依存関係の反転と注入も同じです。
依存関係の反転またはインジェクションは、実行時に呼び出されるメソッドの実装をコードが決定できるようにする手法です。これにより、遅延バインディングの利点が最大化されます。この手法は、言語が非インスタンス関数のランタイム置換をサポートしていない場合に必要です。たとえば、Javaには、静的メソッドの呼び出しを別の実装の呼び出しに置き換えるメカニズムがありません。Pythonとは対照的に、関数呼び出しを置き換えるために必要なのは、名前を別の関数にバインドすることだけです(関数を保持する変数を再割り当てします)。
関数の実装を変更したいのはなぜですか?2つの主な理由があります。
- テスト目的で偽物を使用したい。これにより、実際にデータベースに接続することなく、データベースフェッチに依存するクラスをテストできます。
- 複数の実装をサポートする必要があります。たとえば、MySQLデータベースとPostgreSQLデータベースの両方をサポートするシステムをセットアップする必要がある場合があります。
コントロールコンテナの反転に注意することもできます。これは、この擬似コードのように見える巨大で絡み合った構築ツリーを回避するためのテクニックです。
thing5 = new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());
myApp = new MyApp(
new MyAppDependency1(thing5, thing3),
new MyAppDependency2(
new Thing1(),
new Thing2(new Thing3(thing5, new Thing4(thing5)))
),
...
new MyAppDependency15(thing5)
);
クラスを登録してから、構築を行います:
injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);
myApp = injector.create(MyApp); // The injector fills in all the construction parameters.
登録されたクラスがステートレスシングルトンになる可能性がある場合は、最も単純であることに注意してください。
注意事項
依存関係の反転は、デカップリングロジックの頼りになる答えではないことに注意してください。代わりにパラメーター化を使用する機会を探してください。たとえば、次の擬似コードメソッドを考えてみましょう。
myAverageAboveMin()
{
dbConn = new DbConnection("my connection string");
dbQuery = dbConn.makeQuery();
dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
dbQuery.setParam("min", 5);
dbQuery.Execute();
myData = dbQuery.getAll();
count = 0;
total = 0;
foreach (row in myData)
{
count++;
total += row.x;
}
return total / count;
}
このメソッドのいくつかの部分に依存関係の反転を使用できます。
class MyQuerier
{
private _dbConn;
MyQueries(dbConn) { this._dbConn = dbConn; }
fetchAboveMin(min)
{
dbQuery = this._dbConn.makeQuery();
dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
dbQuery.setParam("min", min);
dbQuery.Execute();
return dbQuery.getAll();
}
}
class Averager
{
private _querier;
Averager(querier) { this._querier = querier; }
myAverageAboveMin(min)
{
myData = this._querier.fetchAboveMin(min);
count = 0;
total = 0;
foreach (row in myData)
{
count++;
total += row.x;
}
return total / count;
}
しかし、少なくとも完全ではありません。でステートフルクラスを作成したことに注意してくださいQuerier
。現在、本質的にグローバルな接続オブジェクトへの参照を保持しています。これは、プログラムの全体的な状態を理解するのが難しい、さまざまなクラスが互いにどのように連携するのかなどの問題を引き起こします。平均化ロジックをテストする場合、クエリアまたは接続を偽造することを余儀なくされていることにも注意してください。さらにより良いアプローチは、パラメータ化を増やすことです:
class MyQuerier
{
fetchAboveMin(dbConn, min)
{
dbQuery = dbConn.makeQuery();
dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
dbQuery.setParam("min", min);
dbQuery.Execute();
return dbQuery.getAll();
}
}
class Averager
{
averageData(myData)
{
count = 0;
total = 0;
foreach (row in myData)
{
count++;
total += row.x;
}
return total / count;
}
class StuffDoer
{
private _querier;
private _averager;
StuffDoer(querier, averager)
{
this._querier = querier;
this._averager = averager;
}
myAverageAboveMin(dbConn, min)
{
myData = this._querier.fetchAboveMin(dbConn, min);
return this._averager.averageData(myData);
}
}
接続は、操作全体を担当し、この出力をどう処理するかを知っている、さらに高いレベルで管理されます。
これで、クエリから完全に独立して平均化ロジックをテストできるようになり、さらに多くの状況でそれを使用できるようになりました。オブジェクトMyQuerier
とAverager
オブジェクトが必要かどうか疑問に思うかもしれませんが、おそらく答えは、ユニットテストを行うつもりがない場合は、データベースに緊密に結合されているため、StuffDoer
ユニットテストStuffDoer
は完全に合理的ではないということでしょう。統合テストでカバーするだけの方が理にかなっているかもしれません。その場合には、我々は細かい作りかもしれないfetchAboveMin
し、averageData
静的メソッドに。