クラスをそれ自体のリストのサブクラスとして定義することの欠点は何ですか?


48

私の最近のプロジェクトでは、次のヘッダーを持つクラスを定義しました。

public class Node extends ArrayList<Node> {
    ...
}

しかし、私のCS教授と話し合った後、彼はこのクラスは「記憶にとって恐ろしい」と「悪い練習」の両方になるだろうと述べました。前者は特に真実であり、後者は主観的であるとは思っていません。

この使用法の理由は、インスタンスの動作をカスタム実装または相互作用する複数の同様のオブジェクトの動作のいずれかによって定義できる、任意の深さを持つことができるものとして定義する必要があるオブジェクトのアイデアがあったことです。これにより、物理的な実装が相互作用する多くのサブコンポーネントで構成されるオブジェクトの抽象化が可能になります。¹

一方で、これがいかに悪い習慣になるかがわかります。何かをそれ自体のリストとして定義するという考えは、単純でも物理的にも実装可能ではありません。

私がなぜ任意の有効な理由があるべきではない、それは私の使用を考慮すると、私のコードでこれを使うには?


¹これをさらに説明する必要がある場合、私は喜んでいるでしょう。この質問を簡潔にしようとしています。



1
@gnatこれは、リストを含むリストを含むのではなく、クラスをそれ自体のリストとして厳密に定義することに関係しています。似たようなものの変形だと思いますが、完全に同じではありません。その質問は、ロバート・ハーベイの答えの線に沿った何かを指します
アディソンクランプ

16
C ++で一般的に使用されるCRTPイディオムを示すように、それ自体でパラメーター化されたものから継承することは珍しいことではありません。ただし、コンテナの場合の重要な質問は、構成ではなく継承を使用する必要がある理由を正当化することです。
クリストフ

1
私はそのアイデアが好きです。結局、ツリー内の各ノード(サブ)ツリーです。通常、ノードのツリーネスはタイプビューから隠され(従来のノードはツリーではなく単一のオブジェクトです)、代わりにデータビューで表現されます(単一ノードはブランチへのアクセスを許可します)。ここでは逆です。ブランチデータは表示されず、タイプにあります。皮肉なことに、C ++の実際のオブジェクトレイアウトはおそらく非常によく似ています(継承はデータを含むのと非常によく似て実装されます)。
ピーター-モニカの復活

1
たぶん私は少し欠けていますが、実装と比較して、本当に拡張する 必要がありますか?ArrayList<Node> List<Node>
マティアス

回答:


107

率直に言って、ここでは継承の必要性は見当たりません。意味がありません。Node ある ArrayListのはNode

これが単なる再帰的なデータ構造である場合は、次のように書くだけです。

public class Node {
    public String item;
    public List<Node> children;
}

これ理にかなっています。ノードには、子ノードまたは子孫ノードのリストがあります。


34
同じものではありません。リストのリストのリスト広告無限はあなたが保存されない限り無意味である何かのリストに新しいリストのほかに。それItem が目的でありItem、リストとは独立して動作します。
ロバートハーベイ

6
あ、なるほど。私がアプローチしたのNode は、 0から多くのオブジェクトを含む as ArrayListof です。それらの機能は、本質的には同じであるが、代わりのもののようなオブジェクトのリスト、それが持っているようなオブジェクトのリストを。書かれたクラスの元の設計は、いずれもsub のセットとして表現できるという信念でしたが、どちらも実装が行いますが、これは確かに混乱が少ないです。+1NodeNode NodeNodeNode
アディソンクランプ

11
CまたはLispなどで単一リンクリストを作成すると、ノードとリストの間のアイデンティティが「自然に」発生する傾向があると言えます。特定の設計では、ヘッドノードはリストであり、リストを表すために使用されたこと。そのため、JavaでEvilBadWrongを感じるとはいえ、一部のコンテキストではノードがノードのコレクションである(「持っている」だけでなく)ノードのコレクションであるという感覚を完全に払拭することはできません。
スティーブジェソップ

12
@ nl-x 1つの構文が短いということは、良いということではありません。また、偶然にも、そのようなツリー内のアイテムにアクセスすることは非常にまれです。通常、構造はコンパイル時に不明であるため、このようなツリーを定数値でトラバースしない傾向があります。深さも固定されていないため、その構文を使用してすべての値を反復処理することさえできません。従来のツリートラバーサルの問題を使用するようにコードを適合させると、最終的にChildren1回または2回しか記述できなくなります。そのような場合は、コードを明確にするために書き出すことが一般的に望ましいです。
セルビー


57

「記憶にとって恐ろしい」議論は完全に間違っていますが、客観的には「悪い習慣」です。クラスから継承する場合、関心のあるフィールドとメソッドだけを継承するのではなく、すべてを継承します。あなたが役に立たなくても、それが宣言するすべてのメソッド。そして最も重要なことは、すべてのコントラクトを継承し、クラスが提供することを保証することです。

頭字語SOLIDは、優れたオブジェクト指向設計のためのヒューリスティックを提供します。ここで、I nterface分掌の原則(ISP)とL置換Pricinple iskov(LSP)は、言いたいことがあります。

ISPは、インターフェイスをできるだけ小さくするように指示しています。しかし、から継承するArrayListことにより、多くのメソッドを取得できます。それは意味のあることですget()remove()set()(置き換え)、またはadd()特定のインデックスの子ノードは、(挿入)?ensureCapacity()基礎となるリストにとって賢明ですか?sort()ノードにとってそれはどういう意味ですか?クラスのユーザーは本当にを取得することになっていsubList()ますか?不要なメソッドを非表示にできないため、唯一の解決策は、ArrayListをメンバー変数として使用し、実際に必要なすべてのメソッドを転送することです。

private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }

ドキュメントにあるすべてのメソッドが本当に必要な場合は、LSPに進むことができます。LSPは、親クラスが予想される場所であればどこでもサブクラスを使用できる必要があることを示しています。関数がArrayListパラメータを受け取り、Node代わりに渡す場合、何も変更されることはありません。

サブクラスの互換性は、型シグネチャなどの単純なものから始まります。メソッドをオーバーライドする場合、親クラスで有効な使用を除外する可能性があるため、パラメーターの種類をより厳密にすることはできません。しかし、それはコンパイラがJavaでチェックするものです。

しかし、LSPはさらに深く実行されます。すべての親クラスとインターフェイスのドキュメントで約束されているすべてのものとの互換性を維持する必要があります。ではその答えは、リンはそのようなケースを発見したList(あなたが経由で継承されているインタフェースはArrayList)どのように保証equals()し、hashCode()メソッドが動作するようになっているが。以下のためにhashCode()あなたも正確に実施されなければならない特定のアルゴリズムを与えられています。あなたがこれを書いたと仮定しましょうNode

public class Node extends ArrayList<Node> {
  public final int value;

  public Node(int value, Node... children) {
    this.value = Value;
    for (Node child : children)
      add(child);
  }

  ...

}

これは、にvalue貢献hashCode()できず、影響を与えられないことを必要としますequals()Listインターフェース-あなたはそれから継承することによって名誉を約束する-必要がnew Node(0).equals(new Node(123))真実であることを。


クラスから継承すると、親クラスが作成した約束を誤って破りやすくなり、通常は意図したよりも多くのメソッドを公開するため、継承よりも合成好むことが一般的に推奨されます。何かを継承する必要がある場合は、インターフェイスのみを継承することをお勧めします。特定のクラスの振る舞いを再利用したい場合は、インスタンス変数に別のオブジェクトとして保持することができます。そうすれば、すべての約束と要件が強制されることはありません。

時々、私たちの自然言語は相続関係を示唆しています:車は乗り物です。オートバイは乗り物です。私は、クラス定義すべきであるCarMotorcycleのこと継承Vehicleクラスを?オブジェクト指向の設計とは、実際のコードを正確にミラーリングすることではありません。ソースコードで現実世界の豊富な分類法を簡単にエンコードすることはできません。

そのような例の1つは、従業員と上司のモデリングの問題です。複数Personのがあり、それぞれに名前と住所があります。AnがEmployeeあるPersonと持っていますBoss。A BossPersonです。ではPersonBossand によって継承されるクラスを作成する必要がありEmployeeますか?今、私には問題があります。上司も従業員であり、上司がいます。だから、Boss拡張する必要があるようEmployeeです。しかし、CompanyOwnerあるBossのではなくEmployee?あらゆる種類の継承グラフが何らかの形でここで分類されます。

OOPは既存のクラスの階層、継承、再利用ではなく、動作を一般化することです。OOPとは、「オブジェクトがたくさんあるので、それらに特定のことをさせたいのですが、どうすればよいか」ということです。それがインターフェイスの目的です。繰り返し可能にしたいためにIterableインターフェースを実装する場合Node、それはまったく問題ありません。Collection子ノードなどを追加/削除するためにインターフェイスを実装する場合、それで問題ありません。しかし、別のクラスから継承するのは、そうではないすべてをあなたに与えるからです。少なくとも、上で概説したように慎重な考えを与えない限り、そうではありません。


10
最後の4つの段落を印刷し、OOPと継承についてのタイトルを付けて、職場の壁に固定しました。私の同僚の中には、私が知っているように、あなたがそれを置く方法に感謝することを確認する必要があります:)ありがとう。
YSC

17

コンテナ自体を拡張することは、通常、悪い習慣として受け入れられます。コンテナを1つだけ持つのではなく、拡張する理由はほとんどありません。自分のコンテナを拡張すると、奇妙になります。


14

言われたことに加えて、この種の構造を避けるにはJava特有の理由があります。

equalsリストのメソッドのコントラクトでは、リストが別のオブジェクトと等しいと見なされる必要があります

指定されたオブジェクトもリストである場合にのみ、両方のリストのサイズは同じであり、2つのリストの対応する要素のペアはすべて等しいです。

ソース:https : //docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)

具体的には、これは、それ自体のリストとして設計されたクラスがあると、等値比較が高価になる可能性があることを意味します(リストが可変の場合はハッシュ計算も)、クラスにいくつかのインスタンスフィールドがある場合、等値比較ではこれらを無視する必要あります。


1
これは、悪い習慣は別として、これをコードから削除する優れた理由です。:P
アディソンクランプ

3

メモリについて:

これは完璧主義の問題だと思います。デフォルトのコンストラクタはArrayList次のようになります。

public ArrayList(int initialCapacity) {
     super();

     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);

     this.elementData = new Object[initialCapacity];
 }

public ArrayList() {
     this(10);
}

ソース。このコンストラクタは、Oracle-JDKでも使用されます。

次に、コードを使用して片方向リンクリストを作成することを検討してください。メモリ消費量を10倍に増やすことに成功しました(正確にはわずかに高くなります)。ツリーの場合、これはツリーの構造に特別な要件がなければ、簡単にかなり悪くなる可能性があります。LinkedListまたは、他のクラスを代わりに使用すると、この問題を解決できます。

正直なところ、ほとんどの場合、使用可能なメモリの量を無視しても、これは単なる完璧主義に過ぎません。A LinkedListは代替手段としてコードを少し遅くするので、パフォーマンスとメモリ消費の間のトレードオフです。それでも、これに関する私の個人的な意見は、これほど簡単に回避できるほど多くのメモリを無駄にしないことです。

編集:コメントに関する説明(正確には@amon)。答えのこのセクションでは、継承の問題を扱いませ。メモリ使用量の比較は、単一リンクリストと最良のメモリ使用量に対して行われます(実際の実装では、要因は少し変わる可能性がありますが、かなり多くの無駄なメモリを合計するにはまだ十分です)。

「悪い練習」について:

絶対に。これは、単純な理由でグラフを実装する標準的な方法ではありません。Graph-Nodeに子ノードがあり、子ノードのリストとしてではありません。コードの意味を正確に表現することは大きなスキルです。変数名で記述するか、このような構造を表現します。次のポイント:インターフェイスを最小限に抑える:ArrayList継承を介してクラスのユーザーがすべてのメソッドを利用できるようにしました。コードの構造全体を壊さずにこれを変更する方法はありません。代わりにListasを内部変数として保存し、必要なメソッドをアダプターメソッドで利用できるようにします。これにより、すべてを台無しにすることなく、クラスの機能を簡単に追加および削除できます。


1
何に比べて10倍高い?インスタンスフィールドとしてデフォルトの構築されたArrayListを(継承するのではなく)持っている場合、オブジェクトにArrayListへの追加のポインタを格納する必要があるため、実際には少し多くのメモリを使用しています。リンゴとオレンジを比較し、ArrayListからの継承とArrayListを使用しないことを比較する場合でも、Javaでは常にオブジェクトへのポインターを処理し、C ++のように値によるオブジェクトを処理しないことに注意してください。がnew Object[0]合計4ワードを使用すると仮定すると、a new Object[10]は14ワードのメモリしか使用しません。
アモン

@amon私はそれを明確に述べていない、それを恐れて。コンテキストは、私がLinkedListと比較していることを確認するのに十分なはずです。そして、それは参照と呼ばれ、ポインタbtwではありません。また、メモリに関するポイントは、クラスが継承を介して使用されるかどうかではなく、(ほとんど)未使用ArrayListのsがかなりのメモリを消費しているという事実だけです。
ポール

-2

これは、複合パターンのセマンティックのように見えます。再帰的なデータ構造をモデル化するために使用されます。


13
これに反対票を投じることはしませんが、これがGoFの本が本当に嫌いな理由です。それは出血した木です!なぜツリーを記述するために「複合パターン」という用語を考案する必要があったのですか?
デビッドアルノ

3
@DavidArnoそれは「複合パターン」としてそれを定義する全体のポリモーフィックノードの側面であり、ツリーデータ構造の使用はその一部にすぎないと考えています。
JAB

2
@RobertHarvey、私は同意しません(あなたの答えに対するコメントとここ)。public class Node extends ArrayList<Node> { public string Item; }回答の継承バージョンです。あなたの推論は簡単ですが、どちらもいくつかのことを達成します:それらは非バイナリツリーを定義します。
デビッドアルノ

2
@DavidArno:うまくいかないとは言わなかったが、たぶん理想的ではないと言った。
ロバートハーベイ

1
@DavidArnoここでは感情や味についてではなく、事実についてです。ツリーはノードで構成され、すべてのノードには潜在的な子があります。特定のノードは、特定の時点でのみリーフノードです。コンポジットでは、リーフノードは構造上リーフであり、その性質は変わりません。
クリストフ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.