Шаблон проектирования для агрегирования ленивых списков - PullRequest
1 голос
/ 07 января 2009

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

  • Найти все файлы с правильным расширением в данном каталоге
  • 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 matchingLines.Count(), он будет повторно перечислять коллекцию!

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

Есть идеи? Приветствия

Ответы [ 6 ]

2 голосов
/ 07 января 2009

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

... почему бы не создать класс Decorator , который может обернуть существующую реализацию 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);

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

2 голосов
/ 07 января 2009

Я могу вспомнить две идеи

  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();
    } 
    
2 голосов
/ 07 января 2009

Я бы сказал, что вам нужно инкапсулировать процесс в класс '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 голос
/ 08 января 2009

Я взял код Бевана и рефакторинг его, пока я не был доволен. Прикольные вещи.

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

1 голос
/ 07 января 2009

Если вы хотите перевернуть код с ног на голову, вас может заинтересовать Push LINQ. Основная идея состоит в том, чтобы полностью изменить модель «вытягивания» IEnumerable<T> и превратить ее в модель «выталкивания» с наблюдателями - каждая часть конвейера эффективно проталкивает свои данные через любое количество наблюдателей (используя обработчики событий), которые обычно образуют новые части трубопровода. Это дает действительно простой способ подключения нескольких агрегатов к одним и тем же данным.

См. эту запись в блоге для более подробной информации. Я выступал с докладом об этом в Лондоне некоторое время назад - на моей странице переговоров есть несколько ссылок для примера кода, слайд-колоды, видео и т. Д.

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

0 голосов
/ 07 января 2009

Предполагая, что эти функции являются вашими собственными, я могу думать только о шаблоне Visitor, передающем абстрактную функцию посетителя, которая вызывает вас обратно, когда происходит каждая вещь. Например: передать ILineVisitor в GetFileContents (который, я предполагаю, разбивает файл на строки). У ILineVisitor будет такой метод, как OnVisitLine (строка String), вы можете затем реализовать ILineVisitor и заставить его сохранять соответствующую статистику. Промойте и повторите с ILineMatchVisitor, IFileVisitor и т. Д. Или вы можете использовать один IVisitor с методом OnVisit (), который имеет различную семантику в каждом случае.

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

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

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