From: Joel VanderWerf Date: 2005-11-28T07:39:17+09:00 Subject: Re: Object access heirarchies Leslie Viljoen wrote: > I often have this problem in my projects. > > I have a bunch of classes to do various things in my program, but > once instantiated they each need access to each other. For example, > in the adventure game I am working on, I have a Parser object that > needs to manipulate a Player, Map and Things in the map. > > I normally do such things by passing objects around when initializing, > like so: > > parser = Parser(Player.new(World.new(Map.new, Things.new))) > > That works, although it seems a little long winded - for the > parser to modify a thing, it needs to set something like: > player.world.thing["ball"].name = "bat" > > Later, the Things needed access to the Map so I could set thing.location > to a map.room object. So I changed thing.location to be a string -- > thing.location_name and made a function that could look up a location > based on it's name: map["garden"]. > > What I want to know is: what's the best way? I had globals $player, > $world, $map > earlier and that makes things a bit easier but it also makes me nervous > (perhaps just because I have been anti-global-indoctrinated for 20 years > now) > I'm still a bit vague on Modules. Any ideas? > > Les > Modules are typically used not for organizing instances, but for grouping methods and constants, so that chuncks of code can be reused in different situations. Why not simply replace the globals with a Game class and an instance that has attributes for player, world, map, etc.? If your program is complex enough, you may find a technique called "dependency injection" useful. It's kind of like the Game class suggestion, with a little extra syntactic sugar. DI is a way of extracting the connection logic (which you are now doing by "passing objets around when initializing") from the rest of the object creation code. With DI, you define containers for your instances, and you can decide whether a particular kind of instance should be unique in the container, or there should be one instance for each key (such as a location name). There are several DI frameworks for ruby (see RAA). Here's an example using my own MinDI library: require 'mindi' Parser = Struct.new :game # add parser state vars here Player = Struct.new :game, :location, :contents World = Struct.new :game # add state vars (:time, maybe) Map = Struct.new :game # The :game attr is useful if you want to write methods of # these classes (Player for example) that refer to the other # game objects. But it's not strictly necessary. Room = Struct.new :name, :contents # Sometimes, a struct is not enough... class Thing attr_accessor :name attr_reader :location def initialize name, location = nil @name = name @location = location end # Moving a Thing updates the Room it's in or Player who has it def location=(loc) @location.contents.delete self if @location @location = loc @location.contents << self if @location end end class GameContainer extend MinDI::Container # These are some singletons--one instance each. parser { Parser.new self } player { Player.new self, start_room, [] } world { World.new self } map { Map.new self } # A multiton. # The |name| means there can be many things--one for each name. # internally, there is a hash stored in @thing that maps # each name strign to a Thing. thing { |name| Thing.new name } # The shovel (unique with that name). shovel { thing "Shovel" } room { |name| Room.new name, [] } start_room { room "garden" } # Set up some initial conditions def initialize # create and locate the shovel shovel.location = start_room end end game = GameContainer.new # The shovel is already defined: p game.shovel puts # Create a new thing: ball = game.thing("ball") ball.location = game.room "basement" p ball puts player = game.player # pick up the shovel and anything else in the start room # This could be made into a #pick_up method of Game or of Player player.location.contents.each { |thing| thing.location = player } p player.contents.map {|thing| thing.name} puts # move the player p player.location.name player.location = game.room "basement" p player.location.name puts # get the ball player.location.contents.each { |thing| thing.location = player } p player.contents.map {|thing| thing.name} puts # You can define new methods on the GameContainer that access the internal # storage used by the "service" methods. class GameContainer def rooms @room.values # because internally the "room" service uses a hash stored in # the @room instance var end def things @thing.values end end # Show all the rooms p game.rooms # Show all the things p game.things.map {|thing| thing.name} __END__ Output: #]>> #]>> ["Shovel"] "garden" "basement" ["Shovel", "ball"] [#, #] ["ball", "Shovel"] -- vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407