【问题标题】:where in class hierarchy should instance methods be written?应该在类层次结构中的哪个位置编写实例方法?
【发布时间】:2011-06-19 03:25:06
【问题描述】:

这是我用于模拟模型的类“层次结构”的一部分(我的代码使用 Python,但我认为我的问题与语言无关):

class World:
# highest-level class, which "knows" everything about the model
# most likely will have just one instance
# contains (e.g., in a dictionary) references to all the instances of class Agent

class Agent:
# each instance represents an agent
# an agent can, among other things, move around according to certain rules
# movement depends on the internal state of the agent,
# but also on the terrain and other information not stored in the Agent instance

问题:move 实例方法应该放在哪里?

我认为我应该将class Agent 的依赖限制在层次结构中低于自身的类(即,其实例包含在Agent 实例中的类)。但这意味着move 方法不能在class Agent 中,因为它创建了对描述地形等的类(至少是接口)的依赖——所以我不妨在Agent 中添加对(因此依赖于)World。从软件设计的角度来看,这可以吗?

另一种方法是将方法move 放在class World 中,它不会导致任何额外的依赖关系。但是,class World 将完成几乎所有的工作,在我看来,这将违背 OOP 的主要思想(我理解为不是将所有功能都集中在一个地方,而是将其包含在相关类)。

性能方面的考虑只是次要问题(而且我认为这两种方法的性能不会有所不同)。

编辑:我误用了上面的“类层次结构”一词。我指的不是继承层次结构,只是指一组实例相互包含的类。

【问题讨论】:

  • 为了公平起见,我不认为我会这样做。在编写之前尝试编写一些“使用”您的域的代码,然后填写空白。从外部开始,向内工作通常会有所帮助。有不少关于域设计和建模的书籍可供参考。
  • 您有什么参考资料您认为可能会有所帮助吗?我在亚马逊上只找到一本关于域设计的书:amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/…

标签: python oop language-agnostic software-design


【解决方案1】:

将 move 方法放在有意义的地方,World 不能移动,Agent 可以。

如果您希望能够访问世界级功能,请给您的构造方法一个参数并传入世界实例。

world = World()
agent = Agent(world)

这使您的代理可以显式访问世界,而不是假设某种层次结构。

您可以更进一步,要求世界中的所有游戏对象都将世界作为参数。您可以通过创建您的代理和其他游戏对象继承的基本 GameObject 类来强制执行此操作。

class GameObject:
    def __init__(self, world):
        self.world = world

class Agent(GameObject):
    def __init__(self, world, startX, startY):
        # don't forget to call the super and pass the world to it
        super(Agent, self).__init__(world)
        self.startX = startX
        self.startY = startY

    def move(self):
        print 'I can see the world'
        print self.world

编辑:进一步扩展我的解释,如果你有一个 Enemy 类并且敌人也有一个 move() 方法,那么你可能希望敌人向代理人。但是,您不希望敌人向世界询问代理的位置,相反,您可以将代理的引用存储在敌人内部,因为它是“目标”,并在需要时随时检查它的位置。

class Enemy(GameObject):
    def __init__(self, world, target):
        super(Agent, self).__init__(world)
        self.target = target

    def move(self):
        if self.target.x > self.x:
            self.x += 5

【讨论】:

  • +1:创建 GameObject 类以帮助管理世界对象的想法。
  • 但我想知道执法方面。它真的确保子类将采用并正确处理world 作为参数吗?我的意思是,如果我不小心创建了class X(GameObject): def __init__(self): pass,我真的不会注意到这个问题吗?
  • 不,不一定,但任何类的扩展都是如此。正确实现扩展类取决于扩展器。如果您使用的是更强类型的语言,您可以尝试更多地使用接口来强制执行它,但即使这样也可以规避。只需放弃严格执行的概念并正确记录您的类,以便您知道如何正确扩展它。
【解决方案2】:

我会将move 放入Agent 类中。如果不需要,Agent 应该不知道整个世界,而只知道他需要移动的相关信息。不过,如果他真的了解整个世界,那也不算太糟糕。以下是一些原因:

  1. 如果您将move 方法放在World 类中,您将如何移动单个代理?是否要将 Agent 的实例传递给该方法?这看起来很丑陋。
  2. 如果您希望单个代理执行某项操作,从 OOP 的角度来看,最好在实例方法中执行此操作。
  3. 您还可以从另一个类的实例调用 move 方法,该类不知道世界,但知道具体的代理。

但是,如果您的所有代理同时移动并且您不希望单个代理移动,则可以将一种方法 moveAllAgents 放入世界级,然后遍历代理列表并移动所有这些代理。这样您就不需要 Agent 类中的 move 方法了。

【讨论】:

  • 实际上,在我的情况下,看起来我只是一次全部移动代理。但我担心它将来可能会改变,所以我不敢承诺。在任何情况下,我总是将代理移动在一起这一事实是否是在 Agent 中不使用 move 方法的充分理由?
  • 嗯,我认为这是一个很好的理由,因为这样你只需要调用一个方法,你不需要遍历所有代理并在每个代理上调用 move 方法。但是,如果您将来可以更改它,我最好在Agent 中实现move 方法 - 否则,您将有很多重构工作要做​​。另一种方法是将两者结合起来:在World 中有一个方法,您可以在其中迭代所有代理并在每个代理上调用move 方法。
【解决方案3】:

但这意味着 move 方法不能 在类代理中,因为它创建了一个 依赖于(至少是接口 of) 描述地形等的类

代理必须了解他的周围环境。这意味着代理必须使用描述地形的接口,是的。不过,我不会称其为“依赖”。假设实现 ITerrain 的类实际上遵循 ITerrain 接口并没有错。 :-)

所以你把 .move() 放在代理上。然后 move() 方法将检查周围环境并尝试根据移动规则在它们之间移动。

【讨论】:

    【解决方案4】:

    不太了解您的应用程序,您也可以允许代理“了解”您世界的某个部分(可能定义为可以执行移动的最大区域,受您实施的有关多远的一些规则的限制代理可以在对 .Move 的任何一次调用中移动)。因此,代理可能包含对更大世界的“剪辑区域”的引用(这个概念是从 .net GDI+ 图形对象中使用的“剪辑矩形”中窃取的)。

    在更一般的意义上,我同意其他人的观点:在代理类上定义 Move 方法是完全有意义的,并且代理类拥有对其周围环境的感知是可以接受的。

    虽然 OOP 倾向于最小化不必要的依赖关系,但当它有意义时,它就有意义了。现实世界中的人知道他们的周围环境,并且能够发起在这些环境中从一个位置移动到另一个位置所需的动作。

    【讨论】:

    • 我在查看您的编辑时发现了另一个错字。时间还早,我的咖啡还没喝完。 . .
    • 别担心,从技术上讲,它们是“计算机生成的图像”,因此称它们为“CGI”并不是虚假声明;)
    【解决方案5】:

    您需要考虑的是Single Responsibility Principle。基本上,每个类都应该负责一个“事物”,并且应该完全封装那个责任。你应该只继承责任延伸的地方。您应该始终能够说扩展类是父类的 100% 甚至更多(在特定意义上更多)。您永远不应该遇到孩子是父母的子集并且“更少”的情况。因此,扩展世界的人并不是一个好的设计,因为世界的某些方面与人无关。

    因此,如果我们看一个示例,您可以将实例方法置于该特定类的角色所规定的级别上。所以,让我们更明确地看一个例子:

    class Person:
        name: ""
        birthDate: ""
    
    class PoliceOfficer extends Person:
        badgeNumber: ""
    

    显然这是伪代码,但它演示了正在发生的事情。

    现在,您将在哪里添加 move() 方法?我们可以将它添加到PoliceOfficer,但是我们会破坏Person 的封装,因为人也可以移动。

    class Person:
        def move(world):
    

    但是,我们应该在哪里添加issueTicket() 方法?广义的Person 不能开票,所以如果我们将它添加到Person 类中,我们将打破它的责任。因此,我们将其添加到 PoliceOfficer,因为这是有意义的地方。

    至于创建依赖,你应该always favor composition over inheritance。所以从这个意义上说,可以有尽可能多的依赖关系,因为它们都是软依赖关系(嗯,有点)。由于move() 采用world 的实例(或具有世界接口的对象),因此依赖项被推出类并进入调用代码。这样一来,您的班级代码就可以保持相当开放和无依赖,同时仍然保持高效。

    硬编码依赖项通常被视为不好的做法。但是注入它们(通过依赖注入或组合)通常被视为一件好事。

    总结:将实例方法放在合理的地方。

    【讨论】:

    • 谢谢 - 澄清一下:我不打算从 World 继承 AgentWorld 对象仅包含(引用)在该世界中创建的所有 Agent 对象。
    • @Max:好的,那么它们属于不同的类层次结构(我从你的问题中不清楚)。所以使用注入或组合将 Agent 的引用添加到 World 中......并且不要将 World 硬编码到 Agent 类中。来回传递引用(这样你就可以将World 添加到Agent 的构造函数中,然后调用word.addAgent(self) 方法)......这样你仍然保持依赖于类之外,同时允许您正在尝试做什么...希望对您有所帮助...
    • 我关心的依赖是这个。 agent.move() 方法需要知道如何检查地形。当然,检查地形的接口是不同类的一部分,我希望完全独立于类Agent。这个依赖可以吗?
    • @max:嗯,它是,它不是。不要依赖于实际的类实例(这是紧耦合)。相反,创建一个描述所需功能的接口,并依赖于它。这样你就与类松耦合,但与接口紧密耦合(这没关系,因为任何类都可以实现接口)。
    • 另一个复杂情况:world 对象想要快速检查给定位置是否有代理。如果Agent 类中的move 方法,则必须更新映射到Agent 对象的world 内部的内部表。怎么办???
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多