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

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

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

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

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

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

3 ответов


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

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

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

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

struct Processor
{
    void doSomething(Data);
};

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

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

  • функция фабрики: получить объекты от модулей
  • композит & & декоратор: формирование цепочки обработки

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

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

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


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

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

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

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

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

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

так как вы ... ?

давайте начнем с завода.

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
};

это очень простой интерфейс на самом деле. Однако, поскольку мы хотим новый экземпляр модуля каждый раз, когда мы вызываем the 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));
  }
};

и от main:

#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;
}