コレクションを表すクラスの反復:IEnumerable <T>とカスタムメソッド


9

何かの列挙/コレクションであるクラスを実装する必要があることがよくあります。このスレッドのために考えてみましょう不自然な例IniFileContentラインの列挙/コレクションです。

このクラスがコードベースに存在しなければならない理由は、ビジネスロジックがいたるところに広がらないようにする(=をカプセル化するwhere)ためであり、可能な限りオブジェクト指向の方法でそれを実行したいからです。

通常、以下のように実装します。

public sealed class IniFileContent : IEnumerable<string>
{
    private readonly string _filepath;
    public IniFileContent(string filepath) => _filepath = filepath;
    public IEnumerator<string> GetEnumerator()
    {
        return File.ReadLines(_filepath)
                   .Where(l => !l.StartsWith(";"))
                   .GetEnumerator();
    }
    public IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

IEnumerable<string>使用が便利になるので、実装を選択します。

foreach(var line in new IniFileContent(...))
{
    //...
}

しかし、そうすることでクラスの意図を「隠す」のだろうか?人がIniFileContentインターフェースを見たときだけ見るEnumerator<string> GetEnumerator()。クラスが実際に提供しているサービスは自明ではないと思います。

次に、この2番目の実装を検討します。

public sealed class IniFileContent2
{
    private readonly string _filepath;
    public IniFileContent2(string filepath) => _filepath = filepath;
    public IEnumerable<string> Lines()
    {
        return File.ReadLines(_filepath)
                   .Where(l => !l.StartsWith(";"));
    }
}

これはあまり便利ではありません(ちなみに、new X().Y()クラスの設計に問題があるように見えます)。

foreach(var line in new IniFileContent2(...).Lines())
{
    //...
}

しかし、IEnumerable<string> Lines()このクラスが実際に何ができるかを明らかにする明確なインターフェースがあります。

どの実装を促進しますか、そしてその理由は?暗黙のうちに、何かの列挙を表すためにIEnumerableを実装することは良い習慣ですか?

私は次の方法についての答えを探していません。

  • このコードの単体テスト
  • クラスの代わりに静的関数を作る
  • このコードを将来のビジネスロジックの進化の影響を受けやすくする
  • パフォーマンスを最適化する

付録

ここに私のコードベースに実装されている実際のコードの種類がありますIEnumerable

public class DueInvoices : IEnumerable<DueInvoice>
{
    private readonly IEnumerable<InvoiceDto> _invoices;
    private readonly IEnumerable<ReminderLevel> _reminderLevels;
    public DueInvoices(IEnumerable<InvoiceDto> invoices, IEnumerable<ReminderLevel> reminderLevels)
    {
        _invoices = invoices;
        _reminderLevels = reminderLevels;
    }
    public IEnumerator<DueInvoice> GetEnumerator() => _invoices.Where(invoice => invoice.DueDate < DateTime.Today && !invoice.Paid)
                                                               .Select(invoice => new DueInvoice(invoice, _reminderLevels))
                                                               .GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}


2
更新された質問は2つのコーディングスタイルに関する意見を求めているため、これを閉じるために投票すると、完全に意見に基づいたものになります。
デビッドアルノ

1
@DavidArno私は、何かが良い習慣であるかどうかを尋ねることは、完全に意見に基づくのではなく、事実、経験、規範にも基づいているという意味で同意しません。
発見

どちらのパターンも私にはうまくいきません:具体的なクラスへの依存、ファイルシステムへの強い依存、あいまいな破棄規則(リークがあると思います)、newこのユースケースでの使用の直感的でないセマンティクス、ユニットテストの難しさ、I /例外が発生する可能性があります。ごめんなさい、失礼なことはしていません。依存性注入のメリットに慣れすぎたと思います。
ジョン・ウー

@JohnWuこれは人為的な例であると考えてください(私が告白する不器用に選択されました)。この質問に答えたい場合は(失礼でもない)、IEnumerableを実装するかどうかの設計上の決定に焦点を当てることを検討してIniFileContentください。
発見

回答:


13

まったく異なるアプローチを提案する前に、私はあなたのアプローチを検討しています。私は別のアプローチを好んでいますが、あなたのアプローチに欠陥がある理由を説明することは重要なようです。


IEnumerable<string>使い方が便利になるので実装を選択

利便性は正確さを上回ってはなりません。

あなたのMyFileクラスにはこれ以上のロジックが含まれるのでしょうか。それはこの答えの正確さに影響を与えるからです。私は特に興味があります:

.Where(l => ...) //some business logic for filtering

これが複雑または十分に動的である場合、そのロジックをコンシューマに提供する前にコンテンツをフィルタリングすることを明らかにしない名前のクラスで非表示にしているためです。
私の一部は、このフィルターロジックがハードコード化されていること(たとえば、.iniファイル#がコメントで始まる行を.iniファイルがどのように考慮するかなどのコメント行を無視する単純なフィルター)がファイル固有のルールセットではないことを期待/想定しています。


public class MyFile : IEnumerable<string>

単数形(File)が複数形()を表すことについては、本当にすごいことがいくつかありIEnumerableます。ファイルは単一のエンティティです。内容だけではありません。また、メタデータ(ファイル名、拡張子、作成日、変更日など)も含まれています。

人間はその子供たちの合計以上のものです。車は部品の合計以上のものです。絵画は絵の具とキャンバスのコレクション以上のものです。そして、ファイルは単なる行の集まりではありません。


あなたのMyFileクラスがこの行の列挙だけよりも多くのロジックを含むことはないと想定している場合(そして、Where単純な静的ハードコードされたフィルターのみが適用されます)、ここで得られるのは、「ファイル」名とその意図された指定の紛らわしい使用法です。 。これは、クラスの名前をに変更することで簡単に修正できますFileContent。意図した構文が保持されます。

foreach(var line in new FileContent(@"C:\Folder\File.txt"))

また、意味論的な観点からも意味があります。ファイルの内容は、別々の行に分割できます。これは、ファイルのコンテンツがバイナリではなくテキストであることを前提としていますが、それでも十分です。


ただし、MyFileクラスにさらに多くのロジックが含まれる場合は、状況が変化します。これが発生する可能性があるいくつかの方法があります。

  • このクラスを使用して、ファイルのコンテンツだけでなく、ファイルのメタデータを表現します。

これを開始すると、ファイルはディレクトリ内のファイルを表します。これは単なるコンテンツではありません。
ここでの正しいアプローチは、あなたがで行ったことMyFile2です。

  • Where()フィルタは、異なるファイルを異なるフィルタされている起動時にハードコードされていない複雑なフィルタロジック、例えばを持つ開始します。

これを開始すると、ファイルには独自のカスタムフィルターがあるため、ファイルには独自のIDが割り当てられます。この手段は、あなたのクラスは、よりになったことFileTypeよりをFileContent。2つの動作は分離するか、コンポジションを使用して組み合わせるか(これによりMyFile2アプローチが優先されます)、またはできれば両方(動作FileTypeとのクラスを分離し、FileContent両方をMyFileクラスに構成する)にする必要があります。


まったく異なる提案。

現状では、あなたの両方MyFileとは、MyFile2純粋な存在は、あなたの周りのラッパー与えるために.Where(l => ...)フィルタを。次に、静的メソッド(File.ReadLines())をラップするクラスを効果的に作成していますが、これは優れたアプローチではありません。

余談ですが、クラスを作成することを選択した理由がわかりませんsealed。どちらかと言えば、継承が最大の機能になると思います:異なるフィルタリングロジックを持つ異なる派生クラス(単一の値を変更するためだけに継承を使用するべきではないため、単純な値の変更よりも複雑であると想定しています)

私はあなたのクラス全体を次のように書き直します:

foreach(var line in File.ReadLines(...).Where(l => ...))

この単純化されたアプローチの唯一の欠点はWhere()、ファイルのコンテンツにアクセスするたびにフィルターを繰り返す必要があることです。それは望ましくないことに同意します。

ただし、再利用可能なWhere(l => ...)ステートメントを作成するときに、そのクラスにを強制的に実装させるのはやり過ぎですFile.ReadLines(...)。本当に必要以上にバンドルしています。

静的メソッドをカスタムクラスでラップする代わりに、独自の静的メソッドでラップする方がはるかに適していると思います。

public static IEnumerable<string> GetFilteredFileContent(string filePath)
{
    return File.ReadLines(filePath).Where(l => ...);
}

異なるフィルターがあると想定すると、適切なフィルターをパラメーターとして渡すことができます。複数のフィルターを処理できる例を示します。これは、再利用性を最大化しながら、必要なすべてを処理できるはずです。

public static class MyFile
{
    public static Func<string, bool> IgnoreComments = 
                  (l => !l.StartsWith("#"));

    public static Func<string, bool> OnlyTakeComments = 
                  (l => l.StartsWith("#"));

    public static Func<string, bool> IgnoreLinesWithTheLetterE = 
                  (l => !l.ToLower().contains("e"));

    public static Func<string, bool> OnlyTakeLinesWithTheLetterE = 
                  (l => l.ToLower().contains("e"));

    public static IEnumerable<string> ReadLines(string filePath, params Func<string, bool>[] filters)
    {
        var lines = File.ReadLines(filePath).Where(l => ...);

        foreach(var filter in filters)
            lines = lines.Where(filter);

        return lines;
    }
}

そしてその使用法:

MyFile.ReadLines("path", MyFile.IgnoreComments, MyFile.OnlyTakeLinesWithTheLetterE);

これは、静的メソッドがここでクラスを作成するよりも意味があることを証明するための、ミルの例の実行にすぎません。

フィルターの実装の詳細に追いつかないでください。必要に応じてそれらを実装できます(Func<>リファクタリングへの拡張性と適応性のため、個人的にはパラメーター化が好きです)。しかし、実際に使用するフィルターの例ではなかったので、実行可能な例を示すためにいくつかの仮定をしました。


new X().Y()クラスのデザインに問題があったように見える)

あなたのアプローチnew X().Yでは、グレーティングの少ないものにすることができます。

しかし、あなたの嫌いなnew X().Y()ことは、クラスがここでは正当化されていないように感じる点を証明していると思いますが、メソッドはそうです。静的であることによって、クラスなしでのみ表すことができます。


クラスの名前をに変更することにつながったフィードバックと主にあなたの考えに本当に感謝しますFileContent。それは私の例がどれほど悪かったかを示しています。また、予想していたフィードバックを完全に収集できなかった質問の作成方法も非常に悪かった。私の意図をより明確にするために編集しました。
発見

@Flaterは、ほとんどの場合GetFilteredContent(string filename)ファイル名を使用してコードで実行されたとしても、作業の本体をa Streamまたはa をとるメソッドに入れて、TextReaderテストを非常に簡単にします。したがってGetFilteredContent(string)、ラッパーになりGetFilteredContent(TextReader reader)ます。しかし、Aはあなたの評価に同意します。
ベリンロリッチ

4

私の考えでは、両方のアプローチの問題は次のとおりです。

  1. カプセル化しているためFile.ReadLines、単体テストが必要以上に難しくなり、
  2. パスをとして保存するために、ファイルが列挙されるたびに新しいクラスインスタンスを作成する必要があります_filepath

だから私はそれを静的メソッドにすることをお勧めします、それは渡されるIEnumerable<string>Stream、ファイルの内容を表すかのどちらかです:

public static GetFilteredLines(IEnumerable<string> fileContents)
    => fileContents.Where(l => ...);

次に、次のように呼び出されます。

var filteredLines = GetFilteredLines(File.ReadLines(filePath));

これにより、ヒープに不要な負荷がかかることが回避され、メソッドの単体テストがはるかに容易になります。


私はあなたに同意しますが、それは私が期待された種類のフィードバックではありませんでした(私の質問はその意味では不十分に定式化されていました)。編集した質問を参照してください。
発見

@Spotted、すごい、あなたが間違った質問をしたので、あなたは本当にあなたの質問への回答に反対票を投じていますか?それは低いです。
David Arno

はい、問題が私の不十分に定式化された質問であることに気づくまで行いました。それを除いて、あなたの回答が編集されない限り、私は私のダウン投票をアンキャストできません...:-/謝罪を受け入れてください(または私が喜んで私のダウン投票を削除するようにあなたの回答をダミー編集します)。
発見

@Spotted、今あなたの質問の問題は、2つのコーディングスタイルについて意見を求めているため、質問がトピックから外れていることです。更新された質問でも、私の答えは同じです。どちらのデザインにも、私が指定する2つの理由で欠陥があるため、どちらも私の考えでは良い解決策ではありません。
デビッドアルノ

私の例の設計には欠陥があることに完全に同意しますが、それは私の質問の要点ではありません(これは不自然な例であるため)、これら2つのアプローチの両方を紹介する口実にすぎませんでした。
発見

2

あなたが提供した実世界の例DueInvoicesは、これが現在期限のある請求書のコレクションであるという概念に非常に適しています。考案された例が、あなたが使用した用語とあなたが伝えようとしている概念とをどのようにまとめるかを完全に理解しています。私は何度も自分自身の苛立たしい終わりにありました。

とは言っても、クラスの目的が厳密にでありIEnumerable<T>、他のロジックを提供しない場合は、クラス全体が必要か、別のクラスのメソッドを単純に提供できるかを質問する必要があります。例えば:

public class Invoices
{
    // ... skip all the other stuff about Invoices

    public IEnumerable<Invoice> GetDueItems()
    {
         foreach(var line in File.ReadLines(_invoicesFile))
         {
             var invoice = ReadInvoiceFrom(line);
             if (invoice.PaymentDue <= DateTime.UtcNow)
             {
                 yield return invoice;
             }
         }
    }
}

yield returnあなただけのLINQクエリをラップ、またはロジックを埋め込むことは従うことが容易であることができないときに動作します。他のオプションは、単にLINQクエリを返すことです。

public class Invoices
{
    // ... skip all the other stuff about invoices

    public IEnumerable<Invoice> GetDueItems()
    {
        return from Invoice invoice in GetAllItems()
               where invoice.PaymentDue <= DateTime.UtcNow
               select invoice;
    }
}

これらのどちらの場合でも、完全なラッパークラスは必要ありません。メソッドを提供するだけで、基本的にイテレータが処理されます。

反復を処理するために完全なクラスが必要になったのは、長時間実行されるクエリでデータベースからblobをプルする必要があったときだけです。ユーティリティは、データを他の場所に移行できるように、1回限りの抽出用でした。を使用してコンテンツをストリーミングしようとしたときに、データベースに遭遇した奇妙な点がありましたyield return。しかし、IEnumerator<T>リソースをクリーンアップするタイミングをより適切に制御するために実際にカスタムを実装したとき、それはなくなりました。これは規則というよりは例外です。

つまり、IEnumerable<T>上記のコードで概説した方法のいずれかで問題を解決できる場合は、直接実装しないことをお勧めします。他の方法で問題を解決できない場合のために、列挙子を明示的に作成するオーバーヘッドを節約してください。


別のクラスを作成した理由はInvoiceDto、永続化レイヤーから取得されたため、単なるデータの袋であり、ビジネス関連のメソッドでそれを混乱させたくありませんでした。したがって、DueInvoiceおよびの作成DueInvoices
発見

@ Spotted、DTOクラス自体である必要はありません。いや、それはおそらく拡張メソッドを持つ静的クラスかもしれません。私が提案しているのは、ほとんどの場合、定型コードを最小限に抑え、同時にAPIを消化可能にすることです。
ベリンロリッチ

0

概念について考えてください。ファイルとその内容の関係は何ですか?これは「has」関係であり、「is」関係ではありません。

したがって、ファイルクラスには、コンテンツを返すためのメソッド/プロパティ必要です。そして、それはまだ呼び出すのに便利です:

public IEnumerable<string> GetFilteredContents() { ... }

foreach(string line in myFile.GetFilteredContents() { ... }

あなたはファイルとその内容との関係について正しいです。例はよく考えられていませんでした。自分の考えを正確にするために、元の質問をいくつか編集しました。
発見

不当な反対投票については私の謝罪を受け入れますが、回答を編集しない限り、削除することはできません。:-/
2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.