遅延リストを集約するための設計パターン
-
03-07-2019 - |
質問
次のようにプログラムを作成しています:
- 指定されたディレクトリで正しい拡張子を持つすべてのファイルを見つける
- Foreach、それらのファイル内の特定の文字列のすべての出現を検索
- 各行を印刷
これを機能的な方法で、一連のジェネレーター関数(yield return
を呼び出して、一度に1つのアイテムのみを遅延ロードするもの)として書きたいので、私のコードは次のようになります:
IEnumerable<string> allFiles = GetAllFiles();
IEnumerable<string> matchingFiles = GetMatches( "*.txt", allFiles );
IEnumerable<string> contents = GetFileContents( matchingFiles );
IEnumerable<string> matchingLines = GetMatchingLines( contents );
foreach( var lineText in matchingLines )
Console.WriteLine( "Found: " + lineText );
これで問題ありませんが、最後に統計情報を出力することもしたいと思います。このようなもの:
Found 233 matches in 150 matching files. Scanned 3,297 total files in 5.72s
問題は、上記のような「純粋な機能」スタイルでコードを記述すると、各アイテムが遅延ロードされることです。
最終的なforeachループが完了するまで、合計で一致するファイルの数だけがわかります。また、一度に1つのアイテムのみがyield
されるため、コードには以前に見つかったものを追跡する場所がありません。 LINQのmatchingLines.Count()
メソッドを呼び出すと、コレクションが再列挙されます!
この問題を解決する多くの方法を考えることができますが、それらのすべてはややいようです。これは、人々が以前にやらなければならないことのように思えます。これを行うためのベストプラクティスの方法を示す素敵なデザインパターンがあるはずです。
アイデアはありますか?乾杯
解決
他の回答と同様の流れですが、少し一般的なアプローチを取っています...
...既存のIEnumerable実装をラップし、他のアイテムを通過させるときに統計を計算できる Decorator クラスを作成してください。
ここに私が一緒に投げたCounter
クラスがありますが、他の種類の集約用にバリエーションを作成することもできます。
public class Counter<T> : IEnumerable<T>
{
public int Count { get; private set; }
public Counter(IEnumerable<T> source)
{
mSource = source;
Count = 0;
}
public IEnumerator<T> GetEnumerator()
{
foreach (var T in mSource)
{
Count++;
yield return T;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
foreach (var T in mSource)
{
Count++;
yield return T;
}
}
private IEnumerable<T> mSource;
}
GetAllFiles()
の3つのインスタンスを作成できます:
- ラップするもの
GetMatches()
ファイルの総数をカウントする - ラップするもの
GetMatchingLines()
一致するファイルの数を数えます。そして - 1つ折り返す
Mitcham
一致する行の数を数えます。
このアプローチの重要な点は、既存のクラス/メソッドに複数の責任を重ねていないことです-<=>メソッドはマッチングのみを処理し、統計の追跡も要求しません。
明確化:<=>:
によるコメントへの応答最終的なコードは次のようになります。
var files = new Counter<string>( GetAllFiles());
var matchingFiles = new Counter<string>(GetMatches( "*.txt", files ));
var contents = GetFileContents( matchingFiles );
var linesFound = new Counter<string>(GetMatchingLines( contents ));
foreach( var lineText in linesFound )
Console.WriteLine( "Found: " + lineText );
string message
= String.Format(
"Found {0} matches in {1} matching files. Scanned {2} files",
linesFound.Count,
matchingFiles.Count,
files.Count);
Console.WriteLine(message);
これはまだ機能的なアプローチであることに注意してください-使用される変数は不変(変数よりもバインディングに近い)であり、関数全体に副作用はありません。
他のヒント
プロセスを 'Matcher'クラスにカプセル化する必要があると思います。このクラスでは、メソッドが進行するにつれて統計をキャプチャします。
public class Matcher
{
private int totalFileCount;
private int matchedCount;
private DateTime start;
private int lineCount;
private DateTime stop;
public IEnumerable<string> Match()
{
return GetMatchedFiles();
System.Console.WriteLine(string.Format(
"Found {0} matches in {1} matching files." +
" {2} total files scanned in {3}.",
lineCount, matchedCount,
totalFileCount, (stop-start).ToString());
}
private IEnumerable<File> GetMatchedFiles(string pattern)
{
foreach(File file in SomeFileRetrievalMethod())
{
totalFileCount++;
if (MatchPattern(pattern,file.FileName))
{
matchedCount++;
yield return file;
}
}
}
}
作業内容をコーディングすることになっているので、そこで停止しますが、一般的な考えはそこにあります。 「純粋な」関数型プログラムの全体のポイントは副作用がないことであり、この種の統計計算は副作用です。
2つのアイデアを考えることができます
-
コンテキストオブジェクトを渡し、列挙子から(文字列+コンテキスト)を返す-純粋に機能的なソリューション
-
スレッドのローカルストレージを使用して統計情報( CallContext )、あなたは空想にふけることができ、コンテキストのスタックをサポートできます。あなたはこのようなコードを持っているでしょう。
using (var stats = DirStats.Create()) { IEnumerable<string> allFiles = GetAllFiles(); IEnumerable<string> matchingFiles = GetMatches( "*.txt", allFiles ); IEnumerable<string> contents = GetFileContents( matchingFiles ); stats.Print() IEnumerable<string> matchingLines = GetMatchingLines( contents ); stats.Print(); }
コードを逆さまにしたい場合は、Push LINQに興味があるかもしれません。基本的な考え方は、<!> quot; pull <!> quot;を逆にすることです。 IEnumerable<T>
のモデルを<!> quot; push <!> quot;に変換します。オブザーバーを使用したモデル-パイプラインの各部分は、通常、パイプラインの新しい部分を形成する任意の数のオブザーバー(イベントハンドラーを使用)を介してデータを効果的にプッシュします。これにより、複数の集計を同じデータに簡単に接続できます。
このブログエントリで詳細を確認してください。私は少し前にロンドンでそれについて講演しました-私の講演のページにはサンプルのリンクがいくつかありますコード、スライドデッキ、ビデオなど
これは楽しい小さなプロジェクトですが、少し頭を動かす必要があります。
Bevanのコードを取得し、満足するまでリファクタリングしました。楽しいもの。
public class Counter
{
public int Count { get; set; }
}
public static class CounterExtensions
{
public static IEnumerable<T> ObserveCount<T>
(this IEnumerable<T> source, Counter count)
{
foreach (T t in source)
{
count.Count++;
yield return t;
}
}
public static IEnumerable<T> ObserveCount<T>
(this IEnumerable<T> source, IList<Counter> counters)
{
Counter c = new Counter();
counters.Add(c);
return source.ObserveCount(c);
}
}
public static class CounterTest
{
public static void Test1()
{
IList<Counter> counters = new List<Counter>();
//
IEnumerable<int> step1 =
Enumerable.Range(0, 100).ObserveCount(counters);
//
IEnumerable<int> step2 =
step1.Where(i => i % 10 == 0).ObserveCount(counters);
//
IEnumerable<int> step3 =
step2.Take(3).ObserveCount(counters);
//
step3.ToList();
foreach (Counter c in counters)
{
Console.WriteLine(c.Count);
}
}
}
期待どおりの出力:21、3、3
これらの関数があなた自身のものであると仮定すると、私が考えることができる唯一のことはVisitorパターンであり、それぞれのことが起こるときにあなたを呼び戻す抽象的なビジター関数を渡します。たとえば、ILineVisitorをGetFileContentsに渡します(ファイルを行に分割すると仮定しています)。 ILineVisitorにはOnVisitLine(String line)のようなメソッドがあります。その後、ILineVisitorを実装し、適切な統計を保持することができます。 ILineMatchVisitor、IFileVisitorなどでリンスして繰り返します。または、単一のIVisitorをOnVisit()メソッドで使用し、それぞれの場合に異なるセマンティクスを使用できます。
各関数は訪問者を取得し、適切なタイミングでOnVisit()を呼び出す必要がありますが、これは面倒に思えるかもしれませんが、少なくとも訪問者はあなた以外の多くの興味深いことを行うために使用できますここでやり直します。実際、OnVisitLine(String line)の一致をチェックするビジターをGetFileContentsに渡すことで、GetMatchingLinesの書き込みを実際に回避できます。
これは、あなたがすでに検討したいものの1つですか?