クラスを継承し、プロパティを追加しないのはなぜですか?


39

私たちの(かなり大きな)コードベースで、次のような継承ツリーを見つけました。

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

私が集めることができるものから、これは主にフロントエンドでのものをバインドするために使用されます。

私にとっては、genericに依存するのではなく、クラスに具体的な名前を付けるため、これは理にかなっていますNamedEntity。一方、追加のプロパティを持たないクラスは数多くあります。

このアプローチには欠点がありますか?


25
アップサイド言及されていない:あなたはにのみ関連している方法を識別することができるOrderDateInfo他に関連するものとS NamedEntityS
Caleth

8
同じ理由で、たまたまIdentifier関係のないクラスNamedEntityがありIdNameプロパティとプロパティの両方が必要な場合は、NamedEntity代わりに使用しません。クラスのコンテキストと適切な使用法は、クラスが保持するプロパティとメソッド以上のものです。
ニール

回答:


15

私にとっては、汎用のNamedEntityに依存するのではなく、クラスに具体的な名前を付けるため、これは理にかなっています。一方、追加のプロパティを持たないクラスは数多くあります。

このアプローチには欠点がありますか?

アプローチは悪くありませんが、利用可能なより良いソリューションがあります。要するに、インターフェイスはこのためのはるかに優れたソリューションです。ここでインターフェイスと継承が異なる主な理由は、1つのクラスからしか継承できないが、多くのインターフェイスを実装できるためです。

たとえば、名前付きエンティティと監査済みエンティティがあるとします。いくつかのエンティティがあります:

One監査対象エンティティでも名前付きエンティティでもありません。それは簡単です:

public class One 
{ }

Twoは名前付きエンティティですが、監査対象エンティティではありません。それは本質的にあなたが今持っているものです:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Three名前付きエントリと監査済みエントリの両方です。ここで問題が発生します。AuditedEntity基本クラスを作成できますが、との両方Three継承させることはできません: AuditedEntity NamedEntity

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

ただし、AuditedEntityから継承することで回避策を考えることができますNamedEntity。これは、すべてのクラスが他の1つのクラスから(直接)継承するだけで済むことを保証する巧妙なハックです。

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

これでも機能します。しかし、ここで行ったことは、すべての監査対象エンティティが本質的に名前付きエンティティでもあると述べています。これで最後の例になります。Fourは監査対象エンティティですが、名前付きエンティティではありません。しかし、あなたは任せることはできないFourから継承しAuditedEntityますが、その後もそれを作ることになるようNamedEntityAuditedEntity間の継承のためにandNamedEntity`。

継承を使用するThreeと、Fourクラスの複製を開始しない限り、両方を作成して動作させる方法はありません(これにより、まったく新しい問題が発生します)。

インターフェイスを使用すると、これを簡単に実現できます。

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

ここでの唯一の小さな欠点は、まだインターフェイスを実装する必要があることです。ただし、特定のエンティティに対して複数の一般的なタイプのバリエーションが必要な場合に生じる欠点を伴わずに、共通の再利用可能なタイプを持つことですべての利点を得ることができます。

ただし、ポリモーフィズムはそのまま残ります。

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

一方、追加のプロパティを持たないクラスは数多くあります。

これはマーカーインターフェイスパターンのバリエーションで、空のインターフェイスを実装して、インターフェイスタイプを使用して、特定のクラスがこのインターフェイスで「マーク」されているかどうかを確認できるようにします。
実装されたインターフェースの代わりに継承されたクラスを使用していますが、目標は同じであるため、「マークされたクラス」と呼びます。

額面通り、マーカーインターフェイス/クラスに問題はありません。それらは構文的および技術的に有効であり、マーカーが(コンパイル時に)普遍的に条件付きではなく真である限り、それらの使用に固有の欠点はありません

これは、例外が実際には基本メソッドと比較して追加のプロパティ/メソッドを持たない場合でも、異なる例外を区別する方法です。

したがって、本質的に問題はありませんが、これを慎重に使用することをお勧めします。不適切に設計されたポリモーフィズムで既存のアーキテクチャ上の間違いを隠そうとしているだけではありません。


4
「1つのクラスからのみ継承できますが、多くのインターフェイスを実装できます。」ただし、すべての言語に当てはまるわけではありません。一部の言語では、複数のクラスの継承が許可されています。
ケネスK.

ケネスが言ったように、2つの非常に人気のある言語には、C ++とPythonの多重継承機能があります。threeたとえば、インターフェイスを使用しない場合の落とし穴の例のクラスは、C ++では実際に有効です。実際、これは4つの例すべてで適切に機能します。実際、これはすべて、インターフェイスを使用する必要があるときに継承を使用しないという警告的な話ではなく、言語としてのC#の制限のようです。
18

63

これは、ポリモーフィズムが使用されるのを防ぐために使用するものです。

NamedEntity継承チェーンのどこかに基本クラスとしてある15の異なるクラスがあり、にのみ適用可能な新しいメソッドを記述しているとしますOrderDateInfo

次のように署名するだけで「できた」

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

そして、型システムを乱用して誰FooBazNamedEntityもそこに押し込めないことを願って祈ってください。

または、「書く」ことができますvoid MyMethod(OrderDateInfo foo)。現在、それはコンパイラーによって実施されています。シンプルでエレガント、人々が間違いを犯さないことに依存しません。

また、@ candied_orangeが指摘したように、例外はこの素晴らしいケースです。非常にまれに(そして、非常に、非常にまれに)、すべてをキャッチしたいと思うことはありませんcatch (Exception e)。より多くの可能性が高いあなたはキャッチしたいSqlExceptionか、FileNotFoundExceptionまたはアプリケーションのカスタム例外を。これらのクラスは、多くの場合、基本Exceptionクラスより多くのデータや機能を提供しませんが、それらを調べたり、タイプフィールドを確認したり、特定のテキストを検索したりすることなく、表現内容を区別できます。

全体として、基本クラスを使用した場合よりも狭いタイプのセットを使用できるように型システムを取得することは、コツです。Objectつまり、すべての変数と引数をtypeとして定義することもできますが、それは仕事を難しくするだけですよね?


4
私の新しさをご容赦ください。しかし、なぜOrderDateInfoにNamedEntityオブジェクトをプロパティとして作成する方が良いアプローチではないのでしょうか?
アダムB

9
@AdamBこれは有効な設計選択です。それが良いかどうかは、何に使用されるかによって異なります。例外の場合、例外プロパティを使用して新しいクラスを作成することはできません。言語(私にとってはC#)でスローできないためです。継承ではなく構成の使用を妨げる他の設計上の考慮事項があるかもしれません。何回も構成が優れています。それはあなたのシステム次第です。
Becuzz

17
@AdamB基本クラスから継承し、基本にないメソッドまたはプロパティを追加するときに、新しいクラスを作成せず、基本クラスをプロパティとして配置するのと同じ理由で。哺乳類である人間と哺乳類を持つことの間には違いがあります。
Logarr

8
@Logarr true私が哺乳類であることと哺乳類を持つことには違いがある。しかし、もし私が私の内なる哺乳類に忠実であるならば、あなたは違いを決して知ることはないでしょう。なぜこんなに多くの種類の牛乳を気まぐれに与えることができるのか疑問に思うかもしれません。
candied_orange

5
@AdamBそれはできます。これは、継承より合成として知られています。ただしNamedEntity、このように名前を変更します(例:)。これEntityNameにより、説明されているときに構成が意味をなします。
セバスチャンレッド

28

これは、私のお気に入りの継承の使用法です。より良い、より具体的な名前を使用する可能性のある例外に主に使用します

連鎖を引き起こし、ヨーヨーの問題を引き起こす継承の通常の問題は、連鎖する動機を与えるものがないため、ここでは当てはまりません。


1
うーん、良い点は例外です。恐ろしいことだと言っていました。しかし、優れたユースケースで私を説得しました。
デビッドアルノ

ヨーヨーホーとラム酒のボトル。まれに、orlopがヤールです。それは板の間の適切なoakumの不足のためにしばしば漏れます。デイビー・ジョーンズのロッカーに、それを作った土地の泥棒。翻訳:継承チェーンが非常に優れているため、基本クラスを抽象的/効果的に無視できることはまれです。
レーダーボブ

私はそれの価値が他から継承新しいクラスがあることを指摘だと思う新しいクラスの名前-新しいプロパティを追加します。これは、より良い名前で例外を作成するときに使用するものです。
ローガンピックアップ

1

私見クラスの設計が間違っています。そのはず。

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoHAS A Nameはより自然な関係であり、元の質問を引き起こさなかった2つのわかりやすいクラスを作成します。

NamedEntityパラメータとして受け入れられたメソッドはIdNameプロパティとプロパティだけに関心があるはずなので、そのようなメソッドはEntityName代わりに受け入れるように変更する必要があります。

元の設計で受け入れる唯一の技術的な理由は、OPが言及したプロパティバインディングのためです。がらくたフレームワークは、余分なプロパティに対処してにバインドすることはできませんobject.Name.Id。しかし、バインディングフレームワークがそれに対処できない場合は、リストに追加する技術的な負債がいくつかあります。

私は@Flaterの答えと一緒に行きますが、プロパティを含む多くのインターフェイスでは、C#の素敵な自動プロパティであっても、多くの定型コードを書くことになります。Javaでそれを行うことを想像してください!


1

クラスは動作を公開し、データ構造はデータを公開します。

クラスのキーワードは表示されますが、動作は表示されません。これは、このクラスをデータ構造として表示し始めることを意味します。このように、私はあなたの質問を次のように言い換えます

共通の最上位データ構造があるのはなぜですか?

したがって、最上位のデータ型を使用できます。これにより、型システムを活用して、プロパティがすべて揃っていることを保証することにより、さまざまなデータ構造の大規模なセットにわたってポリシーを確保できます。

トップレベルのデータ構造を含むが、何も追加しないデータ構造があるのはなぜですか?

したがって、低レベルのデータ型を使用できます。これにより、変数の目的を表現するために、タイピングシステムにヒントを入れることができます。

Top level data structure - Named
   property: name;

Bottom level data structure - Person

上記の階層では、Personに名前を付けて、名前を取得および変更できるように指定すると便利です。Personデータ構造に追加のプロパティを追加することは合理的かもしれませんが、私たちが解決しようとしている問題はPersonの完全なモデリングを必要としないためage、などの一般的なプロパティを追加することを怠りました。

したがって、タイピングシステムを活用して、この名前付きアイテムの意図を更新(ドキュメントなど)で中断せず、後日簡単に拡張できる方法で表現することができます(age後でフィールドが本当に必要な場合は、)。

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