在类层次结构中应该编写实例方法?

时间:2011-02-01 09:08:03

标签: python oop language-agnostic software-design

这是我用于模拟模型的类“层次结构”的一部分(我的代码在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的主要思想(我理解为不将所有功能集中到一个地方,而是包含它在相关课程中。)

性能方面的考虑只是次要问题(我认为两种方法之间的性能并不相同)。

编辑:我误用了上面的“类层次结构”。我没有提到继承层次结构,只是指一堆实例相互包含的类。

5 个答案:

答案 0 :(得分:3)

您需要考虑的是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(或具有世界接口的对象)的实例,因此依赖关系被推出类并进入调用代码。因此,让您的类代码保持相当开放和无依赖性,同时仍然具有生产力。

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

总结:将实例方法放在符合逻辑意义的地方。

答案 1 :(得分:1)

将移动方法放在有意义的地方,世界不能移动,代理可以。

如果您希望能够访问世界级功能,请为构造函数方法指定参数并传递世界实例。

world = World()
agent = Agent(world)

这样可以从代理中明确访问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

答案 2 :(得分:1)

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

  1. 如果将move方法放在World类中,您将如何移动单个代理?是否要将Agent的实例传递给该方法?这看起来很难看。
  2. 如果您希望单个代理执行某些操作,从OOP角度来看,在实例方法中执行此操作会更好。
  3. 您也可以从另一个不了解世界的类的实例调用move方法,但是也可以调用特定的代理。
  4. 但是,如果所有代理同时移动并且您不希望单个代理移动,则可以将一个方法moveAllAgents放入世界类中,然后遍历代理列表并移动所有这些代理商。然后,您不需要move类中的Agent方法。

答案 3 :(得分:0)

  

但这意味着移动方法不可能   在类Agent中,因为它创建了一个   依赖(至少是界面   )描述地形等的类

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

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

答案 4 :(得分:0)

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

从更一般的意义上讲,我同意其他人的观点:它完全适用于在Agent类上定义Move方法,并且代理类可以接受对其周围环境的认识。

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