Шаблоны: создание и преобразование между объектами данных и форматами проводов

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

Задание:

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

  • Мне нужно перевести эти модели между классами (в данном случае C++, но речь идет об общем дизайне) и форматами проводов (Json, Protocol Buffers) и наоборот.

  • Строительство объектов достаточно простое.

Как я уже сказал, я бы обычно использовал шаблон мапперов, определял интерфейс мапперов и конкретные мапперы для сопоставления между каждым форматом. Однако есть две причины, почему я спрашиваю ваше мнение в этом случае:

  • Я бы использовал шаблон отображения только для отображения между двумя и только двумя типами форматов, например, объектом базы данных и классом модели. У меня уже есть третий формат в этом случае, и, возможно, мне придется добавить больше форматов для перевода в ближайшем будущем.

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

Я читал о Translator Pattern [1], но никогда не использовал его. Я думаю, что это в какой-то степени подходит, но не полностью.

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

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

[1] http://www.iro.umontreal.ca/~keller/Layla/translator.pdf

Ответ 1

Цель реализации

Мы собираемся написать автоматический переводчик. Допустим, у нас есть объект, представляющий наш проводной формат:

JsonObject wire_data;

Для удобства мы можем представить, что наш JsonObject имеет add_field член add_field:

wire_data.add_field("name", "value"); 

Однако фактический интерфейс JsonObject на самом деле не имеет значения, и остальная часть этого поста не зависит от его реализации каким-либо конкретным способом.

Мы хотим иметь возможность написать эту функцию:

template<class Car>
void add_car_info(JsonObject& object, Car car) {
    // Stuff goes here
}

со следующими ограничениями:

  • Если у Car есть поле, например, Car::getMake(), наша функция add_car_info должна add_car_info добавить это поле в объект json
  • Если у Car нет поля, наша функция не должна ничего делать.
  • Наша реализация не должна полагаться на то, что Car является производным от чего-либо или является базовым классом чего-либо
  • Наша реализация должна упростить добавление новых полей, не нарушая обратной совместимости.

Пример с четырьмя независимыми классами автомобилей

Допустим, у вас есть четыре класса автомобилей. Никто из них не разделяет базовый класс; какие поля они выставляют, меняется; и вы можете добавить больше классов автомобилей в будущем.

struct Car1
{
    std::string getMake() { return "Toyota"; }
    std::string getModel() { return "Prius"; }
    int getYear() { return 2013; }
};
struct Car2
{
    std::string getMake() { return "Toyota"; }
    int         getYear() { return 2017; };
};
struct Car3
{
    std::string getModel() { return "Prius"; }
    int         getYear() { return 2017; }
};
struct Car4
{
    long long getSerial() { return 2039809809820390; }
};

Сейчас,

JsonObject wire_data;
Car1 car1;
add_field(wire_data, car1);

Должны быть эквивалентны

Car1 car1; 
wire_data.add_field("car make", car1.getMake()); 
wire_data.add_field("car model", car1.getModel()); 
wire_data.add_field("year", car1.getYear()); 

В то время как

Car2 car2;
add_field(wire_data, car2); 

Должен быть эквивалентен

Car2 car2; 
wire_data.add_field("car make", car2.getMake()); 
wire_data.add_field("year", car2.getYear()); 

Как мы реализуем add_car_info универсальным способом?

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

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

template<class Car>
void add_car_info(JsonObject& wire_object, Car car) {
    auto translator = getCarTranslator(); 

    // This lambda adds the inputs to wire_object
    auto add_field = [&](std::string const& name, auto&& value) {
        wire_object.add_field(name, value); 
    };
    // Add the car fields. 
    translator.translate(add_field, car); 
}         

Похоже, что объект- translator просто пинает, может в будущем, однако наличие объекта- translator облегчит написание translator для вещей, отличных от автомобилей.

Как мы реализуем волшебный переводчик?

Давайте начнем с getCarTranslator. Что касается автомобилей, мы можем заботиться о четырех вещах: модель, год и серийный номер.

auto getCarTranslator() {
    return makeTranslator(READ_FIELD("car make", getMake()),
                          READ_FIELD("car model", getModel()),
                          READ_FIELD("year", getYear()),
                          READ_FIELD("serial", getSerial()));
}

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

// This class is used to tell our overload set we want the name of the field
class read_name_t
{
};

#define READ_FIELD(name, field)                                      \
    overload_set(                                                    \
        [](auto&& obj) -> decltype(obj.field) { return obj.field; }, \
        [](read_name_t) -> decltype(auto) { return name; })

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

Реализация набора перегрузки для лямбд

Это довольно просто. Мы просто создаем класс, который наследует обе лямбды:

template <class Base1, class Base2>
struct OverloadSet
  : public Base1
  , public Base2
{
    OverloadSet(Base1 const& b1, Base2 const& b2) : Base1(b1), Base2(b2) {}
    OverloadSet(Base1&& b1, Base2&& b2)
      : Base1(std::move(b1)), Base2(std::move(b2))
    {
    }
    using Base1::operator();
    using Base2::operator();
};

template <class F1, class F2>
auto overload_set(F1&& func1, F2&& func2)
    -> OverloadSet<typename std::decay<F1>::type, typename std::decay<F2>::type>
{
     return {std::forward<F1>(func1), std::forward<F2>(func2)};
}

Реализация класса переводчика с использованием чуть-чуть SFINAE

Первый шаг - создать класс, который читает отдельное поле. Он содержит лямбду, которая делает чтение. Если мы можем применить лямбду, мы применяем ее (читая поле). В противном случае мы его не применяем, и ничего не происходит.

template <class Reader>
class OptionalReader
{
public:
    Reader read;
    template <class Consumer, class Object>
    void maybeConsume(Consumer&& consume, Object&& obj) const
    {
        // The 0 is used to dispatch it so it considers both overloads
        maybeConsume(consume, obj, 0);
    }

private:
    // This is used to disable maybeConsume if we can't read it
    template <class...>
    using ignore_t = void;

    // This one gets called if we can read the object
    template <class Consumer, class Object>
    auto maybeConsume(Consumer& consume, Object& obj, int) const
        -> ignore_t<decltype(consume(read(read_name_t()), read(obj)))>
    {
        consume(read(read_name_t()), read(obj));
    }

    // This one gets called if we can't read it
    template <class Consumer, class Object>
    auto maybeConsume(Consumer&, Object&, long) const -> void
    {
    }
};

Переводчик берет несколько дополнительных заявителей и просто применяет их последовательно:

template <class... OptionalApplier>
class Translator : public OptionalApplier...
{
public:
    // Constructors
    Translator(OptionalApplier const&... appliers)
      : OptionalApplier(appliers)... {}

    Translator(OptionalApplier&&... appliers) 
      : OptionalApplier(appliers)... {}

    // translate fuction
    template <class Consumer, class Object>
    void translate(Consumer&& consume, Object&& o) const
    {
        // Apply each optional applier in turn
        char _[] = {((void)OptionalApplier::maybeConsume(consume, o), '\0')...};
        (void)_;
    }
};

Создание функции makeTranslator теперь действительно просто. Мы просто берем кучу читателей и используем их для создания optionalReader читателей.

template <class... Reader>
auto makeTranslator(Reader const&... readers)
    -> Translator<OptionalReader<Reader>...>
{
    return {OptionalReader<Reader>{readers}...};
}

Заключение

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

Мы можем написать переводчиков для многих вещей очень легко!

Пример переводчика изображений

Например, здесь переводчик для изображений и изображений, который также принимает во внимание различные общие названия для таких вещей, как ширина и высота изображения.

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

auto getImagesTranslator() {
    // Width and height might be implemented as 'getWidth' and 'getHeight',
    // Or as 'getRows' and 'getCols'
    return makeTranslator(READ_FIELD("width", getWidth()),
                          READ_FIELD("height", getHeight()),
                          READ_FIELD("width", getCols()),
                          READ_FIELD("height", getRows()),
                          READ_FIELD("location", getLocation()),
                          READ_FIELD("pixel format", getPixelFormat()),
                          READ_FIELD("size", size()),
                          READ_FIELD("aspect ratio", getAspectRatio()),
                          READ_FIELD("pixel data", getPixelData()),
                          READ_FIELD("file format", getFileFormat())); 
}

Здесь полная реализация

Ответ 2

почему вы не используете некоторую библиотеку сериализации для этого? Примерами могут быть Boost.Serialization, буферы протокола Google и т.д.