私は同僚のコードをレビューしていますが、変更の一部として、この人は次のような新しい純粋なデータクラスをいくつか導入しました(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オブジェクト)
この方法で使用する場合、インターフェースは機能を示します。
たとえば、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
を実装しているだけです)、私は次のように主張しますインターフェイスは有用な目的はありません、間接的な不要な層を提供する以外は。
コードが現在立っているように、
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か所に作成する責任が生じます。そして、それらは間違いなく良い習慣です。
インターフェイス、継承、カプセル化の関係を覚えている方法の1つは次のとおりです。
Vehicle
です。Tire
があります。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
インターフェースは必要ないと主張します。
時期尚早のキーワードインターフェイスはいくつかの問題を軽減します。
どちらも、オープンクローズの原則に厳密に従う必要がある場合にのみ重要です。これにより、テスト済みの動作中のコードに変更を加えないようにすることができます。個別に更新されるコードライブラリを公開している場合、これははるかに重要です。すべてのクラスを変更でき、すべてのバージョンを変更したい場合は、実際のコストはテストをやり直すだけです。リファクタリングツールは、実際のコード変更を簡単にします。
時期尚早のキーワードインターフェイスは、独自の問題を引き起こします。
「すべてがインターフェースを得る」という考え方は、考えずに作成できる儀式のコードの山と山を作成します。これを行うための「抽出インターフェース」リファクタリングさえあります。これは、新しいフィールドを追加したいだけの場合は、楽しくありません。理想的には、フィールドが存在するべきであるという決定は、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;
}
Report
はResult
に依存しているようですね?ええ、でも何? Result
は具体的だと誰が言ったのですか? Report
はnew
でResult
を使用しません。すべてのReport
が知っているResult
はキーワードインターフェイスです。 Report
は、多くのオブジェクトを含むコンポジションである可能性があります。わかりません。知らないことはあなたが保存する必要があるものです。知らないことが、Result
を抽象化する理由です。それを行うためにすべてにキーワードインターフェイスを与える必要はありません。あなたがそれをする必要があるときにそれをしてください。
もしあなたのプログラマーがこれの言い訳としてSOILDを呼び出そうとした場合、マーティン氏自身から少し何かをお見せしましょう。
モデルにはインターフェースがまったくないことに注意してください。これは、モデルが決して変更されないという意味ではありません。後でインターフェイスを追加する機能を保持するだけで、今すぐ追加する必要はありません。
ここに表示されているインターフェイスは、独立した変更が発生する可能性が高い境界を越えて、クライアントが依存する動作を正確に知ることからクライアントを保護しています。モデルには動作がないため、境界はモデルにとってそれほど重要ではありません。
インターフェイスの使用方法によって異なります。私たちのドメインからの実際の有用な例
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つがなければ、インターフェースは冗長になります。