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


14

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

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

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

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

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


2
説明しているのは、関数型プログラミングの重要な側面である高階関数です
ロバートハーベイ

2
Funcパラメータの名前を指定できるので、代わりにデリゲートを使用します。ここで、その意図を述べることができます。
ステファンハンケ

1
関連:stackoverflow:ioc-factory-pros-and-contras-for-interface-versus-delegates。stackoverflowの質問には「工場」の特殊なケースに絞り込むながら質問は、より一般的である@TheCatWhisperer
K3B

回答:


11

インターフェイスに含まれる関数が1つだけであり、2つの名前(インターフェイス名インターフェイス内の関数名)を導入する説得力のある理由がない場合、Func代わりに使用することで、不要な定型コードを回避でき、ほとんどの場合は望ましいです- DTOの設計を開始して、1つのメンバー属性のみが必要であることを認識するときと同じです。

私は多くの人々がより多く使用するために使用されていると思いinterfaces依存性注入とIoCのが人気を集めた時には実際の同等がなかったので、FuncクラスはJavaやC ++で(私もわからない場合は午前FuncC#で、その時点で利用可能でした)。したがって、多くのチュートリアル、例、または教科書はinterface、たとえ使用Funcすることがよりエレガントであっても、この形式を依然として好んでいます。

あなたは、に見えるかもしれません私の元の答えインターフェイス分離原則とラルフウェストファールのフローデザイン・アプローチについて。このパラダイムは、すでに自分で(およびその他の)で述べたのとまったく同じ理由で、FuncパラメーターのみでDIを実装します。あなたが見るように、あなたのアイデアは本当に新しいものではなく、まったく逆です。

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


このプログラムは、インターフェイス分離の原則を念頭に置いて設計されていませんでした。基本的には、抽象クラスとして使用されています。これが、私がこのアプローチを採用した理由のさらなる理由です。インターフェースはよく考えられておらず、とにかく1つのクラスだけがそれらを実装するため、ほとんど役に立ちません。インターフェイス分離の原則は、特にFuncがあるので、極端なものにする必要はありませんが、私の考えでは、それは依然として非常に重要です。
-TheCatWhisperer

1
このプログラムは、インターフェイス分離の原則を念頭に置いて設計されていませんでした -あなたの説明によると、おそらく、この用語があなたの種類の設計に適用できることを知らなかっただけです。
Doc Brown

Doc、前任者が作成したコードを参照していました。これはリファクタリングを行っており、私の新しいクラスはやや依存しています。私はこのクラスの依存関係を含め、今後のプログラムの他の部分に大きな変更を加えることを計画しているため、私はこのアプローチを行った理由の一部はある
TheCatWhisperer

1
「...依存関係の注入とIoCが普及し始めたとき、Funcクラスに相当するものはありませんでした」という文書はまさに答えたとおりですが、十分な自信はありませんでした!その疑いを検証してくれてありがとう。
グラハム

9

IoCの主な利点の1つは、インターフェイスに名前を付けることですべての依存関係に名前を付けることができ、コンテナーが型名を一致させることでコンストラクターに提供する依存関係を認識できることです。これは便利であり、依存関係のより記述的な名前を許可しますFunc<string, string>

また、単一の単純な依存関係であっても、複数の関数が必要になる場合があります-インターフェイスにより、これらの関数を自己文書化する方法でグループ化できますFunc<string, string>Func<string, int>

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


元の実装者がいなくなったときに誰かのコードをデバッグする必要がありましたが、最初の経験から、スタックのどこでどのラムダが実行されているかを見つけることは多くの作業であり、本当にイライラするプロセスです。元の実装者がインターフェイスを使用していた場合、探していたバグを見つけるのは簡単でした。
cwap

ちょっと、私はあなたの痛みを感じます。ただし、私の特定の実装では、ラムダはすべて1つの関数を指す1つのライナーです。それらはすべて単一の場所で生成されます。
-TheCatWhisperer

3

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

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

欠点については:

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

1
IoCコンテナーを使用して、従来の方法でファクトリーを作成します。その後、ファクトリーはインターフェースメソッドをラムダにパッケージ化し、最初のポイントで述べた問題を回避します。良い点。
-TheCatWhisperer

1

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

クラスを注入する

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. ラムダ式自体を単体テストすることは問題です。

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


指定する必要がありますが、作成しているクラスはプロセスロジッククラスです。情報に基づいた決定を下すために必要なデータを取得するために外部クラスに依存しています。ロガーなどのクラスを誘発する副作用は使用されません。あなたの例の問題は、特にIoCコンテナを使用するコンテキスト内で、オブジェクト指向が貧弱であることです。クラスに複雑さを追加するifステートメントを使用する代わりに、単純に不活性ロガーを渡す必要があります。また、ラムダにロジックを導入すると、実際にテストが難しくなります...その使用目的を破って、それが実行されない理由です。
TheCatWhisperer

ラムダは、アプリケーションのインターフェイスメソッドを指し、ユニットテストで任意のデータ構造を構築します。
TheCatWhisperer

なぜ人々は常に明らかにarbitrary意的な例の細部に焦点を当てるのですか?あなたが適切なロギングについて話すことを主張するなら、場合によっては、不活性ロガーは恐ろしいアイデアになるでしょう。例えば、ログに記録される文字列が計算に費用がかかる場合です。
ジョン・ウー

「ラムダがインターフェイスメソッドを指している」という意味がわかりません。デリゲートシグネチャに一致するメソッドの実装を挿入する必要があると思います。メソッドがインターフェイスに属している場合、それは偶発的なものです。コンパイルまたは実行時のチェックは行われません。誤解していますか?おそらくあなたの投稿にコード例を含めることができますか?
ジョン・ウー

ここであなたの結論に同意するとは言えません。1つには、クラスを最小限の依存関係に結合する必要があります。したがって、ラムダを使用すると、注入された各項目が1つの依存関係であり、インターフェイス内の複数のものの混同ではないことが保証されます。Worker一度に1つの実行可能な依存関係を構築するパターンをサポートし、すべての依存関係が満たされるまで各ラムダを個別に注入できるようにすることもできます。これは、FPの部分アプリケーションに似ています。さらに、ラムダを使用すると、依存関係間の暗黙的な状態と隠れた結合を排除できます。
アーロンM.エシュバッハ

0

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

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と、と思います「一つだけの機能があります。することができること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、その後、事前にレンダリングされたテンプレートを受け入れる必要がある場合があります。

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

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

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