List <T>を継承しないのはなぜですか?

2014-02-11 c# .net list oop inheritance

私のプログラムを計画するとき、私はしばしば次のような一連の思考から始めます。

サッカーチームは、サッカー選手のリストにすぎません。したがって、私はそれを次のように表現する必要があります:

var football_team = new List<FootballPlayer>();

このリストの順序は、プレーヤーが名簿にリストされている順序を表しています。

しかし、後で気付くのは、チームには、単なるプレーヤーのリストの他に、記録する必要のある他のプロパティがあることです。たとえば、今シーズンのスコアの累計、現在の予算、均一の色、チームの名前を表すstringなど。

だから私は思う:

さて、サッカーチームは選手のリストのようなものですが、それに加えて、名前( string )と現在の合計スコア( int )を持っています。 .NETはサッカーチームを格納するためのクラスを提供していないため、自分でクラスを作成します。最も類似し、関連する既存の構造はList<FootballPlayer>なので、継承します。

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

しかし、ガイドラインでは、 List<T>から継承してはならないことがわかっています 。このガイドラインは2つの点で完全に混乱しています。

何故なの?

どうやらListはパフォーマンスのためにどういうわけか最適化されています 。どうして? Listを拡張すると、どのようなパフォーマンスの問題が発生しますか?正確には何が壊れますか?

私が見たもう1つの理由は、 ListがMicrosoftから提供されており、私がそれを制御できないため、後で「公開API」を公開した後で変更できないことです。しかし、私はこれを理解するのに苦労しています。パブリックAPIとは何ですか。なぜ気にする必要があるのですか。現在のプロジェクトにこのパブリックAPIが含まれていない可能性が高い場合、このガイドラインを無視しても安全ですか? List 継承していて、パブリックAPIが必要であることが判明した場合、どのような問題が発生しますか?

なぜそれが重要なのですか?リストはリストです。何が変わる可能性がありますか?何を変更したいですか?

そして最後に、MicrosoftがListからの継承を望まないのなら、なぜ彼らはクラスをsealedしなかっsealedですか?

他に何を使うべきですか?

どうやら、カスタムコレクションの場合、MicrosoftはList代わりに拡張する必要があるCollectionクラスを提供しています。しかし、このクラスは非常に裸であり、 たとえばAddRangeなどの多くの有用なものはありません。 jvitor83の答えは、その特定のメソッドのパフォーマンスの根拠を提供しますが、遅いAddRangeないよりも優れているわけではありませAddRange

Collectionからの継承は、 Listからの継承よりもはるかに多くの作業であり、利点がないと思います。確かに、Microsoftは理由もなく余分な作業をするように言わないので、何かを誤解しているように感じることはできず、 Collectionを継承することは実際には私の問題の正しい解決策ではありません。

IList実装などの提案を見てきました。いいえ。これは何十行ものボイラープレートコードで、何も得られません。

最後に、 Listを何かでラップすることを提案する人もいます:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

これには2つの問題があります。

  1. それは私のコードを不必要に冗長にします。ここで、 my_team.Countだけでなく、 my_team.Players.Countを呼び出す必要があります。ありがたいことに、C#を使用すると、インデクサーを定義してインデックス作成を透過的にし、内部Listすべてのメソッドを転送できます。すべての作業で何が得られますか?

  2. それだけでは意味がありません。サッカーチームは、選手のリストを「持っていません」。それは選手のリストです 。 「John McFootballerがSomeTeamのプレーヤーに参加した」とは言わないでください。あなたは「ジョンがSomeTeamに参加した」と言います。 「文字列の文字」に文字を追加するのではなく、文字列に文字を追加します。図書館の本に本を追加するのではなく、本を図書館に追加します。

「内部で」起こることは「XをYの内部リストに追加する」と言うことができることを私は理解していますが、これは世界について非常に直感に反する方法のように思えます。

私の質問(要約)

データ構造を表す正しいC#の方法は何ですか?これは、「論理的に」(つまり、「人間の心に」)は、ほんのわずかthings listです。

List<T>からの継承は常に受け入れられませんか?それはいつ受け入れられますか?なぜ/なぜしないのですか? List<T>から継承するかどうかを決定するとき、プログラマは何を考慮しなければなりませんか?

Answers

まず第一に、それはユーザビリティと関係があります。継承を使用する場合、 Teamクラスは、純粋にオブジェクト操作のために設計された動作(メソッド)を公開します。たとえば、 AsReadOnly()メソッドやCopyTo(obj)メソッドは、チームオブジェクトには意味がありません。 AddRange(items)メソッドの代わりに、より説明的なAddPlayers(players)メソッドがおそらく必要です。

LINQを使用する場合は、 ICollection<T>IEnumerable<T>などの汎用インターフェイスを実装する方が理にかなっています。

述べたように、構成はそれについて取り組む正しい方法です。プレーヤーのリストをプライベート変数として実装するだけです。

うわー、あなたの投稿にはたくさんの質問とポイントがあります。 Microsoftから得られる推論のほとんどは、まさにその通りです。 List<T>すべてから始めましょう

  • List<T> 高度に最適化されています。その主な用途は、オブジェクトのプライベートメンバーとして使用することです。
  • class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }名前のクラスを作成したい場合があるため、Microsoftは封印しませんでした。 class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... } 。これで、 var list = new MyList<int, string>();を実行するのと同じくらい簡単var list = new MyList<int, string>();
  • CA1002:一般的なリストを公開しないでください :基本的に、このアプリを単独の開発者として使用することを計画している場合でも、優れたコーディング手法で開発することは価値があるため、それらはあなたと第二の性質に浸透します。インデックス付きリストを持つコンシューマーが必要な場合でも、リストをIList<T>として公開できIList<T> 。これにより、後でクラス内の実装を変更できます。
  • MicrosoftはCollection<T>非常に一般的なものにしました。それは一般的な概念であるためです...名前はすべてを表しています。それは単なるコレクションです。 SortedCollection<T>ObservableCollection<T>ReadOnlyCollection<T>などのより正確なバージョンがあり、それぞれIList<T>を実装していIList<T> List<T> 実装していません
  • Collection<T>では、メンバー(つまり、追加、削除など)が仮想であるため、それらをオーバーライドできます。 List<T>はありません。
  • あなたの質問の最後の部分はスポットです。フットボールチームは単なる選手のリストではないので、その選手のリストを含むクラスでなければなりません。 構成と継承を考える。フットボールチームに選手のリスト(名簿)があり、それは選手のリストではありません。

このコードを書いている場合、クラスはおそらく次のようになります。

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

ここにいくつかの良い答えがあります。それらに以下の点を付け加えたいと思います。

データ構造を表す正しいC#の方法は何ですか?これは、「論理的に」(つまり、「人間の心に」)は、ほんのわずかなもののリストです。

コンピュータープログラマーではない、サッカーの存在に詳しい10人に空欄に記入してもらいます。

サッカーチームは特定の種類の_____です

だれかが「いくつかの鈴と笛のあるフットボール選手のリスト」と言ったのでしょうか、それとも「スポーツチーム」、「クラブ」、「組織」とすべて言ったのですか。サッカーチームが特定の種類の選手のリストであるというあなたの考えは、あなたの人間の心とあなたの人間の心だけにあります。

List<T>メカニズムです。フットボールチームはビジネスオブジェクトです。つまり、プログラムのビジネスドメインにある概念を表すオブジェクトです。それらを混ぜないでください!サッカーチームは一種のチームです。それは名簿は選手のリストである 、名簿を持っています 。名簿は特定の種類のプレーヤーのリストではありません。名簿 、プレーヤーのリストです。したがって、 List<Player>あるRosterというプロパティを作成します。また、フットボールチームについて知っているすべての人が選手を名簿から削除できると信じていない限り、それを読んでいる間にReadOnlyList<Player>してください。

List<T>からの継承は常に受け入れられませんか?

誰にも受け入れられない?私?番号。

それはいつ受け入れられますか?

List<T>メカニズム拡張するメカニズムを構築する場合。

List<T>から継承するかどうかを決定するとき、プログラマは何を考慮しなければなりませんか?

メカニズムまたはビジネスオブジェクトを構築していますか?

しかし、それはたくさんのコードです!すべての作業で何が得られますか?

あなたは、 List<T>関連するメンバーの転送メソッドを50回以上書くのに時間がかかるだろうという質問の入力により多くの時間を費やしました。あなたは明らかに冗長性を恐れていません。ここでは、ごく少量のコードについて話しています。これは数分の作業です。

更新

もう少し考えましたが、サッカーチームを選手のリストとしてモデル化しない別の理由があります。実際、あまりにも選手のリストを持つものとしてサッカーチームをモデル化するために、悪い考えかもしれません。プレーヤーのリストを持っている/持っているチームの問題は、あなたが手にしているのは、 ある瞬間のチームのスナップショットであるということです。このクラスのビジネスケースはわかりませんが、サッカーチームを代表するクラスがあったら、「2003年から2013年の間に怪我のために何人のシーホークスプレーヤーがゲームに参加できなかったのか」という質問をしたいと思います。または「以前に別のチームでプレーしたデンバーのプレーヤーが、前年比でヤードの増加が最も大きかったのは何ですか?」または「 今年、ピガーズはずっと行きましたか?

つまり、フットボールチームは、プレーヤーが採用されたとき、負傷したとき、引退したときなどの歴史的事実のコレクションとしてよくモデル化されているようです。明らかに、現在のプレーヤー名簿は、おそらく前向きであるべき重要な事実です。中心ですが、より歴史的な視点を必要とするこのオブジェクトで実行したい他の興味深い事柄があるかもしれません。

状況による

チームをプレーヤーのリストと見なす場合、フットボールチームの「アイデア」を1つの側面に投影します。「チーム」をフィールドに表示される人に限定します。この予測は特定の状況でのみ正しいものです。別のコンテキストでは、これは完全に間違っている可能性があります。チームのスポンサーになりたいとします。したがって、チームのマネージャーと話し合う必要があります。このコンテキストでは、チームはそのマネージャーのリストに投影されます。また、これら2つのリストは通常​​、あまり重複していません。他のコンテキストは、現在のプレーヤーと以前のプレーヤーなどです。

不明確なセマンティクス

したがって、チームをプレーヤーのリストと見なすことの問題は、そのセマンティクスがコンテキストに依存し、コンテキストが変化したときに拡張できないことです。さらに、どのコンテキストを使用しているかを表現することは困難です。

クラスは拡張可能です

メンバーが1つだけのクラス(例: IList activePlayers )を使用する場合、メンバーの名前(およびそのコメント)を使用してコンテキストを明確にすることができます。追加のコンテキストがある場合は、追加のメンバーを追加するだけです。

クラスはより複雑です

場合によっては、追加のクラスを作成するのはやり過ぎかもしれません。各クラス定義はクラスローダーを介してロードする必要があり、仮想マシンによってキャッシュされます。これにより、実行時のパフォーマンスとメモリが消費されます。非常に具体的なコンテキストがある場合は、サッカーチームを選手のリストと見なしても問題ありません。ただし、この場合は、 IListから派生したクラスではなく、 IList実際に使用する必要がありIList

結論/考慮事項

非常に具体的なコンテキストがある場合は、チームをプレーヤーのリストと見なしても問題ありません。たとえば、メソッド内では、次のように記述してもまったく問題ありません。

IList<Player> footballTeam = ...

F#を使用する場合、型の省略形を作成しても問題ありません。

type FootballTeam = IList<Player>

ただし、コンテキストがより広い場合や不明確な場合は、これを行うべきではありません。これは特に、将来使用される可能性のあるコンテキストが明確でない新しいクラスを作成する場合に当てはまります。警告サインは、クラスに追加の属性(チームの名前、コーチなど)を追加し始めたときです。これは、クラスが使用されるコンテキストが固定されておらず、将来変更されることを示す明確な兆候です。この場合、チームをプレーヤーのリストと見なすことはできませんが、(現在アクティブで、負傷していないなど)プレーヤーのリストをチームの属性としてモデル化する必要があります。

これは、 構成継承の典型的な例です。

この特定のケースでは:

チームは行動が追加されたプレーヤーのリストですか

または

チームは、たまたまプレーヤーのリストを含む独自のオブジェクトですか?

リストを拡張することにより、いくつかの方法で制限されます。

  1. アクセスを制限することはできません(たとえば、名簿の変更を禁止します)。すべてを必要とするかどうかに関係なく、すべてのListメソッドを取得します。

  2. 他のもののリストも必要な場合はどうなりますか。たとえば、チームにはコーチ、マネージャー、ファン、設備などがあります。それらのいくつかは、それ自体がリストである場合があります。

  3. 継承のオプションを制限します。たとえば、汎用のTeamオブジェクトを作成し、それを継承するBaseballTeam、FootballTeamなどを作成することができます。リストから継承するには、チームから継承を行う必要がありますが、これは、さまざまなタイプのチームすべてが、その名簿の同じ実装を強制されることを意味します。

構成-オブジェクト内で必要な動作を提供するオブジェクトを含みます。

継承-オブジェクトは、必要な動作を持つオブジェクトのインスタンスになります。

どちらにも用途がありますが、これは合成が望ましい明確なケースです。

これは、「ある」と「ある」のト​​レードオフを思い出させます。時には、スーパークラスから直接継承する方が簡単でより理にかなっています。また、スタンドアロンクラスを作成し、メンバー変数として継承したクラスを含める方が理にかなっている場合もあります。クラスの機能には引き続きアクセスできますが、クラスからの継承に起因する可能性のあるインターフェイスやその他の制約にバインドされていません。

あなたはどちらをしますか?多くのことと同じように...それはコンテキストに依存します。私が使用するガイドは、別のクラスから継承するために「is a」関係が本当にあるべきだということです。つまり、BMWというクラスを書いている場合、BMWは本当に車なので、Carから継承することができます。馬は実際には哺乳類であり、哺乳類の機能は馬に関連しているため、馬クラスは哺乳類クラスから継承できます。しかし、チームはリストだと言えるでしょうか?私が知ることができることから、それはチームが本当に「である」リストのようではないようです。したがって、この場合、メンバー変数としてリストを作成します。

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

以前のコードが意味することは、サッカーをしている通りから来たたくさんの男たちで、彼らはたまたま名前を持っています。何かのようなもの:

サッカーをする人

とにかく、このコード(私の答えから)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

意味:これは、経営陣、選手、管理者などがいるサッカーチームです。

マンチェスターユナイテッドチームのメンバー(およびその他の属性)を示す画像

これがあなたの論理を写真で表現する方法です…

サッカーチーム 、サッカー選手のリストではありません 。サッカーチーム 、サッカー選手のリストで構成されています。

これは論理的に間違っています:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

そしてこれは正しいです:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}

「チーム」オブジェクトの動作に依存します。コレクションと同じように動作する場合は、最初にプレーンリストで表すこともできます。次に、リストを反復するコードを複製し続けることに気付くかもしれません。この時点で、プレーヤーのリストをラップするFootballTeamオブジェクトを作成するオプションがあります。 FootballTeamクラスは、プレーヤーのリストを反復処理するすべてのコードのホームになります。

それは私のコードを不必要に冗長にします。 my_team.Countではなく、my_team.Players.Countを呼び出す必要があります。ありがたいことに、C#を使用すると、インデクサーを定義してインデックス作成を透過的にし、内部リストのすべてのメソッドを転送できます...しかし、それは多くのコードです!すべての作業で何が得られますか?

カプセル化。あなたのクライアントはFootballTeamの内部で何が起こっているかを知る必要はありません。すべてのクライアントが知っているように、データベースでプレイヤーのリストを検索することで実装できます。彼らは知る必要はありません、そしてこれはあなたのデザインを改善します。

それだけでは意味がありません。サッカーチームは、選手のリストを「持っていません」。選手一覧です。 「John McFootballerがSomeTeamのプレーヤーに参加した」とは言わないでください。あなたは「ジョンがSomeTeamに参加した」と言います。 「文字列の文字」に文字を追加するのではなく、文字列に文字を追加します。図書館の本に本を追加するのではなく、本を図書館に追加します。

正確には:) FootballTeam.List.Add(john)ではなく、footballTeam.Add(john)と言います。内部リストは表示されません。

皆が指摘したように、選手のチームは選手のリストではありません。この間違いは、多くの人々によって、おそらくさまざまなレベルの専門知識で行われています。この場合のように、多くの場合、問題は微妙であり、非常に重大です。これらのデザインは、 リスコフの代替原則に違反しているため、 不適切です。インターネットには、この概念を説明する良い記事がたくさんあります。例えば、 http://en.wikipedia.org/wiki/Liskov_substitution_principle

要約すると、クラス間の親子関係で保持される2つのルールがあります。

  • 子供は、完全に親を定義するものよりも少ない特性を必要とするべきではありません。
  • 親は、子を完全に定義するものに加えて、特性を必要としません。

つまり、親は子の必要な定義であり、子は親の十分な定義です。

ここでは、1つの解決策を検討し、上記の原則を適用して、そのような間違いを回避するのに役立つ方法を示します。親クラスのすべての操作が構造的にも意味的にも派生クラスに対して有効であるかどうかを検証することによって、仮説をテストする必要があります。

  • サッカーチームはサッカー選手のリストですか? (リストのすべてのプロパティが同じ意味でチームに適用されますか)
    • チームは同種のエンティティのコレクションですか?はい、チームはプレイヤーの集まりです
    • プレーヤーを含める順序はチームの状態を説明していますか?チームは、明示的に変更されない限り、シーケンスが確実に保持されるようにしますか?いいえ、いいえ
    • プレーヤーは、チーム内での順番に基づいて追加/削除されると予想されますか?番号

ご覧のとおり、リストの最初の特性のみがチームに適用されます。したがって、チームはリストではありません。リストは、チームの管理方法の実装の詳細になるため、プレーヤーオブジェクトを格納し、Teamクラスのメソッドで操作するためにのみ使用する必要があります。

この時点で、私の意見では、TeamクラスはListを使用して実装するべきではないことに注意したいと思います。ほとんどの場合、Setデータ構造(HashSetなど)を使用して実装する必要があります。

データ構造を表す正しい C#の方法は何ですか...

「すべてのモデルは間違っていますが、一部は便利です。」 - ジョージEPボックス

「正しい方法」はなく、有用な方法だけです。

あなたやユーザーに役立つものを選択してください。それでおしまい。 経済的に開発し、過剰設計しないでください。記述するコードが少ないほど、デバッグする必要のあるコードが少なくなります。 (次の版を読んでください)。

-編集済み

私の最良の答えは...それは場合によります。 Listから継承すると、このクラスのクライアントは公開すべきではないメソッドに公開されます。これは、主にFootballTeamがビジネスエンティティのように見えるためです。

-エディション2

私は「エンジニアをやりすぎないでください」というコメントで私が言及していたことを心から思い出しません。 KISSの考え方は良いガイドだと思いますが、Listからビジネスクラスを継承すると、 抽象化のリークにより、解決するよりも多くの問題が発生することを強調したいと思います。

一方、Listから継承するだけで便利なケースは限られていると思います。私が前の版で書いたように、それは依存します。それぞれのケースに対する答えは、知識、経験、個人的な好みの両方に大きく影響されます。

回答についてより正確に考える手助けをしてくれた@kaiに感謝します。

人々が言うことを可能にしますか

myTeam.subList(3, 5);

何か意味がありますか?そうでない場合、それはリストであってはなりません。

設計>実装

どのメソッドとプロパティを公開するかは設計上の決定です。継承する基本クラスは実装の詳細です。前者に戻る価値があると思います。

オブジェクトは、データと動作のコレクションです。

したがって、最初の質問は次のようになります。

  • このオブジェクトは、作成しているモデルでどのようなデータを構成していますか?
  • このオブジェクトはそのモデルでどのような動作をしますか?
  • これは将来どのように変化するでしょうか?

継承は「isa」(is a)関係を意味するのに対し、構成は「has a」(hasa)関係を意味することに注意してください。アプリケーションの進化に伴って状況が変わる可能性があることを念頭に置いて、状況に応じて適切なものを選択してください。

具象型を考える前に、インターフェイスについて考えることを検討してください。そうすれば、脳を「デザインモード」に置く方が簡単だと感じる人もいます。

これは、日常のコーディングで誰もがこのレベルで意識的に行うことではありません。しかし、この種のトピックを熟考している場合は、設計の水面に足を踏み入れていることになります。それを意識することは解放することができます。

設計の詳細を検討する

MSDNまたはVisual StudioのList <T>およびIList <T>をご覧ください。それらが公開するメソッドとプロパティを確認します。これらのメソッドはすべて、誰かがあなたの見方でFootballTeamにしたいもののように見えますか?

footballTeam.Reverse()はあなたにとって意味がありますか? footballTeam.ConvertAll <TOutput>()はあなたが望むもののように見えますか?

これは簡単な質問ではありません。答えは本当に「はい」かもしれません。 List <Player>またはIList <Player>を実装または継承する場合は、それらにこだわっています。それがモデルに理想的である場合は、それを実行してください。

はいと判断した場合、それは理にかなっており、オブジェクトをプレーヤーのコレクション/リスト(動作)として処理できるようにしたいので、ICollectionまたはIListを実装する必要があります。概念的に:

class FootballTeam : ... ICollection<Player>
{
    ...
}

オブジェクトにプレーヤー(データ)のコレクション/リストを含め、したがってコレクションまたはリストをプロパティまたはメンバーにしたい場合は、必ずそうしてください。概念的に:

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

プレーヤーをカウントしたり、追加したり、削除したりするのではなく、プレーヤーのセットのみを列挙できるようにしたいと思うかもしれません。 IEnumerable <Player>は、検討すべき完全に有効なオプションです。

これらのインターフェースはいずれも、モデルではまったく役に立たないと感じるかもしれません。これは可能性は低いですが(IEnumerable <T>は多くの状況で役立ちます)、それでも可能です。

これらのいずれかがすべての場合に断定的かつ決定的に間違っているとあなたに伝えようとする人は誰でも誤解されます。あなたにそれをあなたに伝えようとする人は、すべての場合に断固としてそして決定的に正しいです。

実装に移る

データと動作を決定したら、実装について決定できます。これには、継承または構成を介して依存する具象クラスが含まれます。

これは大きなステップではない可能性があり、多くの場合、設計と実装を混同します。1秒か2秒ですべてを頭の中で実行して、すぐに入力し始めることができるからです。

思考実験

人為的な例:他の人が述べたように、チームは必ずしもプレーヤーの集合体の「単なる」ものではありません。チームの試合スコアのコレクションを維持していますか?あなたのモデルでは、チームはクラブと交換可能ですか?もしそうなら、そしてあなたのチームが選手の集まりであれば、おそらくそれはスタッフの集まりやスコアの集まりでもあります。その後、次のようになります。

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

設計にもかかわらず、C#ではこの時点では、List <T>から継承してこれらすべてを実装することはできません。C#は単一の継承のみをサポートしているためです。 (C ++でこの問題を試したことがある場合、これは良いことだと思うかもしれません。)継承を介して1つのコレクションを実装し、コンポジションを介して1つのコレクションを実装することは、汚いと感じる可能性があります。また、Countなどのプロパティは、ILIst <Player> .CountやIList <StaffMember> .Countなどを明示的に実装しない限り、ユーザーにとって混乱を招き、混乱を招くのではなく、苦痛を伴うだけです。これがどこに向かっているかがわかります。この道を考えているときの直感は、この方向に向かうのは間違っていると感じるかもしれません(そして、あなたがこのようにそれを実装した場合、同僚も正しいか間違っているかもしれません!)

短い答え(遅すぎる)

コレクションクラスから継承しないことに関するガイドラインはC#固有ではなく、多くのプログラミング言語で見られます。法律ではなく知恵を受けています。その理由の1つは、実際には、構成がわかりやすさ、実装可能性、保守性の点で継承に勝つと考えられているためです。抽象的、特に時間の経過や、コード内のオブジェクトの正確なデータや動作などを深く理解しない限り、実用的で一貫性のある「isa」関係よりも、実用的で一貫性のある「hasa」関係を見つけることは、現実世界のドメインオブジェクトでより一般的です。変更。これにより、コレクションクラスからの継承が常に除外されるわけではありません。しかしそれは示唆的かもしれません。

FootballTeamメインチームと共に予備チームがある場合はどうなりますか?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

それをどのようにモデル化しますか?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

関係は明らかにされていないです

またはRetiredPlayers

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

経験則として、コレクションから継承したい場合は、クラスにSomethingCollectionという名前を付けます。

あなたのSomethingCollection意味的に意味がありますか?これは、タイプ Somethingコレクションである場合にのみ行ってください。

FootballTeamの場合、それは正しく聞こえません。 TeamCollectionはありません。他の回答で指摘されているように、 Teamはコーチやトレーナーなどを配置できます。

FootballCollectionは、サッカーのコレクションのように聞こえます。あるいは、サッカー用品のコレクションのように聞こえます。 TeamCollection 、チームのコレクション。

FootballPlayerCollectionは、プレーヤーのコレクションのように聞こえます。これは、本当に必要な場合にList<FootballPlayer>から継承するクラスの有効な名前になります。

Really List<FootballPlayer>は、処理するのに最適なタイプです。メソッドから返す場合は、おそらくIList<FootballPlayer>です。

要約すれば

自問してみてください

  1. である X Y ?またはHAS X Y

  2. 私のクラス名はそれらが何を意味するのですか?

私の汚い秘密:私は人々が何を言うか気にしません、そして私はそれをします。 .NET Frameworkは「XxxxCollection」(頭の上の例のUIElementCollection)で広がっています。

だから私が言うのを止めるのは:

team.Players.ByName("Nicolas")

私はそれがより良いと思うとき

team.ByName("Nicolas")

さらに、私のPlayerCollectionは、コードの重複なしに「Club」などの他のクラスで使用される可能性があります。

club.Players.ByName("Nicolas")

昨日のベストプラクティスは、明日のベストプラクティスではないかもしれません。ほとんどのベストプラクティスの背後に理由はありません。ほとんどはコミュニティ間の幅広い合意にすぎません。自問自答したときに自分のせいになるかどうかをコミュニティに尋ねるのではなく、より読みやすく保守しやすいものは何ですか。

team.Players.ByName("Nicolas")

または

team.ByName("Nicolas")

本当に。疑問がありますか?次に、実際のユースケースでList <T>を使用できないようにする他の技術的な制約を試してみる必要があるかもしれません。ただし、存在してはならない制約を追加しないでください。マイクロソフトが理由を文書化しなかった場合、それは確かにどこからともなく「ベストプラクティス」です。

ここには多くの優れた答えがありますが、私が言及しなかったものに触れたいと思います 。オブジェクト指向設計は、 オブジェクトに力を与えることです

すべてのルール、追加の作業、内部の詳細を適切なオブジェクト内にカプセル化する必要があります。このようにして、このオブジェクトと相互作用する他のオブジェクトは、すべてについて心配する必要はありません。実際、さらに一歩進んで、他のオブジェクトがこれらの内部をバイパスするのを積極的に防止したいと考えています。

Listから継承すると、他のすべてのオブジェクトはあなたをListとして認識できます。プレイヤーを追加および削除するためのメソッドに直接アクセスできます。そして、あなたはコントロールを失います。例えば:

プレーヤーが退職したか、辞任したか、解雇されたかを知ることで、プレーヤーがいつ去ったかを区別したいとします。適切な入力列挙型を取るRemovePlayerメソッドを実装できます。ただし、 Listから継承RemoveRemoveRemoveAll 、さらにはClearへの直接アクセスを防ぐことができなくなりClear 。その結果、実際にはあなたのFootballTeamクラスの権限がなくなりました。


カプセル化に関する追加の考え...次の懸念を提起しました。

それは私のコードを不必要に冗長にします。 my_team.Countではなく、my_team.Players.Countを呼び出す必要があります。

正解です。すべてのクライアントがチームを使用するのは不必要に冗長です。ただし、その問題は、 List Playersをすべての人に公開して雑貨を提供しているため、同意なしにチームをいじることができるのに比べると、非常に小さな問題です。

あなたは続けて言う:

それだけでは意味がありません。サッカーチームは、選手のリストを「持っていません」。選手一覧です。 「John McFootballerがSomeTeamのプレーヤーに参加した」とは言わないでください。あなたは「ジョンがSomeTeamに参加した」と言います。

あなたは最初のビットについて間違っています:「リスト」という単語を削除してください、そして実際にはチームにプレイヤーがいることは明らかです。
しかし、あなたは秒で頭に釘を打ちました。クライアントがateam.Players.Add(...)呼び出すことはateam.Players.Add(...)ありません。あなたはそれらがateam.AddPlayer(...)呼び出すことをateam.AddPlayer(...) 。そして、あなたの実装は(おそらく)とりわけ内部でPlayers.Add(...)呼び出します。


うまくいけば、オブジェクトのエンパワーメントの目的にとってカプセル化がいかに重要であるかを理解できます。各クラスが他のオブジェクトからの干渉を恐れずにうまく機能できるようにする必要があります。

List<T>が「最適化されている」と彼らが言ったとき、私はそれが少し高価な仮想メソッドのような機能を持っていないことを意味したいと思います。したがって、問題は、 パブリックAPIList<T>公開すると 、後でビジネスルールを適用したり、その機能をカスタマイズしたりすることができなくなることです。しかし、この継承されたクラスをプロジェクト内で(APIとして何千もの顧客/パートナー/他のチームに公開される可能性があるのとは対照的に)使用している場合は、時間を節約し、目的の機能である場合は問題ない可能性があります。複製。 List<T>から継承することの利点は、予見可能な将来にカスタマイズされることのない多くのダムラッパーコードを排除できることです。また、APIの有効期間中、クラスにList<T>とまったく同じセマンティクスを明示的に持たせたい場合も、それで問題ありません。

FxCopルールがそう言っている、または誰かのブログがそれが「悪い」習慣だと言っているからといって、多くの人が大量の余分な作業をしているのをよく見ます。多くの場合、これはコードをデザインパターンpaloozaの奇妙さに変えます。多くのガイドラインと同様に、例外を含む可能性のあるガイドラインとして扱います。

質問を書き直させてください。だからあなたは別の視点から主題を見るかもしれません。

サッカーチームを代表する必要があるとき、それは基本的に名前であると理解しています。のような:「イーグルス」

string team = new string();

その後、私はチームにもプレイヤーがいることに気づきました。

文字列型を拡張して、プレーヤーのリストも保持できないのはなぜですか?

問題への入り口は任意です。それ何であるか、チームは(性質)を持っていないものではないと考えるようにしてください。

その後、他のクラスとプロパティを共有しているかどうかを確認できます。継承について考えます。

クラスのユーザーがすべてのメソッドとプロパティを必要とする場合**リストにある場合は、そこからクラスを派生させる必要があります。それらが必要ない場合は、Listを囲み、クラスユーザーが実際に必要とするメソッドのラッパーを作成します。

これは、多くの人が使用するパブリックAPIやその他のコードを作成する場合の厳格なルールです。アプリが小さく、開発者が2人以下の場合は、このルールを無視できます。これにより、時間を節約できます。

小さなアプリの場合は、厳密性の低い別の言語を選択することも検討してください。 Ruby、JavaScript-より少ないコードを記述できるもの。

ガイドラインにあることは、リスト、セット、ディクショナリ、ツリーなどを使用しているかどうかの内部設計決定を公開APIが明らかにすべきではないということです。 「チーム」は必ずしもリストではありません。リストとして実装することもできますが、パブリックAPIのユーザーは、必要に応じてクラスを使用する必要があります。これにより、パブリックインターフェイスに影響を与えることなく、決定を変更して別のデータ構造を使用できます。

フットボールチームが "is-a" List<FootballPlayer>または "has-a" List<FootballPlayer>のどちらに接するかで、他の回答がかなり外れていると思うからです。

OPは主にList<T>から継承するためのガイドラインの明確化を求めます:

ガイドラインによると、 List<T>から継承してはいけません。何故なの?

List<T>には仮想メソッドがないためです。通常、比較的簡単に実装を切り替えることができますが、パブリックAPIでははるかに大きな問題になる可能性があるため、これは独自のコードの問題にはなりません。

パブリックAPIとは何ですか。なぜ気にする必要があるのですか。

パブリックAPIは、サードパーティのプログラマーに公開するインターフェースです。フレームワークのコードを考えてください。また、参照されているガイドラインは「.NET Framework設計ガイドライン」であり、「。NET アプリケーション設計ガイドライン」ではないことを思い出してください。違いがあり、一般的に言えば、パブリックAPIの設計ははるかに厳密です。

現在のプロジェクトにこのパブリックAPIが含まれていない可能性が高い場合、このガイドラインを無視しても安全ですか? Listを継承していて、パブリックAPIが必要であることが判明した場合、どのような問題が発生しますか?

そうだね。とにかくそれがあなたの状況に当てはまるかどうかを確認するためにその背後にある理論的根拠を検討する必要があるかもしれませんが、パブリックAPIを構築していない場合は、バージョン管理などのAPIの懸念について特に心配する必要はありません(これは、サブセット)。

将来的にパブリックAPIを追加する場合は、APIを実装から抽象化する( List<T>直接公開しないことにより)か、将来発生する可能性のある苦痛を伴うガイドラインに違反する必要があります。

なぜそれが重要なのですか?リストはリストです。何が変わる可能性がありますか?何を変更したいですか?

コンテキストに依存しますが、例としてFootballTeamを使用しているため、チームが給与の上限を超える場合にFootballPlayer追加できないことを想像してください。それを追加する可能な方法は次のようなものです:

 class FootballTeam : List<FootballPlayer> {
     override void Add(FootballPlayer player) {
        if (this.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
 }

ああ...しかし、(パフォーマンス上の理由から) override Addvirtualはないため、 override Addできません。

アプリケーションを使用している場合(基本的には、自分とすべての呼び出し元が一緒にコンパイルされることを意味します)、 IList<T>を使用するように変更して、コンパイルエラーを修正できます。

 class FootballTeam : IList<FootballPlayer> {
     private List<FootballPlayer> Players { get; set; }

     override void Add(FootballPlayer player) {
        if (this.Players.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
     /* boiler plate for rest of IList */
 }

しかし、サードパーティに公開している場合は、コンパイルエラーやランタイムエラーを引き起こす重大な変更を加えただけです。

TL; DR-ガイドラインはパブリックAPI用です。プライベートAPIの場合は、好きなようにします。

私は、エッフェルの発明者であり、契約により設計されているBertrand MeyerがTeam List<Player>を継承させ、まぶたを打たないようにしました。

彼の著書であるObject-Oriented Software Constructionでは、長方形のウィンドウが子ウィンドウを持つことができるGUIシステムの実装について説明しています。彼は単純にWindow RectangleTree<Window>両方を継承させ、実装を再利用しています。

ただし、C#はエッフェルではありません。後者は、機能の多重継承と名前変更をサポートします 。 C#では、サブクラスを作成するときに、インターフェイスと実装の両方を継承します。実装をオーバーライドできますが、呼び出し規約はスーパークラスから直接コピーされます。ただし、Eiffelでは、パブリックメソッドの名前を変更できるため、 Team AddおよびRemove名前をHire and Fireに変更できます。 TeamのインスタンスがList<Player>にアップキャストされる場合、呼び出し元はAddおよびRemoveを使用してそれを変更しますが、仮想メソッドHireおよびFireが呼び出されます。

私はあなたの一般化に同意しないと思います。チームは単なる選手の集まりではありません。チームには、名前、エンブレム、管理/管理スタッフのコレクション、コーチングクルーのコレクション、そして選手のコレクションなど、それに関する多くの情報があります。したがって、適切には、FootballTeamクラスには3つのコレクションがあり、それ自体はコレクションではありません。現実の世界を適切にモデル化する場合。

Specialized StringCollectionと同様に、オブジェクトが内部ストアに追加または内部ストアから削除される前の検証やチェックなど、他のいくつかの機能を提供するPlayerCollectionクラスを検討できます。

おそらく、PlayerCollectionの概念の方が、あなたが好むアプローチに適していますか?

public class PlayerCollection : Collection<Player> 
{ 
}

そして、FootballTeamは次のようになります。

public class FootballTeam 
{ 
    public string Name { get; set; }
    public string Location { get; set; }

    public ManagementCollection Management { get; protected set; } = new ManagementCollection();

    public CoachingCollection CoachingCrew { get; protected set; } = new CoachingCollection();

    public PlayerCollection Players { get; protected set; } = new PlayerCollection();
}

これらの回答のほとんどのように複雑な比較はしていませんが、この状況を処理するための方法を共有したいと思います。 IEnumerable<T>拡張することにより、 List<T>すべてのメソッドとプロパティを公開することなく、 TeamクラスがLinqクエリ拡張をサポートできるようにすることができます。

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}

クラスよりもインターフェースを優先

クラスはクラスからの派生を避け、代わりに必要な最小限のインターフェースを実装する必要があります。

継承はカプセル化を壊します

クラスから派生すると、 カプセル化が解除されます。

  • コレクションの実装方法に関する内部の詳細を公開します
  • 適切でない可能性があるインターフェース(パブリック関数とプロパティのセット)を宣言します

これにより、コードのリファクタリングが困難になります。

クラスは実装の詳細です

クラスは、コードの他の部分から隠すべき実装の詳細です。 要するに、 System.Listは抽象データ型の特定の実装であり、現在および将来において適切な場合とそうでない場合があります。

概念的には、 System.Listデータ型が「リスト」と呼ばれるという事実は、少々難解です。 System.List<T>は、要素を追加、挿入、削除するための償却済みO(1)操作と、要素数を取得する、またはインデックスによって要素を取得および設定するためのO(1)操作をサポートする、変更可能な順序付けされたコレクションです。

インターフェースが小さいほどコードは柔軟になります

データ構造を設計する場合、インターフェースがシンプルになるほど、コードの柔軟性が高まります。これを示すために、LINQがどれほど強力であるかを見てください。

インターフェイスの選択方法

「リスト」を考えるときは、まず「野球選手の集まりを代表する必要があります」と自分に言い聞かせる必要があります。それで、これをクラスでモデル化することにしたとしましょう。最初にすべきことは、このクラスが公開する必要のあるインターフェースの最小量を決定することです。

このプロセスをガイドするのに役立ついくつかの質問:

  • カウントする必要がありますか? IEnumerable<T>実装を検討しない場合
  • このコレクションは、初期化後に変更されますか?そうでない場合は、 IReadonlyList<T>考慮してIReadonlyList<T>
  • インデックスでアイテムにアクセスできることは重要ですか? ICollection<T>検討する
  • コレクションにアイテムを追加する順序は重要ですか?多分それはISet<T>ですか?
  • これらが本当に必要な場合は、先に進んでIList<T>を実装してください。

この方法では、コードの他の部分を野球選手コレクションの実装の詳細に結合せず、インターフェイスを尊重する限り、実装方法を自由に変更できます。

このアプローチを採用することで、コードが読みやすくなり、リファクタリングが容易になり、再利用できるようになります。

ボイラープレートの回避に関する注意

最新のIDEでのインターフェースの実装は簡単なはずです。右クリックして「インターフェースの実装」を選択します。次に、必要に応じて、すべての実装をメンバークラスに転送します。

とは言っても、定型文をたくさん書いているのなら、本来よりも多くの関数を公開している可能性があります。クラスから継承してはならないのと同じ理由です。

また、アプリケーションにとって意味のある小さなインターフェースを設計することもできます。おそらく、これらのインターフェースを必要な他のインターフェースにマッピングするためのヘルパー拡張関数を2、3だけ設計します。これは、 LinqArrayライブラリの独自のIArrayインターフェイスで採用したアプローチです

シリアライズに関する問題

1つの側面が欠落しています。 List <>から継承するクラスは、 XmlSerializerを使用して正しくシリアル化できません 。その場合は、代わりにDataContractSerializerを使用する必要があります。そうしないと、独自のシリアル化実装が必要になります。

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

参考資料クラスがList <>から継承される場合、XmlSerializerは他の属性をシリアル化しません

それはいつ受け入れられますか?

Eric Lippertを引用するには:

List<T>メカニズム拡張するメカニズムを構築している場合

たとえば、 IList<T> AddRangeメソッドがないことにうんざりしていIList<T>

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }

Related