From: Leslie Viljoen Date: 2005-11-29T04:58:03+09:00 Subject: Re: Object access heirarchies Joel VanderWerf wrote: >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: > >#name="garden", contents=[#]>> > >#name="basement", contents=[#]>> > >["Shovel"] > >"garden" >"basement" > >["Shovel", "ball"] > >[#, #name="basement", contents=[]>] >["ball", "Shovel"] > > > > Thanks a lot for that reply, it's very interesting indeed. It's a little over my head, so I am looking into DI. Les