Variable class (newb question)

I'm trying to figure out how to use Ruby to implement a strategy type
pattern that I used in PHP. Basically I took a set of class names passed in
as variables and instantiated the right class depending on the value of the
variable. It was roughly something like:

Class SurveyQuestion
  drawQuestion
  storeResponse
  reportResponse
  ...

Class SurveyQuestionMultiChoice extends SurveyQuestion
Class SurveyQuestionCheckBox extends SurveyQuestion
...etc.

//build a list of question types based on what the user just submitted
$these_survey_questions = array('MultiChoice', 'MultiChoice', 'CheckBox')

For each $these_survey_questions as $index=$question_type
  $class_name = "SurveyQuestion".$question_type
  $q = new $class_name()
  $q.storeResponse($response_from_this_user)

So I'm struggling to figure out how this type of thing would typically be
done using Ruby. Or maybe it's a bad approach to this type of situation to
begin with so feel free to offer an alternative. Thanks.

Shaun

Shaun Fanning wrote:

I'm trying to figure out how to use Ruby to implement a strategy type
pattern that I used in PHP. Basically I took a set of class names passed in
as variables and instantiated the right class depending on the value of the
variable. It was roughly something like:

Class SurveyQuestion
  drawQuestion
  storeResponse
  reportResponse
  ...

Class SurveyQuestionMultiChoice extends SurveyQuestion
Class SurveyQuestionCheckBox extends SurveyQuestion
....etc.

One way using a base class and subclassing similar to your approach in PHP:

class A
    def talk
       "talking"
    end
end

class B < A
end

defget_class_for_string( class_name )
    eval( "#{class_name}.new" )
end

obj = get_class_for_string( "B" )
obj.talk

//build a list of question types based on what the user just submitted
$these_survey_questions = array('MultiChoice', 'MultiChoice', 'CheckBox')

For each $these_survey_questions as $index=$question_type
  $class_name = "SurveyQuestion".$question_type
  $q = new $class_name()
  $q.storeResponse($response_from_this_user)

So I'm struggling to figure out how this type of thing would typically be
done using Ruby. Or maybe it's a bad approach to this type of situation to
begin with so feel free to offer an alternative.

The only thing I don't like about the above solution is potential insecurity if someone passes in "<code here> ; B" , the <code here> would execute. This could changed to be more secure if you were always using toplevel classes...

def get_class_for_string( class_name )
     eval("#{class_name}.new") if Object.constants.include?( class_name )
end

Which the above code makes sure that the passed in class_name has been defined on the top-level Object otherwise it will return nil. This won't work for things like "MyModule::B" or "MyClass::InnerClass:B", although you could change it to work. And here is the modified version to make it work across the board:

def get_class_for_string( class_name )
     last_constant = Object
     class_name.split( /::confused: ).each do |cons_str|
        if last_constant.constants.include?( cons_str )
          last_constant = eval( "last_constant::#{cons_str}" )
        else
          return nil
        end
     end
     eval("#{last_constant}.new")
end

Then you could do stuff like...

get_class_for_string( "A" )
get_class_for_string( "M::C" )
get_class_for_string( "M::C::D::E::F" )

HTH,

Zach

Hi --

I'm trying to figure out how to use Ruby to implement a strategy type
pattern that I used in PHP. Basically I took a set of class names passed in
as variables and instantiated the right class depending on the value of the
variable. It was roughly something like:

Class SurveyQuestion
drawQuestion
storeResponse
reportResponse
...

Class SurveyQuestionMultiChoice extends SurveyQuestion
Class SurveyQuestionCheckBox extends SurveyQuestion
...etc.

//build a list of question types based on what the user just submitted
$these_survey_questions = array('MultiChoice', 'MultiChoice', 'CheckBox')

For each $these_survey_questions as $index=$question_type
$class_name = "SurveyQuestion".$question_type
$q = new $class_name()
$q.storeResponse($response_from_this_user)

So I'm struggling to figure out how this type of thing would typically be
done using Ruby. Or maybe it's a bad approach to this type of situation to
begin with so feel free to offer an alternative. Thanks.

This might be a good case for a class method approach:

   class Survey
     class Question
       def self.store_response(kind, body)
         case kind
           when "multichoice"
             # ...
           when "checkbox"
             # ...
           # etc.
         end
       end
     end
   end

and then if you can get your kinds and your responses into, say, an
array of little arrays:

   questions.each {|q| Survey::Question.store_response(*q) }

where q, each time, would be something like: ["checkbox","D"].

Another possibility (but I'd have to see more of how your data is
being passed around to know) might be some kind of serialization that
ended up with an array of objects that knew their own class, and knew
how to store themselves -- resulting in something like:

   questions = <some kind of deserialization of a bunch of data, maybe>
   questions.each {|q| q.store_response }

which is, as they say, "more OO".

David

···

On Wed, 20 Jul 2005, Shaun Fanning wrote:

--
David A. Black
dblack@wobblini.net

Hi --

The only thing I don't like about the above solution is potential insecurity if someone passes in "<code here> ; B" , the <code here> would execute. This could changed to be more secure if you were always using toplevel classes...

def get_class_for_string( class_name )
   eval("#{class_name}.new") if Object.constants.include?( class_name )
end

Which the above code makes sure that the passed in class_name has been defined on the top-level Object otherwise it will return nil. This won't work for things like "MyModule::B" or "MyClass::InnerClass:B", although you could change it to work. And here is the modified version to make it work across the board:

def get_class_for_string( class_name )
   last_constant = Object
   class_name.split( /::confused: ).each do |cons_str|
      if last_constant.constants.include?( cons_str )
        last_constant = eval( "last_constant::#{cons_str}" )
      else
        return nil
      end
   end
   eval("#{last_constant}.new")
end

A more concise and eval-free way to do that is:

   class Module
     def deep_const_get(str)
       str.split("::").inject(Object) {|a,b| a.const_get(b) }
     end
   end

"Traditional", as they say for folk songs :slight_smile: I've written it and
others have too -- I don't know who first.

Note that there's no need to test for failure; it will fail if any
call to const_get fails.

David

···

On Thu, 21 Jul 2005, Zach Dennis wrote:

--
David A. Black
dblack@wobblini.net

Basically since the data is coming from an HTTP form post, I am able to name
the elements such that PHP will send it all back as a bunch of associative
arrays and each storeResponse method knows how to parse those arrays and how
to put the responses into the database. I need to study Rails more to see
how complex POST responses are typically sent back and parsed since I will
be using it soon, but I wanted to think through this design problem down at
the Ruby level because I am trying to learn how to "think" in Ruby first.
Thank you for all the feedback. I have learned a great deal about Ruby just
through this little thread.

Shaun

···

On 7/20/05 12:33 PM, "David A. Black" <dblack@wobblini.net> wrote:

Another possibility (but I'd have to see more of how your data is
being passed around to know) might be some kind of serialization that
ended up with an array of objects that knew their own class, and knew
how to store themselves -- resulting in something like:

   questions = <some kind of deserialization of a bunch of data, maybe>
   questions.each {|q| q.store_response }

which is, as they say, "more OO".

David

David A. Black said:

A more concise and eval-free way to do that is:

   class Module
     def deep_const_get(str)
       str.split("::").inject(Object) {|a,b| a.const_get(b) }
     end
   end

"Traditional", as they say for folk songs :slight_smile: I've written it and
others have too -- I don't know who first.

Definitely traditional, I've seen it many times in the last few months
while reading this list.

The question is, why can't the default implementation of const_get just do
this out of the box???

Ryan

David A. Black wrote:

Hi --

The only thing I don't like about the above solution is potential insecurity if someone passes in "<code here> ; B" , the <code here> would execute. This could changed to be more secure if you were always using toplevel classes...

def get_class_for_string( class_name )
   eval("#{class_name}.new") if Object.constants.include?( class_name )
end

Which the above code makes sure that the passed in class_name has been defined on the top-level Object otherwise it will return nil. This won't work for things like "MyModule::B" or "MyClass::InnerClass:B", although you could change it to work. And here is the modified version to make it work across the board:

def get_class_for_string( class_name )
   last_constant = Object
   class_name.split( /::confused: ).each do |cons_str|
      if last_constant.constants.include?( cons_str )
          last_constant = eval( "last_constant::#{cons_str}" )
      else
        return nil
      end
   end
   eval("#{last_constant}.new")
end

A more concise and eval-free way to do that is:

  class Module
    def deep_const_get(str)
      str.split("::").inject(Object) {|a,b| a.const_get(b) }
    end
  end

"Traditional", as they say for folk songs :slight_smile: I've written it and
others have too -- I don't know who first.

Note that there's no need to test for failure; it will fail if any
call to const_get fails.

Very beautiful! And thank you for the more concise and better implemented code, I will have to add this to my repertoire.

Zach

···

On Thu, 21 Jul 2005, Zach Dennis wrote: