web-dev-qa-db-ja.com

IoCのインターフェイスの代わりにFuncを使用する

コンテキスト:C#を使用しています

私はクラスを設計し、それを分離して単体テストを簡単にするために、すべての依存関係を渡します。内部ではオブジェクトのインスタンス化は行われません。ただし、必要なデータを取得するためにインターフェイスを参照する代わりに、必要なデータ/動作を返す汎用Funcを参照しています。依存関係を注入するときは、ラムダ式を使用するだけです。

私にとっては、ユニットテスト中に面倒なモックを行う必要がないため、これはより良いアプローチのようです。また、周囲の実装に根本的な変更がある場合は、ファクトリクラスを変更するだけで済みます。ロジックを含むクラスを変更する必要はありません。

ただし、IoCがこのように行われるのをこれまで見たことがありません。そのため、見落としている潜在的な落とし穴があると思います。私が考えることができる唯一のものは、Funcを定義しない以前のバージョンのC#とのマイナーな非互換性であり、これは私の場合の問題ではありません。

より具体的なインターフェイスではなく、InoCのFuncなどの汎用デリゲート/高次関数の使用に問題はありますか?

15
TheCatWhisperer

インターフェースに含まれる関数が1つだけで、それ以上ではなく、2つの名前を導入する説得力のある理由がない場合(インターフェース名およびインターフェイス内の関数名)、代わりにFuncを使用すると、不要なボイラープレートコードを回避でき、ほとんどの場合、DTOの設計を開始してそれが必要であると認識した場合と同様に、望ましいメンバー属性は1つだけです。

依存性注入とIoCが一般的になった頃は、JavaまたはC++にinterfacesクラスに相当するものがなかったため、多くの人がFuncの使用に慣れていると思います(確信が持てません) Funcがその時点でC#で利用可能だった場合)。したがって、interfaceを使用する方がエレガントな場合でも、多くのチュートリアル、例、または教科書は、Funcフォームを優先します。

インターフェースの分離原理とラルフウェストファルのフローデザインアプローチについて 私の以前の回答 を調べてみてください。このパラダイムマは、Funcパラメーターを排他的に使用してDIを実装します。これは、あなた自身が既に述べたのとまったく同じ理由で(そしていくつかあります)。ご覧のとおり、あなたのアイデアは本当に新しいものではなく、まったく逆です。

そして、はい、私はこのアプローチを自分で、量産コード、各ステップの単体テストを含むいくつかの中間ステップを持つパイプラインの形でデータを処理する必要があるプログラムに使用しました。それから、私はあなたにそれが非常にうまく働くことができる直接の経験を与えることができます。

11
Doc Brown

IoCの主な利点の1つは、インターフェイスに名前を付けることですべての依存関係に名前を付けることができ、コンテナーは型名を照合することでコンストラクターに提供する依存関係を認識できることです。これは便利で、Func<string, string>よりもわかりやすい依存関係の名前を使用できます。

また、単一の単純な依存関係でも、複数の関数が必要になる場合があることもよくあります。インターフェイスを使用すると、これらの関数を自己文書化する方法でグループ化できます。複数のパラメータを使用すると、Func<string, string>のようにすべて読み取ることができます。 、Func<string, int>

依存関係として渡されるデリゲートを単純に持つと便利な場合があります。デリゲートを使用する場合と、メンバーが非常に少ないインターフェイスを使用する場合とでは、判断が必要です。議論の目的が本当に明確でない限り、私は通常、自己文書化コードを作成する側で誤りを犯します。すなわち。インターフェースを書く。

9
Erik

より具体的なインターフェイスではなく、InoCのFuncなどの汎用デリゲート/高次関数の使用に問題はありますか?

あんまり。 Funcは独自のインターフェースです(英語の意味で、C#の意味ではありません)。 「このパラメーターは、尋ねられたときにXを提供するものです。」 Funcには、必要なときにのみ情報を遅延して提供するという利点さえあります。私はこれを少し行い、適度にお勧めします。

欠点については:

  • IoCコンテナは、カスケードされた方法で依存関係を結び付けるためにいくつかの魔法をよく使います。いくつかのものがTで、いくつかのものがFunc<T>
  • Funcsにはいくつかの間接性があるため、デバッグして推論するのが少し難しい場合があります。
  • Funcsはインスタンス化を遅延させます。つまり、テスト中にランタイムエラーが奇妙なときに表示されるか、まったく表示されない可能性があります。また、操作の順序の問題が発生する可能性が高くなり、使用方法によっては、初期化順序のデッドロックが発生する可能性があります。
  • Funcに渡すのは、わずかなオーバーヘッドとそれに伴う複雑さを伴うクロージャです。
  • Funcの呼び出しは、オブジェクトに直接アクセスするよりも少し遅くなります。 (重要なプログラムで気付くほどではありませんが、そこにあります)
3
Telastyn

簡単な例を見てみましょう-おそらく、ロギングの手段を注入しています。

クラスの挿入

class Worker: IWorker
{
    ILogger _logger;

    Worker(ILogger logger)
    {
        _logger = logger;
    }
    void SomeMethod()
    {
        _logger.Debug("This is a debug log statement.");
    }
}        

何が起こっているのかはかなりはっきりしていると思います。さらに、IoCコンテナーを使用している場合は、明示的に何かを注入する必要すらなく、コンポジションルートに追加するだけです。

container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();

Workerをデバッグする場合、開発者はコンポジションルートを調べて、どの具象クラスが使用されているかを判断する必要があります。

開発者がより複雑なロジックを必要とする場合、彼は操作するための完全なインターフェースを持っています:

    void SomeMethod()
    { 
       if (_logger.IsDebugEnabled) {
           _logger.Debug("This is a debug log statement.");
       }
    }

メソッドの注入

class Worker
{
    Action<string> _methodThatLogs;

    Worker(Action<string> methodThatLogs)
    {
        _methodThatLogs = methodThatLogs;
    }
    void SomeMethod()
    {
        _methodThatLogs("This is a logging statement");
    }
}        

まず、コンストラクタパラメータの名前がmethodThatLogsになっていることに注意してください。これは、Action<string>が行うことになっています。インターフェースを使用すると、それは完全に明確でしたが、ここではパラメーターの命名に依存する必要があります。これは本質的に信頼性が低く、ビルド中に実行するのが難しいようです。

では、このメソッドをどのように注入するのでしょうか。まあ、IoCコンテナーはそれを行いません。そのため、Workerをインスタンス化するときに、明示的に注入することになります。これはいくつかの問題を引き起こします:

  1. Workerをインスタンス化するのはもっと大変です
  2. Workerをデバッグしようとする開発者は、どの具象インスタンスが呼び出されるかを理解するのがより困難であることに気付くでしょう。彼らは作曲のルートを単に調べることはできません。コードをたどる必要があります。

もっと複雑なロジックが必要な場合はどうでしょうか?あなたのテクニックは1つのメソッドしか公開していません。ここで、複雑なものをラムダに焼くことができると思います。

var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );

しかし、単体テストを作成するとき、そのラムダ式をどのようにテストしますか?匿名なので、単体テストフレームワークは直接インスタンス化できません。たぶんあなたはそれを行うための巧妙な方法を理解することができますが、それはおそらくインターフェースを使用するよりも大きなPITAになるでしょう。

違いの概要:

  1. メソッドのみを注入すると、目的を推測することが難しくなりますが、インターフェースは目的を明確に伝えます。
  2. メソッドのみをインジェクトすると、インジェクションを受け取るクラスに公開される機能が少なくなります。今日は必要ないとしても、明日は必要になるかもしれません。
  3. IoCコンテナを使用するメソッドのみを自動的に挿入することはできません。
  4. 特定のインスタンスでどの具象クラスが機能しているかは、コンポジションルートからはわかりません。
  5. ラムダ式自体を単体テストすることは問題です。

上記のすべてに問題がなければ、メソッドだけを注入しても問題ありません。それ以外の場合は、伝統に固執し、インターフェイスを注入することをお勧めします。

1
John Wu

私がずっと前に書いた次のコードを考えてみましょう:

public interface IPhysicalPathMapper
{
    /// <summary>
    /// Gets the physical path represented by the relative URL.
    /// </summary>
    /// <param name="relativeURL"></param>
    /// <returns></returns>
    String GetPhysicalPath(String relativeURL);
}

public class EmailBuilder : IEmailBuilder
{
    public IPhysicalPathMapper PhysicalPathMapper { get; set; }
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templateRelativeURL, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(this.PhysicalPathMapper.GetPhysicalPath(templateRelativeURL));

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

テンプレートファイルの相対位置を取得し、メモリに読み込み、メッセージ本文をレンダリングし、電子メールオブジェクトを組み立てます。

IPhysicalPathMapperを見て、「関数は1つしかありません。それはFuncかもしれません」と考えるかもしれません。しかし実際には、ここでの問題はIPhysicalPathMapperが存在するべきではないということです。より良い解決策は、単にパスをパラメータ化するです。

public class EmailBuilder : IEmailBuilder
{
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templatePath, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(templatePath);

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

これは、このコードを改善するために他の多くの質問を引き起こします。たとえば、EmailTemplateだけを受け入れる必要がある場合もあれば、事前にレンダリングされたテンプレートを受け入れる必要がある場合もあれば、インライン化される場合もあります。

これが、制御の逆転が広範囲にわたるパターンとして嫌いな理由です。これは、すべてのコードを作成するためのこの神のようなソリューションとして一般に支持されています。しかし実際には、それを(控えめにではなく)広範に使用すると、多くの不要なインターフェースを導入するように促すことにより、コードが大幅に悪化します。これらは完全に逆向きに使用されます。 (呼び出し側は、クラス自体が呼び出しを呼び出すのではなく、これらの依存関係を評価し、結果を渡す責任を実際に負う必要があるという意味で、逆方向です。)

インターフェイスは控えめに使用する必要があり、制御の反転と依存性注入も控えめに使用する必要があります。それらが大量にある場合、コードを解読するのがはるかに難しくなります。

0
jpmc26