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

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

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

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

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

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

Ответы [ 3 ]

2 голосов
/ 28 мая 2010

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

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

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

Слишком упрощенный контур будет выглядеть так:

struct Processor
{
    void doSomething(Data);
};

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

В моем уме, скорее всего, появятся следующие паттерны:

  • фабричная функция: получать объекты из модулей
  • композитный && decorator: формирование цепочки обработки
2 голосов
/ 28 мая 2010

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

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

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

1 голос
/ 28 мая 2010

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

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

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

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

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

Недостатком метода 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;
}
...