から アジャイルソフトウェア開発、原則、パターン、および実践:ピアソンの新しい国際版 :
場合によっては、クライアントの異なるグループによって呼び出されたメソッドが重複することがあります。オーバーラップが小さい場合、グループのインターフェースは分離したままにする必要があります。共通の関数は、重複するすべてのインターフェースで宣言する必要があります。サーバークラスは、これらの各インターフェイスから共通の機能を継承しますが、実装するのは1回だけです。
ボブおじさんは、少しの重複がある場合について話します。
大きな重複がある場合はどうすればよいですか?
私たちは持っていると言う
Class UiInterface1;
Class UiInterface2;
Class UiInterface3;
Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};
UiInterface1
とUiInterface2
の間に大きな重複がある場合はどうすればよいですか?
キャスティング
これは、引用された本のアプローチに完全に正接するでしょうが、ISPに準拠するための1つの方法は、QueryInterface
COMスタイルのアプローチを使用して、コードベースの1つの中心領域にキャストの考え方を取り入れることです。
純粋なインターフェイスコンテキストで重複するインターフェイスを設計する誘惑の多くは、多くの場合、1つの正確な狙撃のような責任を果たすよりも、インターフェイスを「自給自足」にしたいという欲求から生じます。
たとえば、次のようなクライアント関数を設計するのは奇妙に思えるかもしれません。
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
const Vec2i xy = position->xy();
auto parent = parenting->parent();
if (parent)
{
// If the entity has a parent, return the sum of the
// parent position and the entity's local position.
return xy + abs_position(dynamic_cast<IPosition*>(parent),
dynamic_cast<IParenting*>(parent));
}
return xy;
}
...これらのインターフェイスを使用してクライアントコードにエラーが発生しやすいキャストを実行する責任を漏らしていること、および/または同じオブジェクトを複数の同じパラメータに複数回引数として渡すことを考えると、非常に醜く/危険です関数。そのため、IParenting
とIPosition
の懸念を1つの場所に統合する、より希薄なインターフェイスを設計したいことがよくあります。たとえば、IGuiElement
などの影響を受けやすくなります。同じ「自給自足」の理由で、同様に多くのメンバー関数を持つように誘惑される直交インターフェースの懸念と重なります。
混合の責任とキャスティング
完全に蒸留された極めて特異な責任を持つインターフェースを設計する場合、多くの場合、いくつかのダウンキャストを受け入れるか、インターフェースを統合して複数の責任を果たす(したがって、ISPとSRPの両方を踏襲する)ことになります。
COMスタイルのアプローチ(QueryInterface
部分のみ)を使用することで、ダウンキャストアプローチを採用しながら、コードベースの1つの中心的な場所にキャストを統合し、次のようなことを行うことができます。
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
// `Object::query_interface` returns nullptr if the interface is
// not provided by the entity. `Object` is an abstract base class
// inherited by all entities using this interface query system.
IPosition* position = obj->query_interface<IPosition>();
assert(position && "obj does not implement IPosition!");
const Vec2i xy = position->xy();
IParenting* parenting = obj->query_interface<IParenting>();
if (parenting && parenting->parent()->query_interface<IPosition>())
{
// If the entity implements IParenting and has a parent,
// return the sum of the parent position and the entity's
// local position.
return xy + abs_position(parenting->parent());
}
return xy;
}
...もちろん、うまくいけば、タイプセーフなラッパーと、生のポインタよりも安全なものを取得するために一元的に構築できるすべてのものがあります。
これにより、多くの場合、オーバーラップするインターフェースを設計する誘惑が最小限に抑えられます。 ISPを気にすることなく、好きなものをすべて組み合わせて組み合わせることができ、C++での実行時に疑似ダックタイピングの柔軟性を得ることができます(もちろん、オブジェクトが特定のインターフェイスをサポートしているかどうかを確認するためにクエリオブジェクトを実行する際のランタイムペナルティのトレードオフ)。ランタイム部分は、たとえば、これらのインターフェースを実装するプラグインのコンパイル時情報を事前に関数が持たないソフトウェア開発キットの設定で重要になる場合があります。
テンプレート
テンプレートが可能である場合(オブジェクトを取得するまでに失われない、必要なコンパイル時の情報が事前に用意されている場合)、次のように簡単に実行できます。
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
const Vec2i xy = obj.xy();
if (obj.parent())
{
// If the entity has a parent, return the sum of the parent
// position and the entity's local position.
return xy + abs_position(obj.parent());
}
return xy;
}
...もちろん、そのような場合、parent
メソッドは同じEntity
型を返す必要があります。その場合、インターフェースを完全に回避する必要があります(多くの場合、型情報が失われ、ベースポインタで作業することができます)。
エンティティコンポーネントシステム
柔軟性またはパフォーマンスの観点からCOMスタイルのアプローチをさらに追求し始めると、多くの場合、業界でゲームエンジンが適用されるものと同様のエンティティコンポーネントシステムになります。その時点で、多くのオブジェクト指向のアプローチに完全に垂直になりますが、ECSはGUI設計に適用できる可能性があります(シーン指向のフォーカスの外でECSを使用することを考えた1つの場所ですが、後で遅すぎると考えましたCOMスタイルのアプローチで解決しようとしています)。
このCOMスタイルのソリューションは、GUIツールキットの設計に関しては完全に存在し、ECSはさらに多くなるため、多くのリソースに支えられるものではないことに注意してください。それでも、責任が絶対的に最小になるインターフェースを設計するという誘惑を和らげることが確実にできるため、多くの場合それが問題になりません。
実用的なアプローチ
もちろん、もう1つの方法は、少しガードを緩めるか、インターフェースをきめ細かいレベルで設計してから継承を開始し、IPositionPlusParenting
の両方から派生するIPosition
のような、より粗いインターフェースを作成します。およびIParenting
(できればそれよりも良い名前を付けてください)。純粋なインターフェイスでは、一般的に適用されるモノリシックな深い階層型のアプローチ(Qt、MFCなど)ほどはISPに違反しないはずです。この場合、ドキュメントでは、違反するISPのレベルが高すぎるため、無関係なメンバーを非表示にする必要があると感じています。したがって、実用的なアプローチでは、あちこちでいくつかのオーバーラップを受け入れることができます。しかし、この種のCOMスタイルのアプローチにより、これまでに使用したすべての組み合わせに対して統合されたインターフェイスを作成する必要がなくなります。このような場合、「自給自足」の懸念は完全に排除されます。これにより、SRPとISPの両方との戦いを望む、責任が重複するインターフェースを設計するという誘惑の最終的な原因がしばしば排除されます。
これは、ケースバイケースで行う必要がある判断の呼びかけです。
まず第一に、SOLID=原則はまさに...原則です。それらはルールではありません。それらは特効薬ではありません。それらは単なる原則です。それを奪うことではありません。それらの重要性から、あなたはalways従うことに傾倒するべきですが、彼らがある程度の苦痛をもたらしたなら、あなたはそれらが必要になるまでそれらを捨てるべきです。
それを念頭に置いて、なぜ最初にインターフェースを分離するのかを考えてください。インターフェースの考え方は、「この消費するコードが、消費されるクラスに実装される一連のメソッドを必要とする場合、実装にコントラクトを設定する必要があります。このインターフェースを持つオブジェクトを提供してくれれば、私は働くことができます。それと。"
ISPの目的は、「必要なコントラクトが既存のインターフェイスのサブセットのみである場合、メソッドに渡される可能性のある将来のクラスに既存のインターフェイスを適用しないでください」と言うことです。
次のコードを検討してください。
public interface A
{
void X();
void Y();
}
public class Foo
{
public void ConsumeX(A a)
{
a.X();
}
}
これで、新しいオブジェクトをConsumeXに渡したい場合、X() and Y()=を実装して、契約する。
では、次の例のようにコードを変更する必要がありますか?
public interface A
{
void X();
void Y();
}
public interface B
{
void X();
}
public class Foo
{
public void ConsumeX(B b)
{
b.X();
}
}
ISPは私たちがすべきであると提案しているので、その決定に頼るべきです。しかし、コンテキストがないと、確信が持てません。 AとBを拡張する可能性はありますか?それらは独立して拡張する可能性がありますか? BがAが必要としないメソッドを実装する可能性はありますか? (そうでない場合は、AをBから派生させることができます。)
これはあなたがしなければならない判断の呼びかけです。また、その呼び出しを行うのに十分な情報がない場合は、おそらく最も単純なオプションを選択する必要があります。
どうして?後で考えを変えるのは簡単だからです。新しいクラスが必要な場合は、新しいインターフェイスを作成して、古いクラスに両方を実装します。