Variable class (newb question)

S

Shaun Fanning

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
 
Z

Zach Dennis

Shaun said:
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( /::/ ).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
 
D

David A. Black

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
 
D

David A. Black

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( /::/ ).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 :) 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
 
Z

Zach Dennis

David said:
Hi --




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 :) 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
 
S

Shaun Fanning

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

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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,767
Messages
2,569,570
Members
45,045
Latest member
DRCM

Latest Threads

Top