最近、抽象クラスの使用について困っています。
時々、抽象クラスが事前に作成され、派生クラスがどのように機能するかのテンプレートとして機能します。つまり、多かれ少なかれ、それらはいくつかの高レベルの機能を提供しますが、派生クラスによって実装される特定の詳細を除外します。抽象クラスは、いくつかの抽象メソッドを配置することにより、これらの詳細の必要性を定義します。そのような場合、抽象クラスは設計図、機能の高レベルの説明、またはそれを呼び出したいもののように機能します。単独では使用できませんが、高レベルの実装から除外された詳細を定義するために専門化する必要があります。
場合によっては、いくつかの「派生」クラスの作成後に抽象クラスが作成されることがあります(親/抽象クラスがそこにないため、それらはまだ派生されていませんが、私が何を意味するか知っています)。これらの場合、抽象クラスは通常、現在の派生クラスに含まれるあらゆる種類の一般的なコードを配置できる場所として使用されます。
上記の観察をしたので、私はこれらの2つのケースのどちらがルールであるべきか疑問に思っています。派生クラスすべてに共通しているという理由だけで、抽象クラスに詳細を吹き込む必要がありますか?高レベルの機能の一部ではない一般的なコードがありますか?
たまたま派生クラスに共通しているからといって、抽象クラス自体には意味がないコードがあるべきですか?
例を挙げましょう:抽象クラスAにはメソッドa()と抽象メソッドaq()があります。メソッドaq()は、派生クラスABとACの両方で、メソッドbを使用します()。b() Aに移動しますか?はいの場合、誰かがAだけを見ている場合(ABとACがそこにいないふりをしましょう)、b()はあまり意味がありません!これは悪いことですか?誰かが抽象クラスを見て、派生クラスにアクセスせずに何が起こっているのか理解できるようにする必要がありますか?
正直に言うと、私はこれを尋ねた時点で、派生クラスを調べなくても意味のある抽象クラスを作成することは、クリーンなコードとアーキテクチャの問題であると信じがちです。 anyあらゆる種類のコードのダンプのように機能する抽象クラスのアイデアが、すべての派生クラスで一般的に発生することは、あまり好きではありません。
あなたはどう思いますか/練習しますか?
例を挙げましょう:抽象クラスAにはメソッドa()と抽象メソッドaq()があります。メソッドaq()は、派生クラスABとACの両方で、メソッドbを使用します()。b() Aに移動しますか?はいの場合、誰かがAだけを見ている場合(ABとACがそこにいないふりをしましょう)、b()はあまり意味がありません!これは悪いことですか?誰かが抽象クラスを見て、派生クラスにアクセスせずに何が起こっているのか理解できるようにする必要がありますか?
b()
をどこに配置するかという質問です。ある意味では、A
がAB
とAC
の直接のスーパークラスとして最適かどうかという質問です。
3つの選択肢があるようです。
AB
とAC
の両方にb()
を残すA
を継承し、b()
を導入する中間クラス_ABAC-Parent
_を作成し、AB
およびAC
の直接のスーパークラスとして使用しますA
にb()
を入れます(将来の別のクラスAD
がb()
を必要とするかどうかわからない)b()
を必要としない別のクラスAD
が表示されるまで、(3)は正しい選択のようです。
AD
が提示するようなときに、(2)のアプローチにリファクタリングできます—結局のところ、それはソフトウェアです!
抽象クラスは、便利であるため、抽象クラスに投入されるさまざまな関数やデータのダンプ場となることを意図していません。
最も信頼性が高く拡張可能なオブジェクト指向のアプローチを提供していると思われる経験則の1つは、「 継承よりも構成を優先する 」です。抽象クラスは、おそらくコードを含まないインターフェース仕様と考えるのが一番でしょう。
抽象クラスに付随する一種のライブラリメソッドであるメソッドがある場合、抽象クラスから派生したクラスが通常必要とするいくつかの機能または動作を表現するための最も可能性の高いメソッドであるメソッドは、このメソッドが利用可能な場合、抽象クラスと他のクラスの間にあるクラス。この新しいクラスは、このメソッドを提供することにより、特定の実装の代替またはパスを定義する抽象クラスの特定のインスタンス化を提供します。
抽象クラスのアイデアは、抽象クラスを実際に実装する派生クラスがサービスまたは動作まで提供することになっているものの抽象モデルを持つことです。継承は使いすぎやすいので、最も有用なクラスは mixinパターン を使用してさまざまなクラスで構成されることがよくあります。
ただし、常に変化の問題があり、何が変化し、どのように変化し、なぜ変化するのかがわかります。
継承は脆弱で壊れやすい可能性があります ソースの本体( 継承:すでに使用を停止するだけ! も参照してください)。
壊れやすい基本クラスの問題は、オブジェクト指向プログラミングシステムの基本的なアーキテクチャの問題であり、基本クラス(スーパークラス)は「壊れやすい」と見なされます。 。プログラマは、基本クラスのメソッドを単独で調べるだけでは、基本クラスの変更が安全かどうかを判断できません。
あなたの質問は抽象クラスへのどちらかまたはアプローチを示唆しています。しかし、私はあなたがそれらをあなたのツールボックスの単なる別のツールとみなすべきだと思います。そして、問題は次のようになります:どのジョブ/問題に対して、抽象クラスが適切なツールですか?
優れた使用例の1つは テンプレートメソッドパターン を実装することです。すべての不変ロジックを抽象クラスに配置し、バリアントロジックをそのサブクラスに配置します。共有ロジック自体は不完全であり、機能しないことに注意してください。ほとんどの場合、それはアルゴリズムの実装に関するものであり、いくつかのステップは常に同じですが、少なくとも1つのステップは異なります。この1つのステップを、抽象クラス内の関数の1つから呼び出される抽象メソッドとして配置します。
時々、抽象クラスが事前に作成され、派生クラスがどのように機能するかのテンプレートとして機能します。つまり、多かれ少なかれ、それらはいくつかの高レベルの機能を提供しますが、派生クラスによって実装される特定の詳細を除外します。抽象クラスは、いくつかの抽象メソッドを配置することにより、これらの詳細の必要性を定義します。そのような場合、抽象クラスは設計図、機能の高レベルの説明、またはそれと呼べるもののように機能します。単独では使用できませんが、高レベルの実装から除外された詳細を定義するために専門化する必要があります。
最初の例は基本的にテンプレートメソッドパターンの説明だと思います(私が間違っている場合は訂正してください)。これは、抽象クラスの完全に有効なユースケースと見なします。
場合によっては、いくつかの「派生」クラスの作成後に抽象クラスが作成されることがあります(親/抽象クラスがそこにないため、それらはまだ派生されていませんが、私が何を意味するか知っています)。これらの場合、抽象クラスは通常、現在の派生クラスに含まれるあらゆる種類の一般的なコードを配置できる場所として使用されます。
2番目の例では、共有ロジックと重複コードを処理するための優れた方法があるため、抽象クラスの使用は最適な選択ではないと思います。抽象クラスA
、派生クラスB
およびC
があり、両方の派生クラスがメソッドs()
の形式でいくつかのロジックを共有しているとします。重複を取り除くための正しいアプローチを決定するには、メソッドs()
がパブリックインターフェイスの一部であるかどうかを知ることが重要です。
そうでない場合(メソッドb()
を使用した独自の具体例のように)、ケースは非常に簡単です。それから別のクラスを作成し、操作の実行に必要なコンテキストを抽出するだけです。これは 継承を介した構成 の典型的な例です。コンテキストがほとんどまたはまったく必要ない場合は、いくつかのコメントで提案されているように、単純なヘルパー関数ですでに十分な場合があります。
s()
がパブリックインターフェイスの一部である場合は、少しトリッキーになります。 s()
はB
およびC
がA
に関連していることとは何の関係もないという前提の下で、s()
を中に入れるべきではありませんA
。それでそれをどこに置くのですか? s()
を定義する別のインターフェースI
を宣言することを主張します。次に、s()
を実装するためのロジックを含む別のクラスを作成し、B
とC
の両方に依存する必要があります。
最後に、 here は、SOに関する興味深い質問の優れた回答へのリンクです。これは、いつインターフェイスにアクセスするか、いつ抽象クラスにアクセスするかを決定するのに役立つ場合があります。 :
優れた抽象クラスは、その機能や状態を共有できるため、書き換えが必要なコードの量を削減します。
論理的にも技術的にもオブジェクト指向のポイントが欠けているように思えます。 2つのシナリオについて説明します。基本クラスの型の一般的な動作のグループ化と多態性です。これらはどちらも、抽象クラスの正当なアプリケーションです。ただし、クラスを抽象化する必要があるかどうかは、技術的な可能性ではなく、分析モデルに依存します。
実世界に具体化されていないタイプを認識する必要がありますが、存在する特定のタイプの基礎を築きます。例:動物。動物のようなものはありません。それはいつも犬か猫か何かですが、実際の動物はありません。しかし動物はそれらすべてを組み立てます。
次に、レベルについて話します。継承に関しては、レベルは契約の一部ではありません。また、一般的なデータや一般的な動作を認識していません。これは、おそらく役に立たない技術的なアプローチです。ステレオタイプを認識してから、基本クラスを挿入する必要があります。そのようなステレオタイプがない場合は、複数のクラスによって実装されているいくつかのインターフェースを使用することをお勧めします。
抽象クラスの名前とそのメンバーは、わかりやすいはずです。技術的にも論理的にも、派生クラスから独立している必要があります。アニマルのように、抽象メソッドEat(多態)とブールの抽象プロパティIsPetとIsLifestockを設定できます。これは、猫、犬、または豚について知らなくても意味があります。依存関係(技術的または論理的)は、子孫からベースへの一方向にのみ進む必要があります。基本クラス自体も、降順クラスの知識を持っていてはなりません。
最初のケース、あなたの質問から:
そのような場合、抽象クラスは設計図、機能の高レベルの説明、またはそれと呼べるもののように機能します。
2番目のケース:
また、抽象クラスは通常、現在の派生クラスに含まれるあらゆる種類の一般的なコードを配置できる場所として使用されます。
次に、あなたの質問:
私はこれらの2つのケースのどちらがルールであるべきか疑問に思っています。 ...たまたま、それが派生クラスに共通しているからといって、抽象クラス自体に意味がない可能性のあるコードがあるべきですか?
IMOには2つの異なるシナリオがあるため、適用する必要のあるデザインを決定するための単一のルールを描くことはできません。
最初のケースでは、他の回答ですでに言及されている Template Method patternのようなものがあります。
2番目のケースでは、b()メソッドを受け取る抽象クラスAの例を示しました。 IMO b()メソッドは、他のすべての派生クラスに対して意味がある場合にのみ移動する必要があります。つまり、2つの場所で使用されているために移動している場合、おそらくこれは適切な選択ではありません。なぜなら、明日、b()が意味をなさない新しい具体的なDerivedクラスが存在する可能性があるためです。すべて。また、あなたが言ったように、Aクラスを個別に見て、b()もそのコンテキストで意味をなさない場合、おそらくこれもデザインの選択として適切ではないというヒントになります。