web-dev-qa-db-ja.com

純粋なデータクラスのインターフェイスを定義する理由はありますか?

私は同僚のコードをレビューしていますが、変更の一部として、この人は次のような新しい純粋なデータクラスをいくつか導入しました(C#):

public class Result : IResult
{
    public bool Succeeded { get; set; }
    public string ResponseMessage { get; set; }
}

その後、データスキーマのインターフェイスを定義し、コード全体でそのインターフェイスを使用しました。

public interface IResult
{
    bool Succeeded { get; set; }
    string ResponseMessage { get; set; }
}

理由を尋ねると、彼女はそれが「良い習慣」だと言っただけで、不必要な抽象化のように思えたこと以外に文句を言う理由は見つかりませんでした。

これは実際にどこかに良い習慣として文書化されていますか?クラスに純粋なデータのみが含まれ、他には何も含まれないことがわかっている場合、インターフェイスを定義する理由はありますか? (この場合に役立つ場合、JSONのクラスREST APIオブジェクト)

6
Logan S.

この方法で使用する場合、インターフェースは機能を示します。

たとえば、IEnumerableインターフェイスは、問題のクラスが次のように列挙できることを示しています。

foreach(var item in collection)
{
    // Do something with `item`
}

ここで、collectionは、IEnumerableインターフェースを実装するオブジェクトです。つまり、オブジェクト内のアイテムを1つずつウォークスルーして、各アイテムに対して何かを行うことができます。

あなたが説明した場合、IResultは、いくつかの「標準」フィールドを操作するための機能であるように思われます。その結果、クラスに加えて:

public class Result : IResult
{
    public string Succeeded { get; set; }
    public string ResponseMessage { get; set; }
}

..同じインターフェースを実装できる他のクラスもあると思います。可能な例は次のとおりです。

public class Message : IResult
{
    public string Content { get; set; }
    public string Succeeded { get; set; }
    public string ResponseMessage { get; set; }

    public void Foo(int someParameter)
    {
       // ...
    }
}

したがって、インターフェースを使用して2つの異なる実装を提供できます。たとえば、IResultが存在し、モックオブジェクトを使用できる可能性があります。テスト用。

これらの可能性がない場合(つまり、同僚が単一の純粋なデータクラスにinterfaceを実装しているだけです)、私は次のように主張しますインターフェイスは有用な目的はありません、間接的な不要な層を提供する以外は。

8
Robert Harvey

コードが現在立っているように、

public interface IResult
{
    bool Succeeded { get; set; }
    string ResponseMessage { get; set; }
}

単純なデータクラスを複製するインターフェースがあります。コードに複雑さを追加しますが、メリットはありません。それは良い習慣ではありません。

ただし、そのインターフェースを変更した場合:

public interface IResult
{
    bool Succeeded { get; }
    string ResponseMessage { get; }
}

今ではかなり違うものがあります。そのデータは読み取り専用で抽象化されています。したがって、別の変更を行う場合は、今回のクラスに対して:

internal class Result : IResult
{
    public bool Succeeded { get; internal set; }
    public string ResponseMessage { get; internal set; }
} 

これで、内部的に書き込み可能なプロパティとそのデータの読み取り専用のパブリックビューを持つ内部クラスを取得しました。

これにより、JSONシリアライザのコードが満足できます。ほとんどのそのようなライブラリ(あなたがあなたの質問にタグを付けたので、ここでは.NETのために特別に考えています、C#)コンストラクタを介したオブジェクトの作成を適切に処理しません。書き込み可能なプロパティが必要ですが、リフレクションを使用してプロパティを構築するため、これらのプロパティがパブリックであるかどうかは気にしません。

そして、このアプローチは、システムの残りの部分がデータの読み取り専用バージョンへのアクセスのみを与えられるので、残りのコードに利益をもたらします。これにより、不変性の使用が促進され、そのようなオブジェクトをシステム内の1か所に作成する責任が生じます。そして、それらは間違いなく良い習慣です。

6
David Arno

インターフェイス、継承、カプセル化の関係を覚えている方法の1つは次のとおりです。

  • 継承関係です。車はVehicleです。
  • カプセル化には関係があります。車にはTireがあります。
  • Interfaceacts as関係。車はIPeopleTransportとして機能します

それが私が言及する理由は、最初に関係を作るにはreasonが必要だからです。オブジェクトが動作する方法が1つしかない場合、そのオブジェクトへのインターフェースを持つ理由はまったくありません。

acts as関係は、Robert Harveyの回答で表現された感情を意味することに気づくでしょう。

基本的に、このインターフェースでは、それを実装するオブジェクトを、その役割を果たしているかのように扱うことができます。 1つの例は、C#Enumerable.Rangeで、foreach呼び出しで数値を列挙します。

foreach(var i in Enumerable.Range(10,100))
{
   Console.WriteLine(i);
}

あなたが提供した特定の例では、Resultは1つの役割しか持たないため、IResultインターフェースは必要ないと主張します。

5
Berin Loritsch

時期尚早のキーワードインターフェイスはいくつかの問題を軽減します。

  1. そのためのキーワードインターフェースを必要とする言語で多重継承を有効にします(JavaおよびC#)
  2. クライアントを使用する際に名前を変更する必要をなくします(Iプレフィックス、C#のみ)。

どちらも、オープンクローズの原則に厳密に従う必要がある場合にのみ重要です。これにより、テスト済みの動作中のコードに変更を加えないようにすることができます。個別に更新されるコードライブラリを公開している場合、これははるかに重要です。すべてのクラスを変更でき、すべてのバージョンを変更したい場合は、実際のコストはテストをやり直すだけです。リファクタリングツールは、実際のコード変更を簡単にします。

時期尚早のキーワードインターフェイスは、独自の問題を引き起こします。

  1. インダイレクションは、インダイレクションが多すぎることを除き、あらゆる問題を解決できます

「すべてがインターフェースを得る」という考え方は、考えずに作成できる儀式のコードの山と山を作成します。これを行うための「抽出インターフェース」リファクタリングさえあります。これは、新しいフィールドを追加したいだけの場合は、楽しくありません。理想的には、フィールドが存在するべきであるという決定は、1つの場所でのみ行われるべきです。

正直に言いましょう。キーワードインターフェースは、多重継承を適切にサポートする方法を知る前に作成された、これらの言語の壊れた型システムに対する恐ろしいパッチです。

しかし、依存関係の逆転の原則は、抽象化に依存すると述べています!

  • 高レベルのモジュールは、低レベルのモジュールに依存するべきではありません。どちらも抽象化(インターフェース)に依存する必要があります。
  • 抽象化は詳細に依存すべきではありません。詳細(クラス)は抽象化に依存する必要があります。

その通りです。しかし、それでは、抽象化がキーワードインターフェイスである必要があるとは言えません。

キーワードインターフェイスとは、実際に「インターフェイス」という単語を入力するときです。インターフェースには、それ以外の多くの形式があります。継承を無効にしないクラスはすべてインターフェイスです。申し訳ありませんが、Stringはインターフェースではありません。しかし、他のすべてのクラスはそうです。キーワードインターフェイスを持つためにすべてが必要なわけではありません

難しいのは、クラスが優れた抽象化を行う場合と行わない場合の実現です。保護されたコンストラクターは、クラスが具体的であることをクライアントが知らないようにするのに役立ちます。クライアントが具体的なことを知らなければ、あなたはインターフェースです。これですべてです。怠惰な建築家は、すべてにキーワードインターフェイスを与えることを主張することで、これをチェックする必要性をスキップしようとします。しかし、これをチェックする少しの作業により、実際にポリモーフィズムをサポートする必要がある前に、キーワードインターフェースの作成を延期することができます。

私の例Java損傷した脳:

class Report {
    public print() {
        System.out.println(
            "Succeeded: " + result.succeeded() + " " + 
            "Response Message: " + result.responseMessage() 
        );
    }
    Report(Result result) {
        this.result = result;
    }
    Result result;
} 

ReportResultに依存しているようですね?ええ、でも何? Resultは具体的だと誰が言ったのですか? ReportnewResultを使用しません。すべてのReportが知っているResultはキーワードインターフェイスです。 Reportは、多くのオブジェクトを含むコンポジションである可能性があります。わかりません。知らないことはあなたが保存する必要があるものです。知らないことが、Resultを抽象化する理由です。それを行うためにすべてにキーワードインターフェイスを与える必要はありません。あなたがそれをする必要があるときにそれをしてください。

もしあなたのプログラマーがこれの言い訳としてSOILDを呼び出そうとした場合、マーティン氏自身から少し何かをお見せしましょう。

enter image description here

モデルにはインターフェースがまったくないことに注意してください。これは、モデルが決して変更されないという意味ではありません。後でインターフェイスを追加する機能を保持するだけで、今すぐ追加する必要はありません。

ここに表示されているインターフェイスは、独立した変更が発生する可能性が高い境界を越えて、クライアントが依存する動作を正確に知ることからクライアントを保護しています。モデルには動作がないため、境界はモデルにとってそれほど重要ではありません。

2
candied_orange

インターフェイスの使用方法によって異なります。私たちのドメインからの実際の有用な例

public interface ICreated
{
    string CreatedBy { get; set; }
    DateTime Created { get; set; }
}

public class CreatedListener : IBusinessContextListener
{
    public void OnSavingChanges(IBusinessContext ctx)
    {
        foreach (var created in ctx.Db.ChangeTracker.Entries<ICreated>().Where(c => c.State == EntityState.Added))
        {
            created.Entity.CreatedBy = ctx.Username;
            created.Entity.CreatedUTC = DateTime.UtcNow;
        }
    }

    public void OnTransactionCommited(IBusinessContext ctx)
    {
    }
}

collection
   .AddScoped<IBusinessContextListener, CreatedListener>()
   .AddScoped<IBusinessContextListener, UpdatedListener>();

ここでは、2つのことのためにインターフェースを使用します。これら2つがなければ、インターフェースは冗長になります。

0
Anders