Упростить расширяемую платформу «Выполнение операции X для данных Y» - PullRequest
0 голосов
/ 26 июня 2018

ТЛ; др

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

Я также благодарен за указания на лучшие решения в целом.

Длинная версия

Я работаю над расширяемой платформой для выполнения «операций» над «данными». Одна из основных целей - позволить конфигурациям XML определять поток программы и позволить пользователям расширять как разрешенные типы данных, так и операции на более позднем этапе без необходимости изменения кода платформы.

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

Теперь, если оба должны быть расширяемыми, я не смог бы найти устоявшегося решения.

Мое решение состоит в том, чтобы объявить их независимо друг от друга, а затем зарегистрировать «операцию X для типа данных Y» через фабрику операций. Таким образом, пользователи могут добавлять новые типы данных или реализовывать дополнительные или альтернативные операции, и их можно создавать и настраивать с использованием той же структуры XML.

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

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

Некоторые решения, которые я рассмотрел

  • Как в примере ниже: предоставить реализацию по умолчанию для тривиальных операций. Следствие: нетривиальные операции нужно помнить, чтобы переопределять их собственными методами. Может привести к проблемам во время выполнения, если какой-то будущий разработчик забудет это сделать.
  • НЕ предоставляйте значения по умолчанию. Следствие: нетривиальные функции необходимо в основном копировать и вставлять для каждого конечного производного класса. Много бесполезного кода для копирования и вставки.
  • Предоставить дополнительный класс шаблонов, производный от базового класса cOperation, который реализует шаблонные функции и ничего больше (параметры шаблона аналогичны шаблонам рабочих лошадей конкретной операции). Производные конечные классы наследуют от своего конкретного базового класса операций и этого шаблона. Следствие: и concreteOperationBase, и шаблон шаблона должны виртуально наследоваться от cOperation. Потенциально некоторые накладные расходы, из того, что я нашел на SO. Будущие разработчики должны позволить своим операциям наследовать виртуально от cOperation.
  • std :: enable_if magic. Не работает комбинация виртуальных функций и шаблонов.

Вот (довольно) минимально компилируемый пример ситуации:

//Base class for all operations on all data types. Will be inherited from. A lot. Base class does not define any concrete operation interface, nor does it necessarily know any concrete data types it might be performed on.

class cOperation
{
public:
  virtual ~cOperation() {}

  virtual std::unique_ptr<cOperation> Clone() const = 0;
  virtual bool Serialize() const = 0;
  //... more virtual calls that can be either trivial or quite involved ...

protected:
  cOperation(const std::string& strOperationID, const std::string& strOperatesOnType)
    : m_strOperationID()
    , m_strOperatesOnType(strOperatesOnType)
  {
    //empty
  }

private:
  std::string m_strOperationID;
  std::string m_strOperatesOnType;
};

//Base class for all data types. Will be inherited from. A lot. Does not know any operations that might be performed on it.
struct cDataTypeBase 
{
  virtual ~cDataTypeBase() {}
};

Теперь я опишу пример типа данных.

//Some concrete data type. Still does not know any operations that might be performed on it.
struct cDataTypeA : public cDataTypeBase
{
  static const std::string& GetDataName()
  {
    static const std::string strMyName = "cDataTypeA";
    return strMyName;
  }
};

А вот пример операции. Он определяет конкретный интерфейс операции, но не знает типы данных, с которыми он может быть выполнен.

//Some concrete operation. Does not know all data types it might be expected to work on.
class cConcreteOperationX : public cOperation
{
public:
  virtual bool doSomeConcreteOperationX(const cDataTypeBase& dataBase) = 0;

protected:
  cConcreteOperationX(const std::string& strOperatesOnType)
    : cOperation("concreteOperationX", strOperatesOnType)
  { 
    //empty
  }
};

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

//ConcreteOperationTemplate: absorb as much common/trivial code as possible, so concrete derived classes can have minimal code for easy addition of more supported data types
template <typename ConcreteDataType, typename DerivedOperationType, bool bHasTrivialCloneAndSerialize = false>
class cConcreteOperationXTemplate : public cConcreteOperationX
{
public:
  //Can perform datatype cast here:
  virtual bool doSomeConcreteOperationX(const cDataTypeBase& dataBase) override
  {
    const ConcreteDataType* pCastData = dynamic_cast<const ConcreteDataType*>(&dataBase);
    if (pCastData == nullptr)
    {
      return false;
    }
    return doSomeConcreteOperationXOnCastData(*pCastData);
  }

protected:
  cConcreteOperationXTemplate()
    : cConcreteOperationX(ConcreteDataType::GetDataName()) //requires ConcreteDataType to have a static method returning something appropriate
  {
    //empty
  }

private:
  //Clone can be implemented here via CRTP
  virtual std::unique_ptr<cOperation> Clone() const override
  {
    return std::unique_ptr<cOperation>(new DerivedOperationType(*static_cast<const DerivedOperationType*>(this)));
  }

  //TODO: Some Magic here to enable trivial serializations, but leave non-trivials abstract
  //Problem with current code is that virtual bool Serialize() override will also be overwritten for bHasTrivialCloneAndSerialize == false
  virtual bool Serialize() const override
  {
    return true;
  }

  virtual bool doSomeConcreteOperationXOnCastData(const ConcreteDataType& castData) = 0;
};

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

//Implementation of operation X on type A. Needs to know both of these, but can be implemented if and when required.
class cConcreteOperationXOnTypeADefault : public cConcreteOperationXTemplate<cDataTypeA, cConcreteOperationXOnTypeADefault, true>
{
  virtual bool doSomeConcreteOperationXOnCastData(const cDataTypeA& castData) override
  {
    //...do stuff...
    return true;
  }
};

//Different implementation of operation X on type A.
class cConcreteOperationXOnTypeASpecialSauce : public cConcreteOperationXTemplate<cDataTypeA, cConcreteOperationXOnTypeASpecialSauce/*, false*/>
{
  virtual bool doSomeConcreteOperationXOnCastData(const cDataTypeA& castData) override
  {
    //...do stuff...
    return true;
  }

  //Problem: Compiler does not remind me that cConcreteOperationXOnTypeASpecialSauce might need to implement this method
  //virtual bool Serialize() override {}
};

int main(int argc, char* argv[])
{
  std::map<std::string, std::map<std::string, std::unique_ptr<cOperation>>> mapOpIDAndDataTypeToOperation;
  //...fill map, e.g. via XML config / factory method...

  const cOperation& requestedOperation = *mapOpIDAndDataTypeToOperation.at("concreteOperationX").at("cDataTypeA");
  //...do stuff...

  return 0;
}

1 Ответ

0 голосов
/ 26 июня 2018

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

#include<iostream>
#include<string>

template<class T>
void empty(T t){
   std::cout<<"warning about missing implementation"<<std::endl;
}

template<class T>
void simple_plus(T){
  std::cout<<"simple plus"<<std::endl;
}

void plus_string(std::string){
  std::cout<<"plus string"<<std::endl;
}

template<class Data, void Implementation(Data)>
class Operation{
public:
    static void exec(Data d){
        Implementation(d);
    }
};

#define macro_def(OperationName) template<class T> class OperationName : public Operation<T, empty<T>>{};
#define macro_template_inst( TypeName, OperationName, ImplementationName ) template<> class OperationName<TypeName> : public Operation<TypeName, ImplementationName<TypeName>>{};
#define macro_inst( TypeName, OperationName, ImplementationName ) template<> class OperationName<TypeName> : public Operation<TypeName, ImplementationName>{};

// this part may be generated on base of .xml file and put into .h file, and then just #include generated.h
macro_def(Plus)
macro_template_inst(int, Plus, simple_plus)
macro_template_inst(double, Plus, simple_plus)
macro_inst(std::string, Plus, plus_string)


int main() {
    Plus<int>::exec(2);
    Plus<double>::exec(2.5);
    Plus<float>::exec(2.5);
    Plus<std::string>::exec("abc");
    return 0;
}

Минусом этого подхода является то, что вам придется компилировать проект в 2 этапа: 1) преобразовать .xml в .h 2) скомпилировать проект, используя сгенерированный файл .h. На положительной стороне компилятор / ide (я использую qtcreator с mingw) выдает предупреждение о неиспользованном параметре t в функции

void empty(T t)

и трассировка стека, откуда он был вызван.

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