具有多种风格的对象的 OOP 方法

OOP approach to objects with many flavours

我一直在编写代码来解析和提取机器人发送的消息中的信息。只有几种不同类型的消息,但它们中的每一种都包含我感兴趣的各种不同类型的信息,我正在努力寻找在我的代码中将它们作为对象处理的最佳方法。

如果我使用 Haskell,我会简单地创建一个类型 Message 并为每种消息定义一个定制的构造函数

data Message = Greeting Foo Bar | Warning Yadda Yadda Yadda | ...

这是一种非常好的和干净的方式,既可以将它们全部放在同一个 type 下,又可以轻松区分消息类型。

如何以 OOP 友好(或更好的 pythonic)方式设计对象 classes? 我想到了两种方法,即:

但我对两者都不完全满意。虽然我意识到这只是一个小程序,但我想我正在利用它作为一个机会来了解更多关于抽象和软件架构的使用。谁能给我指路?

对于消息 class 设计,我会使用 dataclasses 来最小化样板文件。您可以完全专注于这些领域:

from dataclasses import dataclass

class Message:
    # common message methods

@dataclass
class Greeting(Message):
    foo: str
    bar: int

@dataclass
class Warning(Message):
    yadda: list[str]

一个简单的项目通常不需要更多。您可以将 @classmethod 工厂添加到 Message 基础 class 以帮助生成特定的消息类型,并且 Message 本身也可以是 @dataclass,如果有的话不同类型之间共享的共同属性。

就是说,一旦您开始考虑序列化和反序列化要求,使用 enum type 字段会有所帮助。

为了说明这一点:对于包含自动 OpenAPI 3.1 文档的当前 RESTFul API 项目,我们使用 Marshmallow to handle translation from and to JSON, marshmallow-dataclasses to avoid having to repeat ourselves to define the schema and validation, and marshmallow-oneofschema 来反映多态模式class 的层次结构,其类型因类型而异,很像您的 Message 示例。

然后使用第 3 方库会限制您的选择,因此我使用元编程(主要是 class.__init_subclass__ and Generic type annotations)来简洁地定义这种以枚举为键的多态类型层次结构。

您的消息类型可以这样表示:

class MessageType(enum.Enum):
    greeting = "greeting"
    warning = "warning"
    # ...

@dataclass
class _BaseMessage(PolymorphicType[MessageType]):
    type: MessageType
    # ...

@dataclass
class Greeting(_BaseMessage, type_key=MessageType.greeting):
    foo: str
    bar: int

@dataclass
class Warning(_BaseMessage, type_key=MessageType.warning):
    yadda: list[str]

MessageSchema = _BaseMessage.OneOfSchema("MessageSchema")

之后使用 MessageSchema.load() 从 JSON 加载消息,根据字典中的 "type" 键生成特定实例,例如

message = MessageSchema.load({"type": "greeting", "foo": "spam", "bar": 42})
isinstance(message, Greeting)  # True

MessageSchema.dump() 让你得到合适的 JSON 输出而不管输入类型:

message = Warning([42, 117])
MessageSchema.dump(message)  # {"type": "warning", "yadda": [42, 117]}

这里使用 enum 使集成效果最好; PolymorphicType 是自定义 class,它处理大部分繁重的工作,使 _BaseMessage.OneOfSchema() 调用在最后工作。您 没有 使用元编程来实现最后一部分,但对我们来说它减少了大部分 marshmallow-oneschema 样板。

此外,我们获得了反映每个特定消息类型的 OpenAPI 模式,Redocly 等文档工具知道如何处理:

components:
  schemas:
    Message:
      oneOf:
        - $ref: '#/components/schemas/Greeting'
        - $ref: '#/components/schemas/Warning'
      discriminator:
        propertyName: type
        mapping:
          greeting: '#/components/schemas/Greeting'
          warning: '#/components/schemas/Warning'
    Greeting:
      type: object
      properties:
        type:
          type: string
          default: greeting
        foo:
          type: string
        bar:
          type: integer
    Warning:
      type: object
      properties:
        type:
          type: string
          default: warning
        yadda:
          type: array
          items:
            type: string