web-dev-qa-db-ja.com

コンストラクターに非契約パラメーターがある場合のデカップリング

非契約パラメータとは、class Person(string name)のような、インターフェイスやサービスの依存関係ではないパラメータを意味します。

私はWebページのスクレイピングアプリケーションを作成していますが、これまでのところ、間違った順序で作成しています(そのため、この質問が作成されました)。 (文字列形式から)HTMLドキュメントを解析し、その中のすべてのURLと画像を取得するクラスを作成しました。

そのクラスには次の説明があります。

public class PageParser
{
    private readonly string _html;

    public PageParser(string html) {
        _html = html;
    }

    public IEnumerable<string> GetImages() {
        /* not important */
    }

    public IEnumerable<string> GetLinks() {
        /* not important */
    }   
}

このコードは、ユニットテスト済み、100%カバレッジなど、うまく機能します。問題は、このクラスのコンストラクターを気にせずに、このクラスを使用するコードのユニットテストをどのように作成するかです。 PageParserの動作を処理するには、この新しいクラスのみが必要です。

この新機能の擬似コードは次のとおりです。

public Report CreateReport(string url)
{
    var html = _webClient.DownloadString(url); 

    var parser = new PageParser(html);  

    var images = parser.GetImages();
    var links = parser.GetLinks();

    var relevantLinks = links.Where(l => l.Contains('something'));

    return _reportBuilder.Create(images, relevantLinks);
}

私が抱えている問題は、CreateReportメソッドが持つことができるすべてのさまざまなテストケースに対してエピックサイズのHTMLドキュメントを含む単体テストの束を持ちたくないということです。

私が思いついたオプションは次のとおりです。

1)すべてそのままにします

クラス構造はそのままにして、大きなセットアップ変数を使用したテストを実行します。

2)ファクトリからページパーサー(+インターフェイス)を公開します

PageParserのインターフェースを作成し、対応するインターフェースを持つファクトリーを作成します。

interface IPageParserFactory
{
    IPageParser Create(string html);
}

class PageParserFactory : IPageParserFactory
{
    IPageParser Create(string html)
    {
        return new PageParser(html);
    }
}

その場合、結果の擬似コードは次のようになります。

// ... download string

var pageParser = _pageParserFactory.Create(html);

// ... create report

)パラメータhtmlを各メソッドに移動し、インターフェイスを作成します(したがって、クラスには構造の依存関係がありません)

interface IPageParser
{
    IEnumerable<string> GetImages(string html);
    IEnumerable<string> GetLinks(string html);
}

4)完全に他の何か

この問題を解決する別のパターンがある場合、それを知りたいと思います。

5
Matthew

一見すると、CreateReportメソッドは、多くのことを行うため、単一責任の原則に違反しているように見えます。

  • DownloadStringを介してURLからHTMLを読み取ります
  • PageParserを作成し、その解析結果を取得します
  • いくつかの関連性基準に基づいて結果をフィルタリングします
  • レポートの実際の作成を行います

メソッド名を考えると、そのメソッドにはその最後の責任しかないと予想されます。代わりにここで提供したのは、データの取得、解析、およびフィルタリングのロジックと高度に結合されたものです。

レポートを作成するという考えは、通常、すでに準備されたある種のデータに基づいています。では、すでに疑問に思われるようになりましたが、なぜメソッドがURLを入力として使用するのでしょうか。

提案された解決策1)から3)まで、noneはこれらの問題に実際に対処します。これらのいずれかを実装する場合でも、単体テスト用のほぼ完全なHTMLコンテンツを含むローカルファイルを作成する必要があります。

一般的に遅すぎるため、ユニットテストでファイルシステムにアクセスしてはならないと主張する人さえいることに注意してください。もちろん、これに反対する人もいるかもしれませんが、少なくとも少し罪悪感を感じるはずです。そうするのには非常に正当な理由がある場合にのみそうします。この場合はそうしません。 。

では、オプション4)を使いましょう。問題を別の見方をすると、実際にはある種のドキュメントデータを処理することがすべてであることがわかります。あなたのPageParserはそのようなデータを作成する最初のオブジェクトなので、個々のIEnumerablesではなく単一のPageDataオブジェクトを返すようにデータを変更するとどうなるでしょうか。 IEnumerablesを提供します。

ただし、この行を続けると、レポートの生成には実際にはPageDataオブジェクトのみが必要です。 _reportBuilder.Create(images, relevantLinks)_reportBuilder.Create(pageData)の呼び出しになり、CreateReportはURLの代わりにPageDataを入力として受け取ります。実際、CreateReportはその1つのCreate行に短縮される場合があります。とにかく、他のすべてはその責任ではありませんでした。

これらのクラス(PageParserPageData、レポートジェネレータークラス)のいずれかの外部で、独立したPageDataFilterインターフェイスと実装クラスを定義できます。それらの1つは、CreateReportにあるフィルターになります。インターフェースは、PageData ApplyFilter(PageData pageData)のような単純な1メソッドのフィルタリングであることに注意してください。このようなフィルターを処理すると、個々のフィルターのテストが容易になり、フィルターを簡単に構成でき、他のすべてのフィルターから分離できるという利点があります。レポートジェネレータは、指定されたPageDataが何らかの方法でフィルタリングされたかどうかを気にする必要はありません。 (実装では、PageDataが不変であるか、またはインプレースのフィルターで変更できるかを決定する必要があることに注意してください。最初のオプションは数学的に適切で、推論が容易で、並列化などに適しています。一方、後者はメモリとCPUの点でより効率的です。)

この時点で、PageParser、レポートジェネレーター、およびリンクフィルターを簡単にテストできます。さらに、メモリ内に必要なクラスの1つの単純なインスタンスを準備するだけで、すべてのテストを実行できます。 URLの読み込み、PageParserテスト以外の複雑なHTMLの処理、ファイルシステムへのアクセス、テストで処理する必要のあるカップリングは不要です。

補遺:上記のテキストに欠けているのは、これらのコンポーネントがどのように相互に配線されているかです。 PageParserを作成するPageDataがあり、PageDataFilterインスタンスを介してフィルタリングし、ReportCreatorを介してレポートを作成できるとします。まだ必要なのは、これらのクラスのインスタンス化とインストルメンテーションです。そのための適切な場所がどこにあるかは、アプリケーションの設計によって異なります。ただし、簡単にするために、Applicationメソッドを持つmainクラスを想定できます。その方法では、次のようにします。

  • HTMLURLを文字列にダウンロードする
  • この文字列をコンストラクター引数として使用してPageParserをインスタンス化します
  • 必要なPageDataFilterオブジェクトをインスタンス化します
  • PageDataFilterオブジェクトをPageDataが提供するPageParserに適用します
  • レポートを作成するために、ReportCreatorをインスタンス化し、フィルタリングされたPageDataを与えます。

繰り返しになりますが、これらが手動でインスタンス化されているか、依存性が注入されているか、または何を持っているかは、全体的な設計とアーキテクチャに依存します。ただし、重要な点は、クラスを小さくし、唯一の責任に限定してから、外部からインストルメント化することです。インストルメンテーションコードが大きくなり、多くのことを実行する場合は、上記のアイデアをそのコードにも再帰的に適用します。

11
Frank