インターフェース分離の原則:インターフェースに大きな重複がある場合はどうすればよいですか?


9

アジャイルソフトウェア開発、原則、パターン、およびプラクティス:ピアソン新国際版

場合によっては、クライアントの異なるグループによって呼び出されるメソッドが重複することがあります。オーバーラップが小さい場合、グループのインターフェースは分離したままにする必要があります。共通の関数は、オーバーラップするすべてのインターフェースで宣言する必要があります。サーバークラスは、これらの各インターフェイスから共通の機能を継承しますが、実装するのは1回だけです。

ボブおじさんは、少しの重複がある場合について話します。

大きな重複がある場合はどうすればよいですか?

私たちは持っていると言います

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

間にかなりの重複がある場合、我々は何をすべきUiInterface1とはUiInterface2


非常に重なっているインターフェースに出くわしたら、親インターフェースを作成します。親インターフェースは、共通メソッドをグループ化し、この共通インターフェースから継承して特殊化を作成します。だが!特殊化せずに誰もが共通インターフェースを使用したくない場合は、実際にコードの複製に取り掛かる必要があります。なぜなら、親の共通インターフェースを導入すれば、人々はそれを使用できるからです。
アンディ

質問は私には少しあいまいです、ケースに応じて多くの異なる解決策で答えることができます。なぜ重なりが大きくなったのですか?
Arthur Havlicek

回答:


1

鋳造

これはほぼ間違いなく引用された本のアプローチに完全に正直になるでしょうが、ISPによりよく準拠する1つの方法は、QueryInterfaceCOMスタイルのアプローチを使用して、コードベースの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;
}

...これらのインターフェイスを使用してクライアントコードにエラーが発生しやすいキャストを実行する責任を漏らしていること、および/または同じオブジェクトを引数として同じオブジェクトの複数のパラメーターに複数回渡すことを考えると、非常に醜く/危険です関数。多くの場合の懸念統合し、より希釈されたインタフェースを設計するために望んで終わる我々はそうIParentingIPosition、一つの場所でのように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はさらに多くなるため、多くのリソースに支えられるものではないことに注意してください。それでも、責任が絶対的に最小になるインターフェースを設計するという誘惑を和らげることが確実にできるため、多くの場合それが問題になることはありません。

実用的なアプローチ

代替は、当然のことながら、細かいレベルであなたのガードビット、または設計インターフェースをリラックスして、あなたが使用することを粗いインターフェイスを作成するためにそれらを継承し始める、のようなものIPositionPlusParentingの両方から派生しているIPositionIParenting(うまくいけば、それよりも良い名前で)。純粋なインターフェイスでは、一般的に適用されるモノリシックな深い階層型のアプローチ(Qt、MFCなど)ほどISPに違反してはなりません。ドキュメントでは、違反するISPのレベルが高すぎるため、関係のないメンバーを非表示にする必要があると感じることがよくあります。したがって、実用的なアプローチでは、あちこちでいくつかのオーバーラップを受け入れることができます。しかし、この種のCOMスタイルのアプローチにより、これまでに使用したすべての組み合わせに対して統合されたインターフェイスを作成する必要がなくなります。このような場合、「自給自足」の懸念は完全に排除されます。これにより、SRPとISPの両方と戦いたいと考える責任が重複するインターフェースを設計するという誘惑の最終的な原因がしばしば排除されます。


11

これは、ケースバイケースで行う必要がある判断の呼びかけです。

まず第一に、SOLIDの原則はまさにその...原則であることを忘れないでください。彼らはルールではありません。それらは特効薬ではありません。それらは単なる原則です。それは彼らの重要性失うことではありませ、あなたは常に彼らに従うことに傾倒すべきです。しかし、2番目にある程度の痛みが生じるので、必要になるまで捨てる必要があります。

それを念頭に置いて、そもそもなぜインターフェースを分離するのかを考えてください。インターフェースの考え方は、「この消費するコードが、消費されるクラスで実装される一連のメソッドを必要とする場合、実装でコントラクトを設定する必要があります。このインターフェースを持つオブジェクトを提供すると、私は働くことができます。それと。"

ISPの目的は、「必要なコントラクトが既存のインターフェイスのサブセットのみである場合、メソッドに渡される可能性のある将来のクラスに既存のインターフェイスを適用しないでください」と言うことです。

次のコードを検討してください。

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

これで、ConsumeXに新しいオブジェクトを渡したい場合、X()と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から派生させることができます。)

これはあなたがしなければならない判断の呼びかけです。また、その呼び出しを行うのに十分な情報がない場合は、おそらく最も単純なオプションを選択する必要があります。これが最初のコードである可能性があります。

どうして?後で考えを変えるのは簡単だからです。その新しいクラスが必要な場合は、新しいインターフェースを作成し、古いクラスに両方を実装するだけです。


1
「まず第一に、SOLIDの原則は単なる...原則であることを覚えておいてください。それらはルールではありません。それらは特効薬ではありません。それらは単なる原則です。それらの重要性を取り除くためではなく、常に学習する必要があります。しかし、彼らがある程度の苦痛をもたらすなら、あなたはそれらが必要になるまでそれらを捨てるべきです。」これは、すべての設計パターン/原則の本の最初のページにあるはずです。また、50ページごとに表示されます。
クリスチャンロドリゲス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.