具有多种风格的对象的 OOP 方法
OOP approach to objects with many flavours
我一直在编写代码来解析和提取机器人发送的消息中的信息。只有几种不同类型的消息,但它们中的每一种都包含我感兴趣的各种不同类型的信息,我正在努力寻找在我的代码中将它们作为对象处理的最佳方法。
如果我使用 Haskell,我会简单地创建一个类型 Message
并为每种消息定义一个定制的构造函数
data Message = Greeting Foo Bar | Warning Yadda Yadda Yadda | ...
这是一种非常好的和干净的方式,既可以将它们全部放在同一个 type
下,又可以轻松区分消息类型。
如何以 OOP 友好(或更好的 pythonic)方式设计对象 classes?
我想到了两种方法,即:
定义一个 base-class Message
并为每种消息定义 subclassing。 优点: 概念清晰。 缺点: 大量样板代码,并不能真正使代码具有可读性或不同消息之间的关系class清晰。
定义一个通用的classMessage
,代表每一种消息类型。它将有一个属性 .type
来区分消息类型,它的 __init__
函数将相应地实例化适合消息类型的属性。 优点: 代码简单,实用。 缺点: 让 class' 属性如此不可预测似乎是一种不好的做法,而且通常感觉不对。
但我对两者都不完全满意。虽然我意识到这只是一个小程序,但我想我正在利用它作为一个机会来了解更多关于抽象和软件架构的使用。谁能给我指路?
对于消息 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
我一直在编写代码来解析和提取机器人发送的消息中的信息。只有几种不同类型的消息,但它们中的每一种都包含我感兴趣的各种不同类型的信息,我正在努力寻找在我的代码中将它们作为对象处理的最佳方法。
如果我使用 Haskell,我会简单地创建一个类型 Message
并为每种消息定义一个定制的构造函数
data Message = Greeting Foo Bar | Warning Yadda Yadda Yadda | ...
这是一种非常好的和干净的方式,既可以将它们全部放在同一个 type
下,又可以轻松区分消息类型。
如何以 OOP 友好(或更好的 pythonic)方式设计对象 classes? 我想到了两种方法,即:
定义一个 base-class
Message
并为每种消息定义 subclassing。 优点: 概念清晰。 缺点: 大量样板代码,并不能真正使代码具有可读性或不同消息之间的关系class清晰。定义一个通用的class
Message
,代表每一种消息类型。它将有一个属性.type
来区分消息类型,它的__init__
函数将相应地实例化适合消息类型的属性。 优点: 代码简单,实用。 缺点: 让 class' 属性如此不可预测似乎是一种不好的做法,而且通常感觉不对。
但我对两者都不完全满意。虽然我意识到这只是一个小程序,但我想我正在利用它作为一个机会来了解更多关于抽象和软件架构的使用。谁能给我指路?
对于消息 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