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


1400

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

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

var football_team = new List<FootballPlayer>();

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

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

だから私は思う:

さて、サッカーチームは選手のリストのようなものですが、それに加えて、名前(a string)と現在の合計スコア(an 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ですか?

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

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

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

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

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

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

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

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

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

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

私の質問(要約)

、「論理的に」(「人間の心に」、と言うことであるが)だけで、データ構造、表現の正確なC#の方法は何であるlistthings少数の添えものとは?

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


90
だから、本当にあなたの質問は継承を使用するとき(Square is-a Rectangleだからです)名前のように、それに対して「基本的」である他のプロパティに)他のプログラマーが単純なリストを期待していたときに、コードの他の場所であなたがFootballTeamを渡した場合、混乱するでしょうか?
オデッセイ便2014

77
フットボールチームがビジネスカンパニーであり、いくつかの追加プロパティを備えたプレーヤーのリストではなく、プレーヤー契約のリストを所有していると私が言った場合はどうなりますか?
STT LCU 2014

75
とてもシンプルです。サッカーチームは選手の名簿ですか?言うまでもなく、他にも関連するプロパティがあるためです。サッカーチームは選手の名簿を持っていますか?はい、リストを含める必要があります。それ以外は、後で変更する方が簡単なので、一般に継承よりも構成の方が望ましいです。継承と構成が同時に論理的であるとわかった場合は、構成に進みます。
Tobberoth 2014

45
@FlightOdyssey:四角形を受け取り、高さを半分にし、幅を2倍にするメソッドSquashRectangleがあるとします。次に、たまたま 4x4であるがタイプ長方形の長方形と、4x4の正方形を渡します。SquashRectangleが呼び出された後のオブジェクトの寸法は何ですか?長方形の場合、明らかに2x8です。正方形が2x8の場合、正方形ではなくなります。2x8でない場合、同じ形状で同じ操作を行うと異なる結果が生成されます。結論としては、変更可能な正方形は一種の長方形ではないということです。
Eric Lippert、2014

29
@Gangnus:あなたの声明は単に偽です。実際のサブクラスには、スーパークラスのスーパーセットである機能が必要です。Aはstringすべて行うために必要とされてobject行うことができ、よりを
Eric Lippert、2014

回答:


1566

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

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

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

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

DID 誰の言う「数の添えものとサッカー選手のリストは、」、またはそれらはすべて「スポーツチーム」または「クラブ」や「組織」と言ったのですか?サッカーチームが特定の種類の選手のリストであるというあなたの考えは、あなたの人間の心とあなたの人間の心だけにあります。

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

から継承しています List<T>常に受け入れられませんか?

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

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

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

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

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

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

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

更新

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

つまり、フットボールチームは、プレーヤーが採用されたとき、負傷したとき、引退したときなどの歴史的事実のコレクションとしてうまくモデル化されいるように見えます。中心ですが、このオブジェクトで実行したい他の興味深い事柄があり、より歴史的な視点が必要な場合があります。


84
他の回答は非常に役に立ちましたが、これは私の懸念に最も直接的に対処すると思います。コードの量を誇張することに関しては、結局それほど多くの作業ではないということは正しいですが、プログラムにコードを含めると、なぜそれを含めるのか理解していないと、非常に混乱します。
スーパーベスト2014

128
@Superbest:助けてくれてうれしい!あなたはそれらの疑問に耳を傾ける権利があります。なぜコードを書いているのか理解できない場合は、それを理解するか、理解している別のコードを書いてください。
Eric Lippert、2014

48
@Mehrdadしかし、最適化の黄金のルールを忘れているようです。1)実行しない、2)まだ実行していない、3)最初にパフォーマンスプロファイルを実行して最適化の必要性を示すことなしに実行しないでください。
AJMansfield 2014

107
@Mehrdad:正直なところ、アプリケーションで仮想メソッドなどのパフォーマンスの負担を気にする必要がある場合、最新の言語(C#、Javaなど)はあなたにとって言語ではありません。私はここにラップトップを持っていて、子供の頃に遊んだすべてのアーケードゲームのシミュレーション同時に実行できました。これにより、あちこちで間接的なレベルについて心配する必要がなくなります。
Eric Lippert、2014

44
@Superbest:現在、私たちは間違いなく「継承ではない構成」の端にいます。ロケットはロケット部品で構成されています。ロケットは「特別なパーツリスト」ではありません!私はあなたにそれをさらに削減することを提案します。なぜリストではなくIEnumerable<T>シーケンスですか?
Eric Lippert、2014

161

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

  • List<T> され、高度に最適化されました。その主な用途は、オブジェクトのプライベートメンバーとして使用することです。
  • 場合によっては、わかりやすい名前のクラスを作成する必要があるため、Microsoftはそれを封印しませんでしたclass MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }。今では簡単ですvar list = new MyList<int, string>();です。
  • CA1002:一般的なリストを公開しないでください:基本的に、このアプリを単独の開発者として使用することを計画している場合でも、優れたコーディング方法で開発することは価値があるため、それらはあなたと第二の性質に浸透します。IList<T>コンシューマーにインデックス付きリストを作成する必要がある場合でも、リストをとして公開できます。これにより、後でクラス内の実装を変更できます。
  • マイクロソフトCollection<T>は、それが一般的な概念であるために非常に一般的にしました...名前はそれをすべて言います。それは単なるコレクションです。などのより正確なバージョンがありSortedCollection<T>ObservableCollection<T>ReadOnlyCollection<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.
}

6
あなたの投稿には完全な質問がたくさんあります」-ええと、私は完全に混乱しました。@Readonlyの質問にリンクしていただきありがとうございます。私の質問の大部分は、より一般的な構成と継承の問題の特殊なケースであることがわかりました。
スーパーベスト2014

3
@ブライアン:エイリアスを使用してジェネリックを作成できないことを除いて、すべての型は具象でなければなりません。私の例でList<T>は、ジェネリックを使用しての使用と宣言を簡略化するプライベート内部クラスとして拡張されList<T>ます。拡張子が単にclass FootballRoster : List<FootballPlayer> { }である場合、そうです、代わりにエイリアスを使用できました。追加のメンバー/機能を追加できることは注目に値すると思いますが、の重要なメンバーをオーバーライドできないため、制限がありますList<T>
私の

6
これがProgrammers.SEである場合、私は現在の回答票の範囲に同意しますが、SOの投稿なので、この回答は少なくともC#(.NET)固有の問題への回答を試みます。設計上の問題。
Mark Hurd、

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden今、それはないリストから継承するには十分な理由があります。他の答えはあまりにも哲学が多すぎます。
Navin、2014

10
@my:探偵は探偵なので、私はあなたが質問の「大量」を意味していると思います。
バティ

152
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.
}

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

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

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


9
2番目の例は、フットボールチームではなく、フットボールクラブであると思います。フットボールクラブには、マネージャーや管理者などがいます。 この情報源から:「フットボールチームは、プレーヤーのグループに与えられる総称です...そのようなチームは、サッカークラブを表すために、対戦するチームとの試合でプレーするために選択できます。 ...」したがって、チームプレーヤーのリスト(または、より正確にはプレーヤーのコレクション)であると私は考えています。
ベン

3
より最適化private readonly List<T> _roster = new List<T>(44);
SiD 2016

2
System.Linq名前空間をインポートする必要があります。
Davide Cannizzo、2017

1
ビジュアルの場合:+1
V.7

115

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

この特定のケースでは:

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

または

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

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

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

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

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

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

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

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


1
#2を拡張すると、リストがリストを持つことは意味がありません。
ランチャー2014

第一に、問題は構成と継承では関係ありません。答えは、OPはより具体的な種類のリストを実装したくないため、List <>を拡張しないことです。私はあなたがスタックオーバーフローで非常に高いスコアを持っているのでねじれているので、これを明確に知っておくべきであり、人々はあなたの言うことを信頼します。システムですが、明らかにそうではありません!#no-rage
Mauro Sampietro

6
@sam彼はリストの動作を望んでいます。彼には2つの選択肢があります。リストを拡張する(継承)か、オブジェクト内にリストを含める(構成)かです。55人が間違っているのではなく、質問または回答の一部を誤解しているのではないでしょうか。:)
Tim B

4
はい。これは、スーパーとサブが1つしかない退化したケースですが、私は彼の具体的な質問についてより一般的な説明をしています。チームは1つだけで、リストは常に1つですが、リストを継承するか、内部オブジェクトとして含めるかを選択できます。複数のタイプのチーム(フットボール、クリケットなど)を含め始めると、彼らが単なる選手のリスト以上のものになり始めると、完全な構成と継承の問題があることがわかります。このような場合、早い段階で間違った選択を避け、後で多くのリファクタリングを行うことを防ぐために、全体像を見ることが重要です。
Tim B

3
OPはコンポジションを要求しませんでしたが、ユースケースは、4つのオブジェクト指向プログラミングの原則の1つが壊れている、誤用されている、または完全に理解されていないX:Y問題の明確な例です。より明確な答えは、StackやQueue(ユースケースに適合していないように見える)などの特殊なコレクションを作成するか、構成を理解することです。サッカーチームは、サッカー選手のリストではありません。OPはそれを求めかどうか、明示的にか、彼らは答えを理解することはありません抽象化、カプセル化、継承を理解せず、無関係であり、かつ多型、エルゴ、X:Yは逆上
ジュリア・マクギガン

67

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

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

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

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

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

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

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

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


8
リストとセットを比較してみてください。それはあまりにも一般的に行われているエラーのようです。コレクション内の要素のインスタンスが1つしか存在できない場合、ある種のセット、または辞書が実装の優先候補になるはずです。同じ名前の2人のプレーヤーでチームを構成することは問題ありません。1人のプレーヤーを同時に2回含めることはできません。
Fred Mitchell 14

1
いくつかの良い点を+1しますが、「データ構造が役立つ以上のものを提供するか」ではなく、「データ構造が有用なものすべてを合理的にサポートできるかどうか(理想的には短期および長期)」よりも重要です。
Tony Delroy、2014

1
@TonyDええと、私が提起しているポイントは、「データ構造が何よりも有用か」をチェックする必要があるということではありません。「Parentデータ構造が、Childクラスが示唆する動作とは無関係、無意味、または直感に反する何かを行う場合」をチェックすることです。
Satyan Raina 14

1
@TonyD多くの場合、ネガティブテストに不合格となるため、親から派生した無関係な特性を持つことには実際に問題があります。プログラマーは、Human {eat();を拡張できます。run(); ゴリラからのwrite();} {eat(); run(); swing();}スイングできるという追加の機能を備えた人間には何も問題はないと考えています。そして、ゲームの世界では、人間が木を振り回すだけで、想定されているすべてのハードルを突然回避し始めます。明示的に指定されていない限り、実用的な人間はスイングできません。このような設計により、APIは非常に悪用されて混乱しやすくなります
Satyan Raina

1
@TonyD PlayerクラスをHashSetから派生させることもお勧めしません。Playerクラスは「ほとんどの場合」Compositionを介してHashSetを使用して実装することをお勧めします。これは完全に実装レベルの詳細であり、デザインレベルの詳細ではありません(そのため、私の回答の補足として言及しました)。そのような実装に正当な理由がある場合、リストを使用して実装することもできます。だからあなたの質問に答えるために、キーによるO(1)ルックアップが必要ですか?いいえ。したがって、HashSetからPlayerを拡張してはなりません。
Satyan Raina 2014

50

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

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

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

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

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

またはRetiredPlayers

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

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

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

以下の場合にはFootballTeam、それ右の音はありません。A TeamはだけではありませんCollection。あTeam他の回答で指摘されているように、はコーチやトレーナーなどを配置できます。

FootballCollection サッカーのコレクションのように聞こえるかもしれません。 TeamCollection、チームのコレクション。

FootballPlayerCollection から継承するクラスの有効な名前になるプレーヤーのコレクションのように聞こえます List<FootballPlayer>ます。これは、本当にそうしたいのであればなります。

本当にList<FootballPlayer>対処するのに最適なタイプです。IList<FootballPlayer>メソッドからそれを返す場合もあるでしょう。

要約すれば

自問してみてください

  1. ありますか ?または持っていますかXY XY

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


すべてのプレーヤーの種類は、どちらかに属するものとして、それを分類する場合DefensivePlayersOffensivePlayersまたはOtherPlayers期待したコードで使用できるタイプの持っている、それが合法的に役に立つかもしれませんList<Player>が、また含まメンバーDefensivePlayersOffsensivePlayersまたはSpecialPlayersタイプのIList<DefensivePlayer>IList<OffensivePlayer>などをIList<Player>。別のオブジェクトを使用して別のリストをキャッシュすることもできますが、メインリストと同じオブジェクト内にそれらをカプセル化する方がきれいに見えます[リスト列挙子の無効化を使用してください...
スーパーカタログ

...メインリストが変更され、次にアクセスされたときにサブリストを再生成する必要があるという事実の手がかりとして]
スーパーキャット2014

2
指摘された点には同意しますが、誰かがデザインアドバイスを提供し、ビジネスオブジェクトでパブリックゲッターとセッターを使用して具体的なList <T>を公開することを提案するのは本当に心が裂けます:(
sara

38

設計>実装

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

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

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

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

継承は「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」関係を見つけることは、現実世界のドメインオブジェクトでより一般的です。変更。これにより、コレクションクラスからの継承が常に除外されるわけではありません。しかしそれは示唆的かもしれません。


34

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

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

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


30

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

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

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

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

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

19
これは理由を説明していません。OPは、他のほとんどのプログラマーがこのように感じていることを知っていますが、なぜそれが重要なのか理解していません。これは本当に問題になっている情報を提供しているだけです。
サービー2014

2
これも私が最初に考えたもので、彼のデータが誤ってモデル化された方法です。その理由は、データモデルが正しくないためです。
rball 2014

3
リストから継承しないのはなぜですか?彼はもっと具体的な種類のリストを望んでいないからです。
マウロサンピエトロ2014

2
2番目の例は正しくないと思います。状態を公開しているため、カプセル化が壊れています。状態はオブジェクトに対して非表示/内部である必要があり、この状態を変更する方法はパブリックメソッドを介する必要があります。正確にはどのメソッドがビジネス要件から決定されるものですが、「今すぐこれが必要」という根拠に基づくべきであり、「いつか便利なのではないか」という根拠に基づくべきではありません。APIをタイトに保つと、時間と労力を節約できます。
サラ

1
カプセル化は問題ではありません。型をより具体的な方法で動作させたくない場合は、型を拡張しないことに重点を置きます。これらのフィールドは、c#の自動プロパティおよび他の言語のget / setメソッドである必要がありますが、このコンテキストでは、これは完全に
不要です

28

状況による

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

不明確なセマンティクス

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

クラスは拡張可能です

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

クラスはより複雑です

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

結論/考慮事項

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

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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


27

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

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

string team = new string();

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

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

問題への入り口は任意です。チームが何を考えてみる必要がありそうでないものを、(プロパティ)です

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


1
それは良い点です-チームは単なる名前と考えることができます。ただし、私のアプリケーションがプレイヤーのアクションを操作することを目的としている場合、その考え方は少し明白ではありません。いずれにせよ、結局のところ、問題は構成と継承のどちらかにあるようです。
スーパーベスト2014

6
それはそれを見る価値のある1つの方法です。副注としてこれを考慮してください:富のある人はいくつかのサッカーチームを所有しており、彼は友人に贈り物として1つを与え、友人はチームの名前を変更し、コーチを解雇します、すべてのプレーヤーを置き換えます。友達チームはグリーンで男子チームと出会い、男子チームが負けているので、「私が与えたチームであなたが私を倒しているなんて信じられない!」男は正しいですか?これをどのように確認しますか?
user1852503 2014

20

フットボールチームが「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(パフォーマンス上の理由から)。

アプリケーションを使用している場合(基本的には、自分とすべての呼び出し元が一緒にコンパイルされることを意味します)、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の場合は、好きなようにします。


18

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

myTeam.subList(3, 5);

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


3
あなたがそれを呼んだのならそれかもしれませんmyTeam.subTeam(3, 5);
サム・リーチ

3
@SamLeachそれが本当だと仮定すると、継承ではなくコンポジションが必要になります。subList戻りませんTeamもう。
ランチャー2014

1
これは、他のどの答えよりも私を納得させました。+1
FireCubez

16

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

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

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

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

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


1
OPはモデルリアリティの方法を理解したいと考えていますが、フットボールチームをフットボール選手のリストとして定義しています(これは概念的には間違っています)。これが問題です。私の意見では、他の議論は彼に誤解を与えています。
Mauro Sampietro 14

3
プレイヤーのリストが概念的に間違っているとは思わない。必ずしも。このページで他の誰かが書いたように、「すべてのモデルは間違っていますが、一部は便利です」
xpmatteo

適切なモデルを入手するのは困難です。一度完了すると、有用なモデルになります。モデルが誤用される可能性がある場合、概念的には間違っています。stackoverflow.com/a/21706411/711061
Mauro Sampietro

15

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

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

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

プレーヤーが退職したか、辞任したか、解雇されたかを知ることで、プレーヤーがいつ去ったかを区別したいとします。RemovePlayer適切な入力列挙型を受け取るメソッドを実装できます。しかし、継承によってList、あなたはへの直接アクセスを防止することができないであろうRemoveRemoveAllでも、とClear。その結果、あなたが実際にしましdisempoweredあなたのFootballTeamクラスを。


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

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

正解です。すべてのクライアントがチームを使用するのは不必要に冗長です。ただし、その問題はList Players、すべての物や雑貨にさらされて、同意なしにチームがいじることができるという事実と比較すると、非常に小さなものです。

あなたは続けて言う:

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

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


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


12

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

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

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

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

-編集済み

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

-エディション2

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

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

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


私の最良の答えは...それは場合によります。Listから継承すると、このクラスのクライアントは公開すべきではないメソッドに公開されます。これは、主にFootballTeamがビジネスエンティティのように見えるためです。この回答を編集する必要があるのか​​、別の投稿をする必要があるのか​​わかりません。
ArturoTena 14

2
から継承するListと、このクラスのクライアントが公開されるべきではないメソッドに公開される」というビットが、まさにListno-no からの継承になります。暴露されると、彼らは徐々に虐待され、時間の経過とともに誤用されます。「経済的に開発する」ことで時間を節約することは、将来失われる節約の10倍につながる可能性があります。悪用をデバッグし、最終的にリファクタリングして継承を修正します。YAGNIの原則は、意味として考えることもできます。あなたはからのこれらのメソッドをすべて必要とListしないので、公開しないでください。
幻滅2014

3
「過剰設計しないでください。記述するコードが少ないほど、デバッグする必要のあるコードも少なくなります。」<-これは誤解を招くと思います。継承を介したカプセル化と構成は、過剰なエンジニアリングではなく、デバッグの必要性を引き起こしません。カプセル化することで、クライアントがクラスを使用(および乱用)する方法の数が制限されるため、テストと入力検証を必要とするエントリポイントが少なくなります。List迅速かつ簡単であり、バグが少なくなるために継承するのは明らかに間違っています。設計が悪いだけで、設計が悪いと「過剰なエンジニアリング」よりも多くのバグが発生します。
サラ

@kai私はあらゆる点であなたに同意します。私は「エンジニアをやりすぎないでください」というコメントで私が言及していたことを心から思い出しません。OTOH、Listから継承するだけで便利なケースは限られていると思います。後の版で書いたように、状況によって異なります。それぞれのケースに対する答えは、知識、経験、個人的な好みの両方に大きく影響されます。人生のすべてのように。;-)
ArturoTena

11

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

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


9

私の汚い秘密:私は人々が何を言うか気にしません、そして私はそれをします。.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>を使用できないようにする他の技術的な制約を試してみる必要があるかもしれません。ただし、存在してはならない制約を追加しないでください。マイクロソフトが理由を文書化しなかった場合、それは確かにどこからともなく「ベストプラクティス」です。


9
必要に応じて、受け入れられた知恵に挑戦する勇気とイニシアチブを持つことが重要ですが、受け入れられた知恵が最初に受け入れられるようになった理由を最初に理解してから、それに挑戦するのが賢明だと思います。-1「そのガイドラインを無視してください!」「なぜこのガイドラインが存在するのか」に対する良い答えではありません。ちなみに、コミュニティはこの場合「私を責める」だけでなく、私を説得することに成功した満足のいく説明を提供しました。
2014

3
実際には、何も説明されていない場合、あなたの唯一の方法は境界を押し広げ、違反を恐れないことをテストすることです。今。List <T>が良い考えではなかったとしても、自問してください。継承をList <T>からCollection <T>に変更するのはどのくらい大変ですか?推測:リファクタリングツールを使用したコードの長さに関係なく、フォーラムに投稿するよりも短い時間です。これは良い質問ですが、実用的な質問ではありません。
Nicolas Dorier、2014

明確にするために:私は確かに私が望むものから私が望むものから継承するためのSOの祝福を待っていませんが、私の質問のポイントは、この決定に入る必要がある考慮事項と、なぜ多くの経験豊富なプログラマーが持っているように見えるかを理解することでした私がやったことの反対を決めました。Collection<T>は同じではないList<T>ため、大規模なプロジェクトで検証するにはかなりの作業が必要になる場合があります。
スーパーベスト2014

2
team.ByName("Nicolas")手段"Nicolas"のチームの名前です。
Sam Leach、

1
「存在してはならない制約を追加しないでください」Au反対に、公開する正当な理由がないものは公開しないでください。これは、純粋主義や盲目的な熱狂主義ではなく、最新のデザインファッショントレンドでもありません。基本的なオブジェクト指向デザイン101、初心者レベルです。この「ルール」が存在する理由は秘密ではありません。これは数十年の経験の結果です。
サラ

8

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


振り返ってみると、@ EricLippertなどの説明の後、実際に私の質問のAPIの部分に優れた回答がありました。あなたが言ったことに加えて、私がそうした場合class FootballTeam : List<FootballPlayers>、私のクラスのユーザーは私に伝えることができます。継承されたveはList<T>、リフレクションを使用して見ることで、List<T>サッカーチームのために意味を持たないメソッドを、そして使用することができることFootballTeamList<T>私は考えて、クライアントへの暴露の実装の詳細も(不必要)。
スーパーベスト2014

7

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

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


4

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

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

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

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

    ...
}

class Player
{
    ...
}

3

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

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

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


3

私は、エッフェルの発明者であり、契約により設計されたベルトラン・マイヤーがまぶたを打つだけでなく、それからTeam継承したものであるList<Player>ことを付け加えたかっただけです。

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

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


1
ここでの問題は、多重継承、ミックスイン(など)またはそれらの不在に対処する方法ではありません。どの言語でも、人間の言語でも、チームは人のリストではなく、人のリストで構成されます。これは抽象的な概念です。Bertrand Meyerや誰かがListをサブクラス化してチームを管理している場合は、間違っています。より具体的な種類のリストが必要な場合は、リストをサブクラス化する必要があります。あなたが同意することを願っています。
マウロサンピエトロ2015

1
それはあなたへの継承が何であるかに依存します。それ自体は抽象的な操作であり、2つのクラスが「特別な種類である」関係にあるという意味ではありませんが、主流の解釈です。ただし、今日では構成が優先される代替であるにもかかわらず、言語設計が継承サポートしている場合、継承実装の再利用のメカニズムとして扱うことできます。
Alexey

2

私はあなたの一般化に同意しないと思います。チームは単なる選手の集まりではありません。チームには、名前、エンブレム、管理/管理スタッフのコレクション、コーチングクルーのコレクション、そして選手のコレクションなど、はるかに多くの情報があります。したがって、適切には、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();
}

2

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

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

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

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

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

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

クラスは実装の詳細です

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

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

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

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

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

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

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

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

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

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

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

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

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

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


2

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

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は他の属性をシリアル化しません


0

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

Eric Lippertを引用するには:

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

たとえば、次のAddRangeメソッドが存在しないことにうんざりしているとしますIList<T>

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

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.