多态成员变量 - class 设计

Polymorphic member variable(s) - class design

想知道是否有人可以帮助确定更优雅的设计方法 - 或者可能确定以下设计的缺点。

目前,我有一个抽象 Response class 派生自可序列化 JSON Object.

//objects.h
struct Object
{
    [[nodiscard]] std::string serialize() const;

    virtual void deserialize(const Poco::JSON::Object::Ptr &payload) = 0;

    [[nodiscard]] virtual Poco::JSON::Object::Ptr to_json() const = 0;
};
// response.h
class Response : public Object
{
public:
    std::unique_ptr<Data> data;
    std::unique_ptr<Links> links;
};

其中 DataLinks 成员变量都是抽象基础 classes - 其中它们各自的子集classes 包含各种 STL 容器。

现在我面临的问题是 class 设计之一 - 以及如何避免根据派生 Response 向下转换每个成员变量(并确定更干净的 hierarchy/design).例如...

ResponseConcreteA response_a;
response_a.deserialize(object_a);
auto data_a = static_cast<DataConcreteA *>(response_a.data.get());

ResponseConcreteB response_b;
response_b.deserialize(object_b);
auto data_b = static_cast<DataConcreteB *>(response_b.data.get());

看似显而易见的解决方案是放弃多态成员变量,代之以各自的具体类型。但是 - 我担心的是,这偏离了 Response having Data & Links 成员的固有关系,每个成员都是特定的多态类型。

需要注意的一件重要事情是,归因于 DataLinks 的具体类型是在编译时确定的 - 派生的 classes 没有必要在编译时更改任何一点。相应的构造由以下预处理模板管理:

#define DECLARE_RESPONSE_TYPE(type_name, data_name, links_name \
        struct type_name final : public Response \
        { \
            type_name() \
            { \
                data.reset(new data_name()); \
                links.reset(new links_name()); \
            } \
            ~type_name() = default; \
            void deserialize(const Poco::JSON::Object::Ptr &payload) override; \
            Poco::JSON::Object::Ptr to_json() const override; \
        };

是否有更合适的方法来避免在我的设计中需要不断向下转型的这些多态成员变量(尽管派生对象指向的事实在编译时是已知的)。谢谢!

(我正在改编 one of my recent Reddit comments 回答了基本相同的问题。)

一般

不要用继承来模拟序列化!这是您想要附加到任意类型的横切关注点。继承是错误的工具。该方法的一些问题:

  • 你强迫所有可序列化的东西成为一个成熟的多态class,并带有所有相关的开销。
  • 您需要控制要序列化的类型,这意味着您不能在不包装的情况下使用第 3 方类型。
  • 因为序列化是横切的,所以您可能 运行 在某些时候陷入继承菱形问题。
  • 基本类型不能以这种方式序列化。您不能使 int 派生自 Serializable.

模式匹配是一种更灵活的方法。简而言之,您模板化您的序列化框架并依赖于某些可用于可序列化类型的函数。快速、肮脏和天真的例子:

struct Something {
    // ...
};

// If these two functions exist a type has serialization support
void serialize(const Something&, SerializedDataStream&);
Something deserialize(SerializedDataStream&);

现在您可以使任何内容可序列化,而无需触及类型。这比继承灵活得多,但可能会使序列化框架的实现变得有些棘手。此外,支持(反)序列化成员函数对于需要访问其私有数据以正确(反)序列化的更复杂类型来说是个好主意。

查看 Boost Serialization or Cereal 模式匹配方法的真实示例。

在您的特定情况下

为了序列化更大的嵌套结构,您拆分了序列化功能。每种类型都必须知道如何序列化自身,但仅此而已。序列化复杂成员是委托给该成员的,因为它也必须知道如何序列化自己。这样您就可以逐步构建最终的 JSON。

One important thing to note is that the concrete types attributed to Data & Links are determined at compile time

显而易见的解决方案是将 Response 变成模板。

template <typename Data, typename Links>
class Response  // note: no more base class
{
public:
    Data data;
    Links links;
};

// externalized serialization functions
void serialize(const Response&, JSONDataStream&);
Response deserialize(JSONDataStream&);

这样您就可以使用正确的类型来找到 DataLinks 的序列化函数的正确重载,委托归结为简单地调用它们。该方法是否可行取决于更大的背景。将模板改装到依赖多态性的项目中可能会在整个代码库中产生连锁反应。换句话说,这可能是一个非常昂贵的改变。

备选方案与您已经在做的类似。 Response 本身仍然使用模式匹配的方式进行序列化。但是您保留 DataLinks 的多态性,包括覆盖的虚拟序列化函数。在每个具体的派生类型中,我们回到了“每个类型都知道如何序列化自己”的最初想法。如果具体的 DataLinks classes 也需要在其他上下文中序列化(而不是作为 Response 的成员),请为它们实现模式匹配函数并从覆盖的成员函数中调用它们。否则序列化可以直接在这些成员函数中发生。

class Data
{
public:
    virtual ~BaseData() = default;
    
    void deserialize(const Poco::JSON::Object::Ptr &payload) = 0;
    Poco::JSON::Object::Ptr to_json() const = 0;
    
    //...
};

class ConcreteData
{
public:
    ~BaseData() override = default;
    
    void deserialize(const Poco::JSON::Object::Ptr &payload)
    {
        // ...
    }
    
    Poco::JSON::Object::Ptr to_json() const
    {
        // ...
    }
}

// ------

Poco::JSON::Object::Ptr Response::to_json() const
{
    // ...
    
    auto serializedData = data->to_json();
    
    // ...
}