実際の例として、シミュレーションでロボットグリッパーを表すGripper
クラスを想像してください。 Gripper
にはTryGrip
メソッドがあり、正しい位置(グリッパーの長方形内)にGrippableItem
があるかどうかをチェックします。存在する場合、グリッパーはドロップされるまでアイテムを制御します。
class GripperItemLocator //Struggling to find a good name
{
public:
virtual std::unique_ptr<GrippableItem>
FindItemInRange(Rect rect) = 0;
};
class Gripper
{
public:
bool
TryGrip(GripperItemLocator& itemLocator)
{
auto item = itemLocator.FindItemInRange(CalcCurrentRect());
if (item)
{
TakeControlOfItem(std::move(item));
}
}
private:
Rect
CalcCurrentRect() const;
void
TakeControlOfItem(std::unique_ptr<GrippableItem> item);
};
依存関係の逆転の原則に従って、グリッパーを他の世界から切り離すためにGripperItemLocator
抽象化があります。 GrippableItem
がプログラムのどこにどのように配置されているかが変わっても、これはグリッパーには関係ありません。GripperItemLocator
を実装するクラスのみを変更する必要があります。
これにインターフェースクラスが本当に必要かどうか疑問に思っていますか?通常のクラスで同じ仕事をしているとしましょう:
class GripperItemLocator
{
public:
std::unique_ptr<GrippableItem>
FindItemInRange(Rect rect)
{
//Immediate implementation
}
};
これには実行時に別の実装を選択する機能がないことを理解していますが、これが必要になることはないと100%確信していると考えます。デカップリングのみに関心があります。私が見落としている他のものを失うことはありますか、またはこれは同じくらい良いですか?
DIPはそれ自体が目的ではなく、目的を達成するための手段です。
たとえば、単体テストにDIPを使用するとします。 「本当の」Gripper
なしでGripperItemLocator
を分離してテストできるようにするには、後者をモックで置き換える方法が必要です。これを行う標準的な方法は、インターフェイスを使用するか、C++では抽象仮想基本クラスを使用することです。
ただし、常に実際のGripper
オブジェクトを使用してGripperItemLocator
のテストを作成するだけで十分だと言う場合は、そのアプローチに固執し、インターフェイス/仮想基本クラスを提供しないでください。
このトレードオフを行う方法は、クラスの複雑さ、クラスを個別にテストできない場合にデバッグするのがどれほど単純または困難であるか、およびエラーが発生した場合にエラーを修正するのに必要な時間に大きく依存しますが、 2つのクラスのどちらに根本的な原因が隠されているかを知る。
依存関係の反転の「反転」部分には、インターフェース、またはインターフェースのような構造が必要です。
つまり、依存関係を持つクラスは、その依存関係が何であるかを指定しなくなります。代わりに、呼び出しコードが依存関係を伝えます。
インターフェースは、あなたが私を通過するものは何でも、私はこれをこのように呼んでみようと言っています。
クラスまたは抽象クラスを使用して定義した場合、そのクラスの実装を空にする必要があります。効果的にインターフェース。
依存関係の逆転の原則 は、インターフェースを使用して高レベルを低レベルのクラスから分離することを目的としています。高レベルのクラスはインターフェースを使用し、低レベルのクラスはインターフェースを実装します。
インターフェイスに相当する通常のC++は、純粋な仮想メンバー関数のみを持つ抽象クラスです。ただし、他の方法もあります。
実際、高レベルのクラスがその仮想メンバー関数のみを呼び出す場合は、依存関係の逆転に任意の多態性クラスを使用できます。例:
_class GripperItemLocator
{
public:
std::unique_ptr<GrippableItem>
virtual FindItemInRange(Rect rect) // <-- it should be virtual
{
//Immediate implementation
}
};
_
後で、仮想関数をオーバーライドする派生クラスを定義できます。オーバーライドとは、コンストラクターが派生クラスに提供されている場合、Gripper
でもオーバーライドされた関数を使用することを意味します。
_class GripperFragileItemLocator : public GripperItemLocator
{
public:
std::unique_ptr<GrippableItem>
FindItemInRange(Rect rect) override
{
//Immediate implementation that takes extra care of fragile items
}
};
_
これは依然、依存関係の逆転です。単一の実装に依存するのではなく、さらに特化できる抽象的なものに依存します。
ただし、注意が必要です。 FindItemInRange()
が仮想ではない場合、独自の実装で派生Gripper
を提供しても、GripperItemLocator
は常にこの関数を呼び出します。したがって、依存関係の逆転のように見えますが、そうではありません。実際、Gripper
は、より低いレベルの具象クラスに完全に依存します。 itemLocater
は、他のものと同じように(何も反転せずに)単に引数になります。
したがって、インターフェースを使用することが最も安全な方法です。誤って非仮想メンバーを使用するリスク、十分に抽象化されていない抽象化を(誤って)使用するリスクはありません。境界は非常に明確です。
テンプレートについて一言
R.C.マーティンが1996年5月に依存関係の逆転に関する重要な記事を書いたとき、C++テンプレートは存在しませんでした。
テンプレートは、低レベルに依存しない高レベルの要素を設計する他の方法を提供します。テンプレートは、いくつかの低レベルの抽象化を使用し、提供する必要があるインターフェースについての仮定を行います。このアプローチを広範囲に使用するスタイルは、 ポリシー主導の設計 です。
ただし、同様の目標を達成しますが、DIPとは大きく異なります。テンプレートは、実際には、下位レベルのクラスが実装する必要のあるインターフェースを定義します。したがって、上位レベルが抽象化に依存している場合でも、下位レベルは抽象化自体ではなく、上位レベルに依存しています。
インターフェイスの代わりに基本クラスを使用して、基本クラスまたはサブクラスのインスタンスを渡すことができます。テストにパターンを使用する場合、置換しようとするスタブクラスは、ベースクラスから派生したものではない可能性が高く、完全に独立しているため、損失します。
固定クラスの固定インスタンスを使用できますが、依存関係の注入は無意味になります。できないと言っているわけではありませんが、なぜでしょうか。
もちろん、ベースクラスから始めて、インターフェースを使用する必要があるとわかった場合、それは些細な変更です。あなたが持っているべきではないクラスについての知識を使用していない限り。