【问题标题】:OOP approach to objects with many flavours具有多种风格的对象的 OOP 方法
【发布时间】:2021-09-05 01:18:05
【问题描述】:

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

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

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

将它们都放在同一个type 下并且能够轻松区分消息类型,这是一种非常好的和干净的方式。

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

  • 定义基类Message 并为每种消息子类化它。 优点:概念上干净。 缺点:大量的样板代码,并没有真正使代码具有很强的可读性或不同消息类之间的关系清晰。

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

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

【问题讨论】:

  • 这两种方法都可以,但第二种方法似乎更轻松。你可以定义一个enum 来让你更好地控制潜在的消息类型,同时保持类的描述性
  • 制作一个简单的minimal reproducible example,包含几个字符串以及您打算如何使用它们。
  • 你考虑过 Python 数据类吗?那些应该给你干净的代码,没有太多的样板。
  • @joshmeranda 感谢您的参与!我自己倾向于第一种方法,但是这个enum 索引确实很有趣
  • @MadPhysicist 谢谢你的回复!正如我所看到的,我的问题本质上更笼统,我不希望它过分依赖实现细节。可以这么说,我担心添加这些额外的信息会搅浑水,但如果你认为它对你有帮助,我不介意分享它。

标签: python class object oop abstraction


【解决方案1】:

对于消息类设计,我会使用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 基类以帮助生成特定的消息类型,并且Message 本身也可以是@dataclass,如果在不同类型之间有共同的属性。

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

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

使用第 3 方库会限制您的选择,因此我使用元编程(主要是 class.__init_subclass__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 是处理大部分繁重工作以使 _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

【讨论】:

  • 感谢您的详细解答;这与@David 所说的完全一致。我接受它,同时我在考虑你在下半场提出的建议,干杯!
猜你喜欢
  • 2015-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-14
相关资源
最近更新 更多