Как написать гибкую модульную программу с хорошими возможностями взаимодействия между модулями?

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

Я хочу написать программу, которая обрабатывает файлы. Обработка нетривиальна, поэтому лучший способ - разделить разные фазы на отдельные модули, которые затем будут использоваться по мере необходимости (поскольку иногда меня будут интересовать только выходные данные модуля A, иногда мне потребуется вывод пяти других модулей и т. д. ). Дело в том, что мне нужно, чтобы модули взаимодействовали, потому что вывод одного может быть вводом другого. И мне нужно, чтобы это было БЫСТРО. Более того, я хочу избежать выполнения определенной обработки более одного раза (если модуль A создает некоторые данные, которые затем должны быть обработаны модулями B и C, я не хочу запускать модуль A дважды, чтобы создать ввод для модулей B, C) .

Информация, которой должны обмениваться модули, в основном представляет собой блоки двоичных данных и/или смещения в обрабатываемых файлах. Задача основной программы была бы довольно простой — просто анализировать аргументы, запускать необходимые модули (и, возможно, выдавать какой-то вывод, или это должно быть задачей модулей?).

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

Может ли кто-нибудь указать мне хорошее направление, как это сделать на С++? Любые советы приветствуются (ссылки, учебные пособия, книги в формате pdf...).


person PeterK    schedule 28.05.2010    source источник
comment
Этот вопрос в основном заключается в том, как мне написать модульный код? Поскольку весь код должен быть модульным, здесь нет ничего конкретно о C++ или о вашей конкретной проблемной области. и ответ заключается в применении навыков, таланта и опыта.   -  person    schedule 28.05.2010


Ответы (3)


Это очень похоже на архитектуру плагина. Я рекомендую начать с (неофициальной) диаграммы потока данных, чтобы определить:

  • как эти блоки обрабатывают данные
  • какие данные нужно передать
  • какие результаты возвращаются из одного блока в другой (данные/коды ошибок/исключения)

С помощью этой информации вы можете начать создавать общие интерфейсы, которые позволяют выполнять привязку к другим интерфейсам во время выполнения. Затем я бы добавил фабричную функцию к каждому модулю, чтобы запросить из него реальный объект обработки. Я не рекомендую получать объекты обработки напрямую из интерфейса модуля, а возвращать фабричный объект, из которого можно получить объекты обработки. Затем эти объекты обработки используются для построения всей цепочки обработки.

Упрощенно схема будет выглядеть так:

struct Processor
{
    void doSomething(Data);
};

struct Module
{
    string name();
    Processor* getProcessor(WhichDoIWant);
    deleteprocessor(Processor*);
};

В моем уме, вероятно, появятся эти шаблоны:

  • фабричная функция: получать объекты из модулей
  • композитный && декоратор: формирование цепочки обработки
person Rudi    schedule 28.05.2010
comment
Спасибо за ваш ответ, подход с фабричным шаблоном выглядит хорошо! - person PeterK; 28.05.2010
comment
Однако реализация фабрики выглядит неправильно. Используйте RAII и перестаньте просить клиента вернуть его Processor в Module: мы знаем, что он забудет! - person Matthieu M.; 28.05.2010
comment
@Matthieu M., даже если не было метода удаления, клиентская сторона должна выполнить удаление, поскольку объекты не могут передаваться по значению, а только по указателю. Таким образом, RAII не предотвращает никаких повреждений в этот момент. Причина наличия метода удаления состоит в том, чтобы иметь больше свободы для реализации фабрики, а не в том, чтобы быть вынужденным использовать новый для построения объекта. Я использую этот шаблон в одном проекте, где одни фабрики создают объекты по запросу, тогда как другие возвращают указатели на одиночки или объекты из пула. - person Rudi; 28.05.2010
comment
Хм, кажется, я понимаю, что метод deleteprocessor на самом деле состоит в том, чтобы попросить Module (фабрику) удалить элемент из конструируемых объектов, не так ли? Я обычно использую для этого идентификатор, чтобы не просить клиента сначала получить объект. - person Matthieu M.; 28.05.2010
comment
@Matthieu M. Мой подход заключается в том, что фабрика возвращает объект процессора, этот объект процессора связывается запрашивающим кодом с некоторым контекстом обработки, затем происходит обработка, после чего объект процессора передается обратно на фабрику для удаления. Используя этот способ, я могу иметь более одного объекта процессора одновременно. Скажем, у меня есть два конвейера, в которых каждый символ должен быть преобразован в нижний регистр, моя фабрика может возвращать два независимых процессора нижнего регистра (или один экземпляр синглтона), и всякий раз, когда один из этих каналов выполняется, он возвращает свой процессор нижнего регистра. - person Rudi; 28.05.2010
comment
Ах, тогда мне не нравится ваше решение: почему вы явно должны вернуть процессор на завод? Используя концепцию RAII, он будет автоматически возвращен (если вы не хотите просто удалить его), когда дескриптор, к которому он привязан, выпадает из стека. Так намного чище. - person Matthieu M.; 28.05.2010
comment
@Matthieu M. вы смешиваете RAII с умными указателями. RAII означает, что выделение ресурса также является инициализацией, об освобождении ресурса ничего не сказано. - person Rudi; 31.05.2010
comment
Ну, технически RAII говорит только об инициализации. Однако обычно он используется для гарантированного освобождения (что подразумевается битом владения). Так что я не путаю, но, может быть, я недостаточно ясно выражаюсь... - person Matthieu M.; 31.05.2010

Мне интересно, подходит ли C++ для этой цели. По моему опыту, в соответствии с философией UNIX всегда было полезно иметь отдельные программы, объединенные конвейером.

Если ваши данные не слишком велики, у разделения есть много преимуществ. Сначала вы получаете возможность тестировать каждую фазу вашей обработки независимо, вы запускаете одну программу и перенаправляете вывод в файл: вы можете легко проверить результат. Затем вы сможете воспользоваться преимуществами многоядерных систем, даже если каждая из ваших программ является однопоточной, и, таким образом, их намного проще создавать и отлаживать. И вы также пользуетесь преимуществами синхронизации операционной системы, используя каналы между вашими программами. Может быть, некоторые из ваших программ можно было бы сделать и с помощью уже существующих утилит?

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

person Didier Trosset    schedule 28.05.2010
comment
Забыл сказать, что я привязан к ОС Windows. И мне очень нужна именно одна программа, а не набор программ, которые бы работали вместе (поскольку вполне возможно, что создаваемые мной модули будут использоваться не только в моем приложении, но и в других). В любом случае, спасибо за ваш ответ. - person PeterK; 28.05.2010
comment
Существуют библиотеки для конвейерной обработки, не зависящей от ОС (точнее, абстрагирующей ее). - person Matthieu M.; 28.05.2010
comment
Привязка к Windows не мешает создавать несколько программ и соединять их вместе. Даже Windows может сделать это отлично! - person Didier Trosset; 28.05.2010

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

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

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

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

  • Образец: вы получаете уникальный экземпляр такого модуля и выполняете его.
  • Фабрика: вы создаете модуль требуемого типа, выполняете его и выбрасываете.

Недостатком метода Exemplar является то, что если вы запустите модуль дважды, вы начнете не из чистого состояния, а из состояния, в котором он остался при последнем (возможно, неудачном) выполнении. Для мемоизации это может рассматриваться как преимущество. , но если это не удалось, результат не вычисляется (угу), поэтому я бы не рекомендовал этого делать.

Так как ты...?

Начнем с завода.

class Module;
class Result;

class Organizer
{
public:
  void AddModule(std::string id, const Module& module);
  void RemoveModule(const std::string& id);

  const Result* GetResult(const std::string& id) const;

private:
  typedef std::map< std::string, std::shared_ptr<const Module> > ModulesType;
  typedef std::map< std::string, std::shared_ptr<const Result> > ResultsType;

  ModulesType mModules;
  mutable ResultsType mResults; // Memoization
};

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

class Module
{
public:
  typedef std::auto_ptr<const Result> ResultPointer;

  virtual ~Module() {}               // it's a base class
  virtual Module* Clone() const = 0; // traditional cloning concept

  virtual ResultPointer Execute(const Organizer& organizer) = 0;
}; // class Module

А теперь все просто:

// Organizer implementation
const Result* Organizer::GetResult(const std::string& id)
{
  ResultsType::const_iterator res = mResults.find(id);

  // Memoized ?
  if (res != mResults.end()) return *(it->second);

  // Need to compute it
  // Look module up
  ModulesType::const_iterator mod = mModules.find(id);
  if (mod != mModules.end()) return 0;

  // Create a throw away clone
  std::auto_ptr<Module> module(it->second->Clone());

  // Compute
  std::shared_ptr<const Result> result(module->Execute(*this).release());
  if (!result.get()) return 0;

  // Store result as part of the Memoization thingy
  mResults[id] = result;

  return result.get();
}

И простой пример модуля/результата:

struct FooResult: Result { FooResult(int r): mResult(r) {} int mResult; };

struct FooModule: Module
{
  virtual FooModule* Clone() const { return new FooModule(*this); }

  virtual ResultPointer Execute(const Organizer& organizer)
  {
    // check that the file has the correct format
    if(!organizer.GetResult("CheckModule")) return ResultPointer();

    return ResultPointer(new FooResult(42));
  }
};

И из основного:

#include "project/organizer.h"
#include "project/foo.h"
#include "project/bar.h"


int main(int argc, char* argv[])
{
  Organizer org;

  org.AddModule("FooModule", FooModule());
  org.AddModule("BarModule", BarModule());

  for (int i = 1; i < argc; ++i)
  {
    const Result* result = org.GetResult(argv[i]);
    if (result) result->print();
    else std::cout << "Error while playing: " << argv[i] << "\n";
  }
  return 0;
}
person Matthieu M.    schedule 28.05.2010