私にとって、これらのポイントは、コードをより簡単に/より速く記述できるようにするためのものとして見るのをやめたときにはじめて明らかになりました。これはその目的ではありません。彼らは多くの用途があります:
(これの使い方を視覚化することは非常に簡単ではないので、これはピザのアナロジーを失うでしょう)
画面上で簡単なゲームを作っているとしましょう。それはあなたが相互作用する生き物を持っているでしょう。
A:フロントエンドとバックエンドの実装間の疎結合を導入することにより、将来的にコードを維持しやすくすることができます。
トロルしか存在しないので、これを最初に書くことができます:
// This is our back-end implementation of a troll
class Troll
{
void Walk(int distance)
{
//Implementation here
}
}
フロントエンド:
function SpawnCreature()
{
Troll aTroll = new Troll();
aTroll.Walk(1);
}
2週間後、マーケティングはOrcsもTwitterで読むため、Orcも必要だと判断するため、次のようなことを行う必要があります。
class Orc
{
void Walk(int distance)
{
//Implementation (orcs are faster than trolls)
}
}
フロントエンド:
void SpawnCreature(creatureType)
{
switch(creatureType)
{
case Orc:
Orc anOrc = new Orc();
anORc.Walk();
case Troll:
Troll aTroll = new Troll();
aTroll.Walk();
}
}
そして、これがいかに厄介になり始めるかを見ることができます。ここでインターフェースを使用して、フロントエンドを1回記述して(重要なビットはここで)テストし、必要に応じてさらにバックエンドアイテムを接続することができます。
interface ICreature
{
void Walk(int distance)
}
public class Troll : ICreature
public class Orc : ICreature
//etc
フロントエンドは次のとおりです。
void SpawnCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
}
フロントエンドは、インターフェイスICreatureのみを対象としています。トロールやオークの内部実装については気にせず、ICreatureを実装しているという事実についてのみ気にします。
この観点からこれを見るときに注意すべき重要な点は、抽象クリーチャークラスを簡単に使用することもでき、この観点から、これは同じ効果を持つということです。
そして、作成物をファクトリーに抽出することができます:
public class CreatureFactory {
public ICreature GetCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
return creature;
}
}
そして、フロントエンドは次のようになります。
CreatureFactory _factory;
void SpawnCreature(creatureType)
{
ICreature creature = _factory.GetCreature(creatureType);
creature.Walk();
}
フロントエンドはTrollとOrcが実装されているライブラリへの参照さえも持つ必要がなくなりました(ファクトリが別のライブラリにある場合)-それらについてまったく何も知る必要はありません。
B:一部のクリーチャーだけが他の同種のデータ構造に持つ機能を持っているとしましょう。
interface ICanTurnToStone
{
void TurnToStone();
}
public class Troll: ICreature, ICanTurnToStone
フロントエンドは次のようになります。
void SpawnCreatureInSunlight(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
if (creature is ICanTurnToStone)
{
(ICanTurnToStone)creature.TurnToStone();
}
}
C:依存関係注入の使用法
ほとんどの依存関係注入フレームワークは、フロントエンドコードとバックエンドの実装の間の結合が非常に緩い場合に、より簡単に処理できます。上記のファクトリーの例を取り、ファクトリーにインターフェースを実装させる場合:
public interface ICreatureFactory {
ICreature GetCreature(string creatureType);
}
次に、フロントエンドはこれを(通常は)コンストラクターを介して(たとえばMVC APIコントローラーに)注入できます。
public class CreatureController : Controller {
private readonly ICreatureFactory _factory;
public CreatureController(ICreatureFactory factory) {
_factory = factory;
}
public HttpResponseMessage TurnToStone(string creatureType) {
ICreature creature = _factory.GetCreature(creatureType);
creature.TurnToStone();
return Request.CreateResponse(HttpStatusCode.OK);
}
}
DIフレームワーク(NinjectやAutofacなど)を使用すると、実行時にコンストラクターでICreatureFactoryが必要なときにCreatureFactoryのインスタンスが作成されるようにそれらを設定できます。これにより、コードが美しくシンプルになります。
また、コントローラーの単体テストを作成するときに、モック化されたICreatureFactoryを提供でき(たとえば、具体的な実装でDBアクセスが必要な場合、単体テストをそれに依存させたくない場合)、コントローラーのコードを簡単にテストできます。
D:他にも用途があります。たとえば、「レガシー」の理由で構造が適切でない2つのプロジェクトAとBがあり、AはBを参照しています。
次に、すでにAにあるメソッドを呼び出す必要がある機能をBで見つけます。循環参照を取得するため、具体的な実装を使用してそれを行うことはできません。
Aのクラスが実装するインターフェイスをBで宣言できます。Bのメソッドには、具象オブジェクトがAの型であっても、問題なくインターフェースを実装するクラスのインスタンスを渡すことができます。