模式:在数据对象和有线格式之间创建和转换

问题描述 投票:12回答:2

我倾向于只使用我在应用程序代码中的各个地方使用的映射器模式。但我认为在这种特殊情况下它可能不是最合适的:

任务:

  • 我需要根据给定的规范实现数据对象。规范为每个对象类型定义了多个版本,因此我有一个类CarV1和CarV2代表规范的每个版本。
  • 我需要在类之间翻译这些模型(在这种情况下是C ++,但问题是关于一般设计)和有线格式(Json,Protocol Buffers),反之亦然。
  • 对象的构造相当简单。

正如我所说,我通常使用映射器模式,定义映射器接口和具体映射器以在每种格式之间进行映射。在这种情况下,为什么我会问你的意见有两件事:

  • 我只使用映射器模式来映射两种类型的格式,例如两种格式,例如:数据库对象和模型类。在这种情况下我已经有了第三种格式,我可能需要在不久的将来添加更多格式来进行翻译。
  • 版本控制在映射之上增加了一些复杂性,我认为需要在它们之间存在另一个间接性。

我读过有关译者模式[1],但从未使用它。我认为它在某种程度上适合,但不完全适合。

我也考虑过抽象工厂。这将允许创建类似的对象(在我的情况下是版本化对象)。但它不适合在对象表示之间进行映射。

我应该使用什么样的模式,为什么?

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

java c++ design-patterns
2个回答
5
投票

实施目标

我们打算写一个自动翻译器。假设我们有一个表示我们的线格式的对象:

JsonObject wire_data;

为方便起见,我们可以想象我们的JsonObject有一个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应该自动将该字段添加到json对象
  • 如果Car没有字段,我们的功能不需要做任何事情。
  • 我们的实现不应该依赖于Car来自任何东西,或者是任何东西的基类
  • 我们的实现应该使添加新字段变得微不足道,而不会破坏向后兼容性。

Example with four independent Car classes

假设你有四个车型。他们都没有共享基类;他们揭露的领域各不相同;而且您将来可能会增加更多的汽车课程。

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's fields. 
    translator.translate(add_field, car); 
}         

它看起来像translator对象只是踢,可以在路上,但有一个translator对象将使很容易为汽车以外的东西写translators。

我们如何实现魔术翻译?

让我们从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; })

我们在两个lambdas上定义了一个重载集。其中一个获取对象的字段,另一个获取用于序列化的名称。

Implementing an overload set for lambdas

这非常简单。我们只创建一个继承自lambdas的类:

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

Implementing a translator class using a tiny bit of SFINAE

第一步是创建一个读取单个字段的类。它包含一个执行读数的lambda。如果我们可以应用lambda,我们应用它(阅读字段)。否则,我们不会应用它,也没有任何反应。

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功能非常简单。我们只是带了一堆读者,用它们来制作optionalReaders。

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

结论

这是一篇很长的帖子。我们必须构建许多基础设施才能使一切工作正常。它使用起来非常简单,并且它不需要任何关于我们应用它的类的知识,除了我们想要使用的字段。

我们可以很容易地为很多东西编写翻译!

Image translator example

例如,这里是图片和图像的翻译器,它还考虑了图片宽度和高度等不同的常用名称。

请记住,给译员的任何图像类都可以选择实现这些方法中的任何一种。

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

Here's the complete implementation


0
投票

为什么不'你使用一些序列化库来做这个?例如Boost.Serialization,Google协议缓冲区等。

© www.soinside.com 2019 - 2024. All rights reserved.