java - 模式:在数据对象和有线格式之间创建和转换
问题描述
我倾向于只使用我已经在应用程序代码的各个地方使用的映射器模式。但我认为它实际上可能不是最适合这种特殊情况:
任务:
我需要根据给定的规范实现数据对象。规范为每种对象类型定义了多个版本,因此我有一个类 CarV1 和 CarV2 代表规范的每个版本。
我需要在类(在这种情况下为 C++,但问题是关于一般设计)和有线格式(Json、Protocol Buffers)之间转换这些模型,反之亦然。
对象的构造相当简单。
正如我所说,我通常会使用映射器模式,定义映射器接口和具体映射器以在每种格式之间进行映射。尽管我在这种情况下征求您的意见,但有两件事:
我只使用映射器模式来映射两种,只有两种格式类型,例如数据库对象和模型类。在这种情况下,我已经有了第三种格式,在不久的将来我可能需要添加更多格式来进行转换。
版本控制在映射之上增加了一些复杂性,我认为在两者之间需要有另一个间接。
我已经阅读过翻译模式 [1],但从未使用过它。我认为它在某种程度上适合,但不完全。
我还考虑了一个抽象工厂。这将允许创建类似的对象(在我的情况下是版本化对象)。但它不适合对象表示之间的映射。
我应该使用什么模式,为什么?
[1] http://www.iro.umontreal.ca/~keller/Layla/translator.pdf
解决方案
实施目标
我们将编写一个自动翻译器。假设我们有一个代表我们的有线格式的对象:
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
任何东西的派生,或者是任何东西的基类 - 我们的实现应该使添加新字段变得微不足道,而不会破坏向后兼容性。
具有四个独立 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's fields.
translator.translate(add_field, car);
}
看起来translator
对象只是踢,罐子在路上,但是拥有一个translator
对象可以很容易地translator
为汽车以外的东西写 s。
我们如何实现魔术翻译?
让我们从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 上定义了一个重载集。其中一个获取对象的字段,另一个获取用于序列化的名称。
为 lambda 实现重载集
这很简单。我们只需创建一个继承自两个 lambda 的类:
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 实现一个翻译器类
第一步是创建一个读取单个字段的类。它包含一个执行读取的 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
函数真的很简单。我们只是拿了一堆读者,然后用他们来制作optionalReader
s。
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()));
}
这是完整的实现
推荐阅读
- swift - 用户收到推送通知后更新表视图中的数据
- tensorflow - 在打开 csvfile 时在 python 中引发 pickle.PicklingError("Cannot pickle closed files")
- asp.net-web-api - WEB API 多列中的一个文本框值搜索
- c# - 如何创建可以从 Microsoft Access 调用的 .NET 程序集?(完全丧失)
- php - 在不同的列中显示帖子
- javascript - React-native 使用钩子更新数组的值
- python - facebook_scraper.exceptions.LoginRequired:需要登录(cookies)才能看到这个页面
- python - 导入“spacy”无法解决 Pylance (reportMissingImports)
- python - python字典中的有效占位符
- wordpress - 如何通过 Timber 库/WordPress 在 Twig 中输出当地时间?