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

imp*_*sso 1 python oop abstraction class object

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

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

data Message = Greeting Foo Bar | Warning Yadda Yadda Yadda | ...
Run Code Online (Sandbox Code Playgroud)

这是一种非常好的、干净的方式,可以将它们全部放在同一个目录下type,并且能够轻松区分消息类型。

如何以 OOP 友好(或者更好,Pythonic)的方式设计对象类来实现这一效果?我想到了两种方法,即:

  • 定义基类Message并为每种消息对其进行子类化。优点:概念上干净。缺点:有很多样板代码,并且它并没有真正使代码变得非常可读或不同消息类之间的关系清晰。

  • 定义一个通用类Message,它代表每种消息类型。它将有一个属性.type来区分消息类型,并且其__init__功能将相应地实例化适合于消息类型的属性。优点:编码简单,实用。缺点:让类的属性如此不可预测似乎是一种不好的做法,而且通常感觉是错误的。

但我对两者都不完全满意。虽然我意识到这只是一个小程序,但我想我将其用作了解更多有关抽象和软件架构的使用的机会。有人能给我指路吗?

Mar*_*ers 6

对于消息类设计,我将使用数据类来最小化样板文件。您可以完全专注于以下领域:

from dataclasses import dataclass

class Message:
    # common message methods

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

@dataclass
class Warning(Message):
    yadda: list[str]
Run Code Online (Sandbox Code Playgroud)

对于一个简单的项目来说,通常不需要太多东西。您可以@classmethodMessage基类添加一个工厂来帮助生成特定的消息类型,如果不同类型之间共享公共属性,也Message可以将其本身作为工厂。@dataclass

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

为了说明这一点:对于当前包含自动化 OpenAPI 3.1 文档的 RESTFul API 项目,我们使用Marshmallow来处理 JSON 之间的转换,使用marshmallow-dataclasses来避免重复定义模式和验证,以及marshmallow-oneofschema反映类层次结构的多态模式,这些类的类型因类型而异,就像您的Message示例一样。

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

您的消息类型将表达如下:

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")
Run Code Online (Sandbox Code Playgroud)

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

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

whileMessageSchema.dump()为您提供合适的 JSON 输出,无论输入类型如何:

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

使用enumhere可以使集成效果最好;PolymorphicType是一个自定义类,它处理大部分繁重的工作以使调用_BaseMessage.OneOfSchema()最终工作。您不必使用元编程来实现最后一部分,但对我们来说,它减少了大部分marshmallow-oneschema样板文件。

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

from dataclasses import dataclass

class Message:
    # common message methods

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

@dataclass
class Warning(Message):
    yadda: list[str]
Run Code Online (Sandbox Code Playgroud)