Шаблон проектирования для агрегирования отложенных списков

StackOverflow https://stackoverflow.com/questions/418983

Вопрос

Я пишу программу следующим образом:

  • Найдите все файлы с правильным расширением в заданном каталоге
  • Foreach, найдите все вхождения данной строки в этих файлах
  • Выведите каждую строку

Я хотел бы написать это функциональным способом, в виде серии функций генератора (вещей, которые вызывают yield return и возвращать только один элемент за раз, лениво загруженный), поэтому мой код будет выглядеть следующим образом:

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, и поскольку только один элемент всегда yieldредактируемый одновременно, в коде нет места для отслеживания того, сколько вещей было найдено ранее.Если вы вызовете LINQ's matchingLines.Count() метод, он повторно перечислит коллекцию!

Я могу придумать много способов решить эту проблему, но все они кажутся несколько уродливыми.Мне кажется, что люди обязательно делали это раньше, и я уверен, что появится хороший шаблон дизайна, который покажет наилучший практический способ сделать это.

Есть какие-нибудь идеи?Ваше здоровье

Это было полезно?

Решение

В том же духе, что и другие ответы, но с использованием несколько более общего подхода ...

...почему бы не создать Декоратор класс, который может обернуть существующую реализацию IEnumerable и вычислять статистику по мере прохождения через нее других элементов.

Вот такой 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;
}

Вы могли бы создать три экземпляра Counter:

  1. Один для обертывания GetAllFiles() подсчет общего количества файлов;
  2. Один для обертывания GetMatches() подсчет количества совпадающих файлов;и
  3. Один для обертывания GetMatchingLines() подсчет количества совпадающих строк.

Ключевым моментом этого подхода является то, что вы не накладываете несколько обязанностей на свои существующие классы / методы - GetMatchingLines() метод обрабатывает только совпадение, вы не просите его также отслеживать статистику.

Разъяснение в ответ на комментарий от 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;
        }
     }
  }
}

Я остановлюсь на этом, поскольку предполагается, что я буду кодировать рабочие материалы, но общая идея есть.Весь смысл "чистой" функциональной программы в том, чтобы не иметь побочных эффектов, и этот тип вычисления статики является побочным эффектом.

Мне приходят в голову две идеи

  1. Передайте контекстный объект и верните (строка + контекст) из ваших счетчиков - чисто функциональное решение

  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.Основная идея состоит в том, чтобы обратить вспять модель "вытягивания" IEnumerable<T> и превратите это в модель "выталкивания" с наблюдателями - каждая часть конвейера эффективно проталкивает свои данные мимо любого количества наблюдателей (используя обработчики событий), которые обычно формируют новые части конвейера.Это дает действительно простой способ подключить несколько агрегатов к одним и тем же данным.

Видишь эта запись в блоге для получения более подробной информации.Некоторое время назад я выступал с докладом об этом в Лондоне - мой страница выступлений содержит несколько ссылок на примеры кода, слайд-деку, видео и т.д.

Это забавный маленький проект, но для этого нужно немного собраться с мыслями.

Я взял код Бевана и переделывал его до тех пор, пока не остался доволен.Забавная штука.

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, передающий абстрактную функцию visitor, которая перезванивает вам, когда что-то происходит.Например:передайте ILineVisitor в getFileContents (который, я предполагаю, разбивает файл на строки).У ILineVisitor был бы метод, подобный OnVisitLine (строковая строка), затем вы могли бы реализовать ILineVisitor и заставить его сохранять соответствующую статистику.Промойте и повторите процедуру с помощью ILineMatchVisitor, IFileVisitor и т.д.Или вы могли бы использовать один IVisitor с методом OnVisit(), который в каждом случае имеет разную семантику.

Каждая из ваших функций должна была бы принимать посетителя и вызывать его OnVisit() в соответствующее время, что может показаться раздражающим, но, по крайней мере, посетителя можно было бы использовать для выполнения множества интересных вещей, отличных от того, что вы делаете здесь.На самом деле вы могли бы избежать написания GetMatchingLines, передав посетителя, который проверяет соответствие в OnVisitLine(строка String), в getFileContents.

Это одна из тех уродливых вещей, которые вы уже рассматривали?

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top