4

私はObjCでコーディングしていますが、この質問は意図的に言語に依存しません-ほとんどのオブジェクト指向言語に適用されるはずです

「Collection」クラスがあり、「Collection」から継承する「FilteredCollection」を作成するとします。フィルタはオブジェクトの作成時に設定され、それ以降、クラスはそのコンテンツにフィルタが適用された「コレクション」のように動作します。

私は物事を明白な方法で行い、コレクションをサブクラス化します。私はすべてのアクセサーをオーバーライドし、かなりきちんとした仕事をしたと思います-FilteredCollectionはコレクションと同じように動作するように見えますが、ユーザーにフィルターで除外されているフィルターに対応するオブジェクトが含まれています。FilteredCollectionsを作成して、コレクションとしてプログラムに渡すことができると思います。

しかし、私はテストに来て、-ああ、いや-それは機能していません。デバッガーを詳しく調べてみると、一部のメソッドのCollection実装が、オーバーライドされたFilteredCollectionメソッドを呼び出していることがわかります(たとえば、Collectionがオブジェクトを反復処理するときに依存する「count」メソッドがありますが、フィルター処理されたカウントを取得しています。 、正しい外部動作を与えるためにcountメソッドをオーバーライドしたため)。

ここで何が問題になっていますか?OOもこのように機能する必要があるように感じられるにもかかわらず、いくつかの重要な原則に違反しているように感じるのはなぜですか?この問題の一般的な解決策は何ですか?ありますか?

ちなみに、特にこの問題の良い「解決策」は、オブジェクトをコレクションに入れる前にフィルタリングすることであり、コレクションをまったく変更する必要がないことを知っていますが、より一般的な質問をしていますそれよりも-これは単なる例です。より一般的な問題は、サブクラスによって変更される可能性のある他のメソッドの動作に依存する不透明なスーパークラスのメソッドと、このような動作を変更するためにオブジェクトをサブクラス化する場合の対処方法です。

4

11 に答える 11

11

Collection継承元には一定の契約があります。クラスのユーザー(および、独自のメソッドを呼び出すことができるため、クラス自体を含む)は、サブクラスがコントラクトに従うと想定します。運が良ければ、契約はそのドキュメントで明確かつ明確に指定されています...

たとえば、コントラクトは、「要素を追加xしてから、コレクションを反復処理すると、元にx戻す必要があります」と言うことができます。あなたのFilteredCollection実装はその契約を破っているようです。

ここには別の問題がありCollectionます。具体的な実装ではなく、インターフェースである必要があります。実装(例TreeSet)はそのインターフェースを実装する必要があり、もちろんその契約にも従う必要があります。

Collectionこの場合、正しいデザインはから継承するのではなく、そのFilteredCollection周りに「ラッパー」として作成することだと思います。おそらく、コレクションの通常の契約に従わないため、インターフェイスをFilteredCollection実装するべきではありません。Collection

于 2009-09-28T14:12:27.200 に答える
4

Collectionをサブクラス化してFilteredCollectionを実装するのではなく、FilteredCollectionをiCollectionを実装し、既存のコレクションに委任する別のクラスとして実装してみてください。これは、GangofFourのデコレータパターンに似ています。

部分的な例:

class FilteredCollection implements ICollection
{
    private ICollection baseCollection;

    public FilteredCollection(ICollection baseCollection)
    {
        this.baseCollection = baseCollection;
    }

    public GetItems()
    {
        return Filter(baseCollection.GetItems());
    }

    private Filter(...)
    {
        //do filter here
    }

}

FilteredCollectionをICollectionのデコレータとして実装すると、サブクラス化した1つのクラスだけでなく、ICollectionを実装するすべてのものをフィルタリングできるという追加の利点があります。

さらに便利なように、コマンドパターンを使用して、実行時にFilteredCollectionにFilter()の特定の実装を挿入し、適用するフィルターごとに異なるFilteredCollection実装を作成する必要をなくすことができます。

于 2009-09-28T14:19:38.767 に答える
3

(私があなたの例を使用する間、私はあなたの特定の例の何が悪いのかをあなたに話すのではなく、概念に集中しようとすることに注意してください)。

ブラックボックスの継承?

あなたがぶつかっているのは、「ブラックボックスの継承」の神話です。多くの場合、継承を許可する実装を、その継承を使用する実装から完全に分離することは実際には不可能です。私はこれが継承がしばしば教えられる方法に直面して飛ぶことを知っています、しかしそれはあります。

あなたの例を挙げると、コレクション契約の消費者に、コレクションから取得できるアイテムの数と一致するカウントを表示させるのは非常に合理的です。また、継承された基本クラスのコードがそのCountプロパティにアクセスし、期待するものを取得することも非常に合理的です。何かを与える必要があります。

誰が責任者ですか?

回答:基本クラス。基本クラスの上の両方の目標を達成するには、物事を異なる方法で処理する必要があります。なぜこれが基本クラスの責任なのですか?これは、それ自体を継承し、メンバーの実装をオーバーライドできるようにするためです。選択の余地がないのは、オブジェクト指向デザインを容易にする一部の言語である可能性があります。しかし、それはこの問題に対処するのを難しくしますが、それでも対処する必要があります。

この例では、基本コレクションクラスは、サブクラスがCountの既存の実装をオーバーライドする可能性があることを認識して、実際のカウントを決定する独自の内部手段を備えている必要があります。パブリックでオーバーライド可能なCountプロパティの独自の実装は、基本クラスの内部操作に影響を与えるべきではなく、実装している外部契約を達成するための手段にすぎません。

もちろん、これは基本クラスの実装が私たちが望むほど鮮明でクリーンではないことを意味します。これは、ブラックボックスの継承が神話であるという意味です。継承を許可するためだけに実装コストがかかります。

結論...

継承可能なクラスは、オーバーライド可能なメンバーの想定される操作に依存しないように、防御的にコーディングする必要があります。または、何らかの形式のドキュメントで、メンバーのオーバーライドされた実装からどのような動作が期待されるかを正確に明確にする必要があります(これは抽象メンバーを定義するクラスで一般的です)。

于 2009-09-28T14:52:30.273 に答える
2

あなたFilteredCollectionは気分が悪い。通常、コレクションがあり、それに新しい要素を追加すると、コレクションがcount1つ増え、新しい要素がコンテナーに追加されることが期待されます。

あなたFilteredCollectionはこのようには機能しません-フィルタリングされたアイテムを追加した場合count、コンテナのは変更されない可能性があります。これがあなたのデザインがうまくいかないところだと思います。

その動作が意図されている場合、のコントラクトはcount、メンバー関数がそれを使用しようとしている目的には不適切になります。

于 2009-09-28T14:12:02.803 に答える
2

本当の問題は、オブジェクト指向言語がどのように機能するかについての誤解だと思います。次のようなコードがあると思います。

Collection myCollection = myFilteredCollection;

そして、Collectionクラスによって実装されたメソッドを呼び出すことを期待します。正しい?

C ++プログラムでは、コレクションのメソッドが仮想メソッドとして定義されていない場合、これが機能する可能性があります。ただし、これはC++の設計目標の成果物です。

他のほぼすべてのオブジェクト指向言語では、すべてのメソッドが動的にディスパッチされます。変数の型ではなく、実際のオブジェクトの型を使用します。

それがあなたが疑問に思っていることではない場合は、リスコフの置換原則を読んで、それを破っているかどうかを自問してください。多くのクラス階層がそうします。

于 2009-09-28T14:18:03.493 に答える
1

例は「医者、私がこれをするときそれは痛い」カテゴリーに分類されます。はい、サブクラスはさまざまな方法でスーパークラスを壊すことができます。いいえ、それを防ぐための簡単な防水ソリューションはありません。

言語がこれをサポートしている場合は、スーパークラスを封印する(すべてを作成するfinal)ことができますが、柔軟性が失われます。これは悪い種類の防御プログラミングです(良いものは堅牢なコードに依存し、悪いものは強い制限に依存します)。

あなたができる最善のことは、人間レベルで行動することです-サブクラスを書く人間がスーパークラスを理解していることを確認してください。個別指導/コードレビュー、優れたドキュメント、単体テスト(おおよそこの重要度の順序で)は、これを達成するのに役立ちます。そしてもちろん、基本クラスを防御的にコーディングしても問題はありません。

于 2009-09-28T15:44:56.657 に答える
1

あなたが説明したのは、ポリモーフィズムの癖です。サブクラスのインスタンスを親クラスのインスタンスとしてアドレス指定できるため、カバーの下にどのような実装があるのか​​わからない場合があります。

私はあなたの解決策は非常に単純だと思います:

  1. コレクションを変更せず、ユーザーがコレクションからフェッチするときにのみフィルターを適用すると述べました。したがって、countメソッドをオーバーライドしないでください。これらの要素はすべてコレクションに含まれているため、呼び出し元に嘘をつくことはありません。
  2. base .countメソッドを正常に動作させたいが、それでもcountが必要なので、getFilteredCountフィルタリング後に要素の量を返すメソッドを実装する必要があります。

サブクラス化とは、「種類」の関係に関するものです。あなたがしていることは、標準から外れているわけではありませんが、最も標準的なユースケースでもありません。コレクションにフィルターを適用しているので、「FilteredCollection」は「一種の」コレクションであると主張できますが、実際にはコレクションを変更しているわけではありません。フィルタリングを簡素化するレイヤーでラップしているだけです。いずれにせよ、これはうまくいくはずです。唯一の欠点は、.getCountではなく'getFilteredCount'を呼び出すことを忘れないでください。

于 2009-09-28T14:12:47.063 に答える
0

スーパークラスは、少なくともあなたが望む方法では、サブクラス化のためにうまく設計されていないと主張することができます。スーパークラスが「Count()」や「Next()」などを呼び出す場合、その呼び出しをオーバーライドする必要はありません。C ++では、「仮想」と宣言されていない限りオーバーライドできませんが、すべての言語に適用されるわけではありません。たとえば、正しく覚えていれば、Obj-Cは本質的に仮想です。

さらに悪いことに、スーパークラスのメソッドをオーバーライドしなくても、この問題が発生する可能性があります。サブタイピングとサブクラス化を参照してください。特に、その記事のOOP問題のリファレンスを参照してください。

于 2009-09-28T14:21:23.227 に答える
0

これがオブジェクト指向プログラミングが機能することになっている方法であるため、このように動作します。

OOPの要点は、サブクラスがそのスーパークラスメソッドの一部を再定義できることであり、スーパークラスレベルで実行される操作によってサブクラスの実装が取得されます。

あなたの例をもう少し具体的にしましょう。犬、猫、ライオン、バジリスクを含む「コレクション動物」を作成します。次に、ライオンとバジリスクを除外するFilteredCollectiondomesticAnimalを作成します。したがって、domesticAnimalを反復処理すると、犬と猫だけが表示されると予想されます。メンバー数のカウントをお願いした場合、結果が「2」になるとは思いませんか?オブジェクトにメンバーの数を尋ねて「4」と表示した後、それらをリストするように要求すると、2つしかリストされなかった場合は確かに奇妙な動作になります。

オーバーライドをスーパークラスレベルで機能させることは、OOPの重要な機能です。これにより、たとえば、Collectionオブジェクトをパラメーターとして受け取り、その下にあるのが本当に「純粋な」コレクションであるかFilteredCollectionであるかを知らなくても、それを操作する関数を定義できます。すべてがどちらの方法でも機能するはずです。純粋なコレクションの場合は、純粋なコレクション関数を取得します。FilteredCollectionの場合は、FilteredCollection関数を取得します。

カウントが他の目的でも内部的に使用されている場合(新しい要素をどこに配置するかを決定するなど、実際には5番目の要素を追加して、#3を不思議に上書きする場合)、クラスの設計に問題があります。OOPは、クラスの動作に大きな力を与えますが、大きな力には大きな責任が伴います。:-)関数が2つの異なる目的で使用され、目的#1の要件を満たすために実装をオーバーライドする場合、それが目的#2に違反しないことを確認するのはあなた次第です。

于 2009-09-29T14:25:32.723 に答える
0

あなたの投稿に対する私の最初の反応は、「すべてのアクセサー」をオーバーライドすることについての言及でした。これは私がよく見たものです。基本クラスを拡張してから、ほとんどの基本クラスメソッドをオーバーライドします。これは私の意見では継承の目的を打ち負かします。ほとんどの基本クラス関数をオーバーライドする必要がある場合は、クラスを拡張する理由を再検討する必要があります。前に述べたように、インターフェースは異種のオブジェクトを緩く結合するため、より良い解決策になる可能性があります。サブクラスは、完全に書き直すのではなく、基本クラスの機能を拡張する必要があります。基本クラスのメンバーをオーバーライドしている場合、予期しない動作が発生するのは非常に論理的であるように思われます。

于 2009-10-10T21:21:07.480 に答える
0

継承がどのように機能するかを最初に理解したとき、私はそれをよく使用しました。私はこれらの大きな木を持っていて、すべてが何らかの形で接続されていました。

なんて痛い。

必要なものについては、オブジェクトを拡張するのではなく、参照する必要があります。

また、パブリックAPI(および一般的にはプライベートAPI)からコレクションを渡す痕跡を個人的に非表示にします。コレクションを安全にすることは不可能です。WordCountクラスまたはUsersWithAgesクラスまたはAnimalsAndFootCountクラス内でコレクションをラップすること(さあ、それは何のために使用されますか?署名から推測できますよね?)は、はるかに理にかなっています。

また、wordCount.getMostUsedWord()、usersWithAges.getUsersOverEighteen()、animalsAndFootCount.getBipeds()メソッドなどのメソッドを使用すると、コード全体に散在する反復的なユーティリティ機能が、コードが属する新しいビジネスコレクションに移動します。

于 2009-12-04T00:45:16.630 に答える