web-dev-qa-db-ja.com

OOP-継承の乱用を識別する方法は?

継承が悪用される可能性がある場合を確認するための簡単なヒューリスティックを探す際に、次の仮説を立てました。

サブクラスBがメソッドfooをオーバーライドするが、base.foo()を呼び出さない場合、同じメソッドシグネチャのみを使用するため、そのメソッドの継承は壊れているようですが、必ずしも同じ動作です。

これは、本来あるべきではない継承を特定する正しい方法ですか?

2
cowboydan

意図せずに継承を誤ってしまう方法は100万通りあります。サブクラスが適切に動作することを確認するために確認できることの完全なリストはありません。たとえあったとしても、サブクラスにそれを強制することはできません。そして、可能であれば、スーパークラスは、一部のサブクラスを意図せずに壊すような方法で変更することもできます。

よりよい質問は、どのようにavoid問題を解決するかであり、以下の回答があります。

  • そもそも継承は避けましょう。いいえ、真剣に。 「構成」を好むだけでなく、継承を悪いオプションと見なします。
  • 絶対に継承する必要がある場合は、次のことを確認してください。
    • スーパークラスが明示的に抽象化されている(サブクラス化のために明示的に記述されている)
    • オーバーライドできるすべてのメソッドはオーバーライドする必要があります。つまり、ボディはありません。
    • オーバーライドしてはならないすべてのメソッド、上書きを許可してはならない
    • 状態、プロパティ、メソッド、ユーティリティメソッドなどをサブクラスと共有しないでください。スーパークラスを個別のオブジェクトと見なし、サブクラスを完全にエイリアンのオブジェクトと見なし、他のオブジェクトと同じ保護(カプセル化)を使用します。

これらのルールがあっても、平等のような問題がまだあります。

4

Robert Brautiganの回答は適切であり、私はそれを置き換えるつもりはありませんが、投稿した理由について直接フィードバックを追加したいと思いました。

サブクラスBがメソッドfooをオーバーライドするが、base.foo()を呼び出さない場合、そのメソッドの継承は壊れているようです

その場合、base.foo()が呼び出されるように強制するのではなく、コンパイラーが(outを設定する必要があることを強制できるのと同様に)意味がありません。メソッド本体のどこかにパラメータ)。

base.foo()を呼び出すかどうかは、派生クラスが実装を変更するか実装するか(呼び出さないで)か、さらに拡張するか(呼び出すか)によって異なります。あなたの派生クラスは何か別のことをしていますか、それとも何か特別なことをしていますか?

これらのオプションはどちらも、継承の有効なユースケースになります。あなたが思いついた仮説は、それが動作を拡張することのみを考慮し、それを上書きすることはしないという点で、非常に厳格です。 overrideキーワードの命名に関するヒントを参考にしてください-オーバーライド動作は、確かに継承のユースケースとして有効です。

同じメソッドシグネチャのみを使用しますが、必ずしも同じ動作ではありません。

あなたの仮説は間違っていますが、私はあなたがどこから来ているのかを理解しています。根本的なアイデアにはいくつかのメリットがありますが、そこから導き出した結論は違います。

はい、動作はfunctionalレベルで同じである必要があります(つまり、メソッドの目的)。基本メソッドがメッセージをログに記録し、オーバーライドされたメソッドがデータベーステーブルを削除する場合、それらは2つのまったく異なる動作であるため、オーバーライドの合理的な実装ではありません。

ただし、これは、オーバーライドされたメソッドがtechnicalレベル(つまり、コード自体)で異なることができないという意味ではありません。派生メソッドがこのような異なるアプローチを使用します(同じ意図で!)基本メソッドを実際に再利用することはできません。

これまでに作成したクラスのいずれかでオーバーライドするたびに基本メソッドを呼び出したことがない場合は、継承を実装するときにおそらく何かが間違っています。しかし、これは、ベースメソッドを一度も呼び出さないことが、その特定のインスタンスにおける継承の乱用の直接的な証拠であることを意味するものではありません。


あなたが思いついた仮説にあなたを導くものを考えると、あなたがしたい状況にいると思いますguarantee基本ロジックが呼び出され、派生クラスが迂回できないそれ。

これは、基本クラスの適切な構造で実施できます。例:

_public class BasePizza
{
    sealed public void MakePizza()
    {
        MakePizzaBase();
        AddToppings();
    }

    private void MakePizzaBase()
    {
        // this will ALWAYS be done and derived classes cannot avoid it

        Console.Writeline("Making dough");
        Console.Writeline("Adding pizza sauce");
    }

    protected virtual void AddToppings()
    {
        // Derived classes CAN rewrite this if they want

        Console.WriteLine("Adding cheese");
    }
}
_

あなたが作るすべての派生ピザは常に生地とピザソースを持っていますが、彼らが望むなら、彼らはデフォルトのトッピング(この場合はチーズ)を追加することを避けて自由です。例えば:

_public class LactoseFreePizza : BasePizza // Basic pizza with extra meat but NO cheese!
{
    protected override void AddToppings()
    {
        // No cheese on this!

        Console.WriteLine("Adding lots of meat");
    }
}

public class MeatLoversPizza : BasePizza // Basic pizza with extra meat
{
    protected override void AddToppings()
    {
        // Here, we do use the default toppings
        base.AddToppings();

        Console.WriteLine("Adding lots of meat");
    }
}
_

これらのメソッドがどのようにプライベートで保護されているかに注目してください。唯一のパブリックメソッドはMakePizza()であるため、消費者はピザを作成するためにそのメソッドを経由する必要があります。これは、基本クラスが一部のロジックが常に実行されることを強制できることを意味します。これはいくつかの異なることに依存しています:

  • MakePizzaBaseprivateなので、派生クラスはアクセスしたり変更したりできません。
  • MakePizzaも派生クラスでは変更できませんが、コンシューマがアクセスする必要があるため、publicにする必要があります。したがって、MakePizzasealedであるため、派生クラスはその動作を変更することだけを決定することはできません。
  • AddToppingsvirtualメソッドです。これは、派生クラス変更できる部分(MakePizzaロジックの)であるためです彼らが合うと思うように。ただし、コンシューマーがこのメソッドを直接呼び出すことは許可されていないため(MakePizzaを使用して、基本ロジックが確実に実行されるようにするため)、AddToppingsメソッドをprotected
  • メソッド本体なしでAddToppingsabstractメソッドにすることも可能であったことに注意してください。つまり、デフォルトの動作はありませんが、派生クラスに追加するトッピングを明示的に定義するように強制します。空のメソッドを実装することもできますが、強制的に実装されます。これは、一部のケースでは理にかなっていますが、すべてのケースではありません。

これはあなたが望むものを手に入れます。基本クラスは、派生クラスが何をしようとしても、そのロジックの一部が常に実行されるように強制できます。


sealedは、この例では実際にはオプションです。virtualでないメソッドは、本質的にsealedと見なされるためです。ただし、3番目の継承レイヤーがあり、AとBの間でこのメソッドを継承したいが、B(C、D、またはE)の派生に対してではない場合、Aはそのメソッドをvirtualとして定義し、 Bはそれを継承すると同時にsealedにして、それ以上の派生で上書きされないようにします。
この場合、デフォルトでsealedを追加することをお勧めします。意図を電信で伝えるため、このメソッドが開発者に通知されるためです必須ではない将来の任意の時点で仮想化されます。


だからといって、継承が常に正しいアプローチであるとは限りません。継承が適切なツールではない場合がたくさんあります。この回答では、具体的なコンテキストを指定しなかったため、特定のユースケースに継承が適切かどうかを評価することはしません。howについてのみ言及し、継承がここで保証されます。

4
Flater