私たちの(かなり大きな)コードベースに、次のような継承ツリーが見つかりました。
public class NamedEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OrderDateInfo : NamedEntity { }
私が集めることができるものから、これは主にフロントエンドでのバインドに使用されます。
私にとっては、ジェネリックNamedEntity
に依存するのではなく、クラスに具体的な名前を付けるため、これは理にかなっています。一方、単純に追加のプロパティを持たないクラスがいくつかあります。
このアプローチには欠点がありますか?
私にとっては、汎用の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
を継承することはできませんbothAuditedEntity
andNamedEntity
:
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
から継承させることはできません。これは、AuditedEntityNamedEntity
NamedEntity`間の継承により、and
にもなるためです。
継承を使用すると、クラスの複製を開始しない限り、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);
一方、単純に追加のプロパティを持たないクラスがいくつかあります。
これは マーカーインターフェイスパターン のバリエーションであり、空のインターフェイスを実装して、インターフェイスタイプを使用して、特定のクラスがこのインターフェイスで「マーク」されているかどうかを確認できるようにします。
実装されたインターフェイスではなく継承されたクラスを使用していますが、目的は同じなので、「マークされたクラス」と呼びます。
額面通り、マーカーインターフェイス/クラスに問題はありません。それらは構文的にも技術的にも有効であり、使用に固有の欠点はありませんマーカーが(コンパイル時に)普遍的に真であり、条件付きではない場合。
これは、基本メソッドと比較して、例外に実際に追加のプロパティ/メソッドがない場合でも、異なる例外を区別する方法です。
そのため、そうすることに本質的に問題はありませんが、これは慎重に使用することをお勧めします。不適切に設計されたポリモーフィズムで既存のアーキテクチャの間違いをカバーするだけではないことを確認してください。
これは、ポリモーフィズムが使用されないようにするために使用するものです。
継承チェーンのどこかにNamedEntity
を基本クラスとして持つ15の異なるクラスがあり、OrderDateInfo
にのみ適用できる新しいメソッドを書いているとします。
次のように署名を書くことができます
void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)
そして、型システムを悪用してFooBazNamedEntity
をそこに押し込まないようにと祈ってください。
または、単にvoid MyMethod(OrderDateInfo foo)
と書くことができます。これはコンパイラーによって強制されます。シンプルでエレガントで、間違いをしない人に依存しません。
また、 @ candied_orangeが指摘したように 、例外はこれの優れたケースです。ごくまれに(そして、ごくまれに)、catch (Exception e)
を使用してすべてをキャッチしたい場合があります。アプリケーションのSqlException
またはFileNotFoundException
またはカスタム例外をキャッチする可能性が高くなります。これらのクラスは、基本のException
クラスよりも多くのデータまたは機能を提供しないことがよくありますが、それらを調べてタイプフィールドをチェックしたり、特定のテキストを検索したりすることなく、それらが表すものを区別できます。
全体として、基本クラスを使用する場合よりも狭い型の型のセットを使用できるように型システムを取得するのはトリックです。つまり、すべての変数と引数をObject
型として定義することもできますが、それだけでは仕事が難しくなりますよね。
これは、私のお気に入りの継承の使用法です。主に、より適切で具体的な名前を使用できる例外に使用します
長い連鎖につながり、 yo-yo問題 を引き起こす継承の通常の問題は、連鎖する動機を与えるものがないため、ここでは当てはまりません。
私見クラスのデザインが間違っています。そのはず。
public class EntityName
{
public int Id { get; set; }
public string Text { get; set; }
}
public class OrderDateInfo
{
public EntityName Name { get; set; }
}
OrderDateInfo
HAS A Name
はより自然な関係であり、元の質問を引き起こさなかった2つの理解しやすいクラスを作成します。
NamedEntity
をパラメーターとして受け入れるメソッドは、Id
およびName
プロパティのみに関心があるはずなので、そのようなメソッドはEntityName
を受け入れるように変更する必要があります。代わりに。
元のデザインについて私が受け入れる唯一の技術的理由は、OPが言及したプロパティバインディングのためです。がらくたフレームワークは、追加のプロパティに対処してobject.Name.Id
にバインドすることができません。しかし、あなたの拘束力のあるフレームワークがそれを処理できない場合は、リストに追加するいくつかの技術的負債があります。
@Flaterの答えに沿って説明しますが、プロパティを含む多くのインターフェイスを使用すると、C#の美しい自動プロパティを使用しても、多くのボイラープレートコードが作成されてしまいます。 Javaでそれを行うことを想像してください!
クラスは動作を公開し、データ構造はデータを公開します。
クラスのキーワードは表示されますが、動作は表示されません。つまり、このクラスをデータ構造として表示し始めます。この流れで、私はあなたの質問を次のように言い換えます
なぜ共通の最上位データ構造があるのですか?
したがって、最上位のデータ型を使用できます。これにより、型システムを活用して、プロパティがすべて存在することを保証することにより、異なるデータ構造の大規模なセット全体でポリシーを保証できます。
最上位のデータ構造を含むが何も追加しないデータ構造があるのはなぜですか?
したがって、下位レベルのデータ型を使用できます。これにより、タイピングシステムにヒントを入力して、変数の目的を表現できます。
Top level data structure - Named
property: name;
Bottom level data structure - Person
上の階層では、Personに名前を付けることを指定すると便利です。そのため、人々は自分の名前を取得して変更できます。 Person
データ構造に追加のプロパティを追加することは合理的かもしれませんが、私たちが解決している問題はPersonの完全なモデリングを必要としないため、age
などの一般的なプロパティを追加することを怠っています。
つまり、このNamedアイテムの意図を更新(ドキュメントなど)で壊れず、後日簡単に拡張できるようにタイピングシステムを活用しています(本当にage
フィールドが必要な場合)後)。