6
\$\begingroup\$

I am fairly satisfied with my solution here but I would definitely appreciate any constructive criticism of my style and design. The basic idea is to use the strategy pattern to simplify assembling a custom procedural generator. I'm using modules rather than classes for improved simplicity of 'mixing' everything together.

Here's the root of the 'strategy' class tree, AbstractStrategy:

      #
      #    the contract here is that the child strategy should implement an apply
      #    function which takes a target problem instance and hash options.
      #
      module AbstractStrategy
        def apply!(problem, opts={})
          log.debug "--- AbstractStrategy.apply! problem=#{problem}, opts=#{opts}"
          apply(problem, opts)  
        end
      end

One level down, we have another 'parent' pattern which implements a particular strategy 'action' or 'style'. In this case it's an AbstractGenerationStrategy which implements apply according to the contract above.

      #
      # the contract here is that the child strategy needs to implement "generate_[component]!({})"
      #
      module AbstractGenerationStrategy extend AbstractStrategy
        include AbstractStrategy

        def extend!(base)
          log.debug("--- extending #{base}")
          self.extend(base)
        end

        #
        # => use like -- generate :rooms, ConstructNestedRoomsStrategy, ...opts...
        #
        def generate(component, strategy, opts={})
          log.debug "--- AbstractGenerationStrategy.generate[component=#{component}, strategy=#{strategy}]"
          extend!(strategy)
          apply!(component,opts)
        end

        def apply(component_to_generate, opts)
          log.debug "--- AbstractGenerationStrategy.apply component=#{component_to_generate}, opts=#{opts})"
          self.send("generate_#{component_to_generate}!", opts)
        end
      end

To see this in action, one strategy module which implements the above contract is AbstractRoomGenerationStrategy:

      #
      # contract -- expects children to implement generate_room(opts)
      #      
      module AbstractRoomGenerationStrategy extend AbstractGenerationStrategy
        include AbstractGenerationStrategy
        include LoggingHelpers

        DEFAULT_ROOM_COUNT = 3

        def generate_rooms!(opts={})
          room_count = opts[:count] || DEFAULT_ROOM_COUNT
          log.debug "generating #{opts[:count]} rooms"
          room_count.times { generate_room!(opts) }
        end

      end

Finally, a 'concrete' example of an AbstractRoomGenerationStrategy is ConstructSingleHugeRoomStrategy. Note at this point we are expecting to be 'mixed' into (technically, extended from) certain kinds of classes, and so we are expecting certain member variables (like @width and @height) to exist:

      module ConstructSingleHugeRoomStrategy extend RoomGenerationStrategy
        include RoomGenerationStrategy

        def generate_room!(opts={})
          @rooms ||= []
          log.debug 'generating room'
          if @rooms.empty?
            place_first_room(opts)
          else
            # do nothing
          end
        end

        #
        #  place a huge room in the center of the space
        #
        def place_first_room(opts)
          log.debug "placing first huge room"        
          default_opts = { 
            :width => @width/2, :height => @height/2,
            :position => [@width/4, @height/4]
          }
          opts ||= default_opts        
          room = Room.new(opts)
          log.debug "new room created: #{room}"
          @rooms << room
        end
      end

In the context of an appropriate class the operation can be invoked like

generate :rooms, ConstructSingleHugeRoomStrategy, opts

Here's a snippet from a test scenario exercising the above strategy:

log.info '--- building new space with defaults (no opts)'
@@space = Space.new 

log.debug 'calling space.generate_rooms! with ConstructSingleHugeRoomStrategy'
@@space.generate :rooms, ConstructSingleHugeRoomStrategy

log.debug 'asserting room count == 1'
@@space.rooms.count.should == 1

At the moment I can't provide a direct link to more as the repository is in a private bitbucket, though I am happy to try to provide more context if necessary.

I'd like feedback on class design and pattern usage (and of course coding style and correctness if you see real problems here.) Note the main goal of using the 'strategies' here was really just to enable me to mix and match algorithms in reusable and relatively straightforward way.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Stop trying to write Ruby like Java. You've got a whole mess of modules to do something fairly simple.

Most (not all) of the Gang of Four patterns are workarounds for lack of flexibility in languages like C++ and Java, and are less necessary in a language like Ruby that has classes that are first-class objects and always extensible.

I'd like to know a bit more about your use case, but it seems to me that you can probably remove a layer or two of indirection.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This is a very fair comment and really reasonable, thank you! \$\endgroup\$ Oct 21, 2011 at 22:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.