where in class hierarchy should instance methods b

2019-06-26 03:33发布

问题:

Here's a part of a class "hierarchy" that I use for a simulation model (my code is in Python, but I think my question isn't language dependent):

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

Question: where should I put the move instance method?

I thought I'm supposed to limit the dependency of class Agent to classes lower in the hierarchy than itself (i.e., classes whose instances are contained in Agent instances). But that means move method cannot be in class Agent since it creates a dependency on (at least the interface of) classes that describe terrain, etc - so I might as well add to Agent a reference to (and hence a dependency on) World. Is this ok from software design perspective?

The alternative is to put method move in class World, where it won't cause any additional dependencies. However, class World would then be doing almost all the work, and it seems to me that it would go against the main idea of OOP (which I understand as not to pile all the functionality into one place, but rather contain it in the relevant classes).

Performance considerations are only of minor concern (and I don't think performance would differ between the two approaches anyway).

EDIT: I misused the words "class hierarchy" above. I wasn't referring to inheritance hierarchy, just to a bunch of classes whose instances contain each other.

回答1:

The thing that you need to take into account is the Single Responsibility Principle. Basically, each class should be responsible for one "thing", and should completely encapsulate that one responsibility. And you should only inherit where the responsibility is extended. You should always be able to say that the extending class is 100% of the parent and more (more in a specific sense). You should never have a situation where the child is a subset of the parent and is "less". So a person extending a world is not a good design since there are aspects of the world that do not relate to a person.

So, if we look at an example, you would put the instance methods on the level that is dictated by the role of that particular class. So, let's take a more defined look at an example:

class Person:
    name: ""
    birthDate: ""

class PoliceOfficer extends Person:
    badgeNumber: ""

Obviously this is pseudocode, but it demonstrates what's happening.

Now, where would you add a move() method? We could add it to PoliceOfficer, but then we would break the encapsulation of Person since a person can also move.

class Person:
    def move(world):

But, where would we add an issueTicket() method? The generalized Person cannot issue a ticket, so if we added that to the Person class, we'd be breaking the responsibility of it. So instead, we'd add it to PoliceOfficer, since that's where it makes sense.

As far as creating dependency, you should always favor composition over inheritance. So in that sense, there can be as many dependencies as you'd like since they are all soft-dependencies (well, kind of). Since move() takes an instance of world (or an object with the world interface), the dependency is pushed out of the class and into the calling code. So that lets your class's code remain pretty open and dependency-free while still being productive.

It's generally seen as bad practice to hard-code dependencies. But injecting them (through Dependency Injection or Composition) is typically seen as a good thing.

In summary: Put instance methods where it makes logical sense to put them.



回答2:

I would put move into the class Agent. If not needed, the Agent shouldn't know the whole world, but only the relevant information he needs to move. If he does know the whole world, however, that is not too bad either. Here are some reasons:

  1. How would you move a single agent if you would put the move method in the World class? Do you want to pass the instance of Agent to be moved to that method? That seems quite ugly.
  2. If you want a single agent to do something, it is nicer to do that in an instance method from an OOP perspective.
  3. You can also call the move method from an instance of another class that does not know the world, but the specific agent.

However, if all your agents move simultaneously and you don't want single agents to move, you could just put one method moveAllAgents into the world class and then iterate through the list of agents and move all of these agents. You then don't need a move method in the Agent class.



回答3:

Put the move method where it makes sense, the World can't move, the Agent can.

If you're looking to be able to access world-level functionality, give your constructor method a parameter and pass the world instance in.

world = World()
agent = Agent(world)

This give explicit access to the World from your agent, rather than assuming some sort of hierarchy.

You can take this a step further and require that all game objects within the world take the world as a parameter. You could enforce this by creating a base GameObject class that your Agent, and other game objects, inherit.

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

EDIT: To extend my explanation further, if you had an Enemy class and the enemy had a move() method as well, chances are good you may want the enemy to move towards the agent. However, you don't want the enemy asking the world for the position of the agent, instead you can just store a reference to the agent inside the enemy as it's "target" and check it's position any time you need.

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


回答4:

But that means move method cannot be in class Agent since it creates a dependency on (at least the interface of) classes that describe terrain, etc

The agent must know about his surroundings. That means that the Agent must use the interfaces that describe the terrain, yes. I wouldn't call that a "dependency", though. There is nothing wrong with assuming that the class that implements ITerrain actually follows the ITerrain interface. :-)

So you put .move() on the agent. The move() method then would check the surroundings and try to move through them according to the rules of movement.



回答5:

Not knowing much about your application, you could also allow an Agent to "know" about a certain portion of your world (possibly defined as the maximum area within which movement can be executed, constrained by some rules you implement about how far the Agent can move in any one call to .Move). So the agent might contain a reference to a "Clip Region" of the greater world (this concept stolen from the "Clip Rectangle" used in .net GDI+ Graphics Object).

In a more general sense, I agree with the others: it makes complete sense for the Move method to be defined on the Agent Class, and that it is acceptable for the agent Class to posses an awareness of it's surroundings.

While OOP tends towards minimizing unecessary dependencies, when it makes sense, it makes sense. A person in the real world is aware of their surroundings, and is able to initiat the action required to move within those surroundings from one location to another.