インターフェースと抽象の違いは理解できたと思います。Abstractはデフォルトの動作を設定し、純粋なabstractの場合、動作は派生クラスによって設定する必要があります。インターフェイスは、基本クラスからのオーバーヘッドなしで必要なものを取得します。では、構成に対するインターフェースの利点は何ですか?私が考えることができる唯一の利点は、基本クラスで保護されたフィールドを使用することです。私は何が欠けていますか?
5 に答える
タイトルが意味をなさず、説明が少しぼやけているので、用語を定義しましょう(そして不足しているキーを紹介します)。
ここで起こっていることは2つあります。
- 抽象クラスとインターフェース
- 継承と構成
インターフェイスと抽象クラスから始めましょう。
- 抽象クラス(C ++)は、少なくとも1つのメソッドが純粋仮想メソッドであるため、インスタンス化できないクラスです。
- Javaのような言語では、インターフェイスは実装されていないメソッドのセットであり、C ++では、純粋仮想メソッドのみを持つ抽象クラスでエミュレートされます。
したがって、C ++のコンテキストでは、どちらにも大きな違いはありません。特に、区別が自由関数を考慮に入れていなかったためです。
たとえば、次の「インターフェイス」について考えてみます。
class LessThanComparable {
public:
virtual ~LessThanComparable() {}
virtual bool less(LessThanComparable const& other) const = 0;
};
無料の機能を使用しても、簡単に拡張できます。
inline bool operator<(LessThanComparable const& left, LessThanComparable const& right) {
return left.less(right);
}
inline bool operator>(LessThanComparable const& left, LessThanComparable const& right) {
return right.less(left);
}
inline bool operator<=(LessThanComparable const& left, LessThanComparable const& right) {
return not right.less(left);
}
inline bool operator>=(LessThanComparable const& left, LessThanComparable const& right) {
return not left.less(right);
}
この場合、動作を提供します...それでもクラス自体はまだインターフェースです...まあ。
したがって、本当の議論は継承と構成の間です。
継承は、動作を継承するために誤用されることがよくあります。これは悪いです。継承は、is-a関係をモデル化するために使用する必要があります。それ以外の場合は、おそらくコンポジションが必要です。
単純なユースケースを考えてみましょう。
class DieselEngine { public: void start(); };
さて、これを使ってどのように構築Car
しますか?
継承すれば動作します。しかし、突然あなたはそのようなコードを手に入れます:
void start(DieselEngine& e) { e.start(); }
int main() {
Car car;
start(car);
}
ここで、に置き換えることにDieselEngine
したWaterEngine
場合、上記の機能は機能しません。コンパイルは失敗します。そして、WaterEngine
継承することはDieselEngine
確かに厄介だと感じます...
それでは解決策は何ですか?構成。
class Car {
public:
void start() { engine.start(); }
private:
DieselEngine engine;
};
このように、車がエンジンであると想定する無意味なコードを書くことはできません(doh!)。したがって、エンジンの交換は簡単で、顧客への影響はまったくありません。
これは、実装とそれを使用するコードの間のアドヒアランスが少ないことを意味します。またはそれが通常参照されるように:より少ない結合。
経験則では、一般に、データまたは実装動作を持つクラスからの継承は眉をひそめる必要があります。それは合法である可能性がありますが、多くの場合、より良い方法があります。もちろん、すべての経験則のように、それは一粒の塩と一緒に摂取されるべきです。過剰設計に注意してください。
インターフェイスは、使用方法を定義します。
再利用するために継承します。これは、何らかのフレームワークに適合させたいことを意味します。フレームワークに適合させる必要がない場合は、自分で作成したものであっても、継承しないでください。
構成は実装の詳細です。基本クラスの実装を取得するために継承しないで、それを構成します。フレームワークに適合できる場合にのみ継承します。
インターフェイスは動作を定義します。抽象クラスは、動作の実装に役立ちます。
理論的には、実装がまったくない純粋な抽象クラスとインターフェースの間に大きな違いはありません。どちらも実装されていないAPIを定義します。ただし、純粋な抽象クラスは、セマンティクス(C ++など)のようなインターフェイスを提供するインターフェイスをサポートしていない言語でよく使用されます。
選択肢がある場合、通常、抽象ベースは、完全ではない場合でも、ある程度の機能を提供します。これは、一般的な動作の実装に役立ちます。欠点は、あなたがそれから派生することを余儀なくされていることです。単に使用法を定義する場合は、インターフェースを使用してください。(インターフェースを実装する抽象的なベースの作成を妨げるものは何もありません)。
インターフェイスはシンであり、C ++では、純粋仮想関数のみを持つクラスとして記述できます。薄いので良いです
- インターフェースの使用または実装における学習曲線を短縮します
- これにより、ユーザーとインターフェースの実装者との間の結合(依存関係)が減少します。したがって、ユーザーは、使用しているインターフェースの実装の変更から非常によく隔離されています。
これは、ダイナミックライブラリリンクと組み合わせて、プラグアンドプレイを容易にするのに役立ちます。これは、最近では知られていないが優れたソフトウェアイノベーションの1つです。これにより、ソフトウェアの相互運用性、拡張性などが向上します。
インターフェイスを配置するのは、より多くの作業になる可能性があります。いつか複数の実装が可能な重要なサブシステムがある場合は、それらの採用を正当化してください。その場合、サブシステムはインターフェースを介して使用する必要があります。
継承による再利用には、オーバーライドする実装の動作に関するより多くの知識が必要であるため、より大きな「結合」があります。とはいえ、インターフェイスが過剰な場合にも有効なアプローチです。
タイプYがタイプXを継承する場合、タイプXのオブジェクトを処理する方法を知っているコードは、ほとんどの場合、タイプYのオブジェクトを自動的に処理できます。同様に、タイプZがインターフェイスIを実装している場合、 Iを実装するオブジェクトの使用方法は、それらについて何も知らなくても、自動的にタイプZのオブジェクトを使用できるようになります。継承とインターフェースの主な目的は、そのような置換を可能にすることです。
対照的に、タイプPのオブジェクトにタイプQのオブジェクトが含まれている場合、タイプQのオブジェクトで動作することを期待するコードは、タイプPのオブジェクトで動作できません(Pが次のオブジェクトを保持することに加えてQから継承しない限り)そのタイプ)。タイプQのオブジェクトを操作することを期待するコードは、P内に含まれるQインスタンスを操作できますが、Pのコードが明示的にそのコードに直接提供するか、外部のコードで利用できるようにする場合に限ります。