Surely there's a better way to do this... (implementing a DSL)

T

Tony Arcieri

[Note: parts of this message were removed to make it a legal post.]

I'm trying to write a method that builds a class and takes arguments from
the outer scope when doing so:

def awesome_mcjobify(state, options = {}, &action)
subject_class = @subject_class

job = Class.new do
metaclass = class << self; self; end

metaclass.send :define_method, :perform do |id|
subject = subject_class.find(id)
action subject
end

metaclass.send :define_method, :action, &action
end

subject_class.const_set "#{state.to_s.camelize}Job", job
end

This is trying to do a lot of things at once and feels icky. It's building a
class that responds to a couple of methods, encapsulates a bit of state
(into the class itself, I guess?), then sticks that class namespaced
underneath another given class.

Refactor me? :) I know there's supposed to be define_singleton_method in
1.9, but using define_*method at all (not to mention send :define_method)
seems a lot messier than it could potentially be.

Any suggestions?
 
R

Robert Klemme

I'm trying to write a method that builds a class and takes arguments from
the outer scope when doing so:

=A0 =A0def awesome_mcjobify(state, options =3D {}, &action)
=A0 =A0 =A0subject_class =3D @subject_class

=A0 =A0 =A0job =3D Class.new do
=A0 =A0 =A0 =A0metaclass =3D class << self; self; end

=A0 =A0 =A0 =A0metaclass.send :define_method, :perform do |id|
=A0 =A0 =A0 =A0 =A0subject =3D subject_class.find(id)
=A0 =A0 =A0 =A0 =A0action subject
=A0 =A0 =A0 =A0end

=A0 =A0 =A0 =A0metaclass.send :define_method, :action, &action
=A0 =A0 =A0end

=A0 =A0 =A0subject_class.const_set "#{state.to_s.camelize}Job", job
=A0 =A0end

This is trying to do a lot of things at once and feels icky. It's buildin= g a
class that responds to a couple of methods, encapsulates a bit of state
(into the class itself, I guess?), then sticks that class namespaced
underneath another given class.

Refactor me? :) I know there's supposed to be define_singleton_method in
1.9, but using define_*method at all (not to mention send :define_method)
seems a lot messier than it could potentially be.

Any suggestions?

First thing that strikes me odd is that you define methods on the
singleton class, i.e. instance methods of the newly created class.
What do you need a class for then - especially since your new class
inherits Object (which means, it does not inherit particular instance
functionality)? From what I see you need something that responds to
"perform" by doing two things

1. find a subject via the subject class based on the id method parameter
2. invoke the block passed with the subject found

Wouldn't this do what you need?

Proxy =3D Struct.new :subject_class, :action do
def perform(id)
subject =3D subject_class.find id
action[subject]
end
end

def awesome_mcjobify(state, options =3D {}, &action)
Proxy[@subject_class, action]
end

It does not even need metaprogramming...

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 
T

Tony Arcieri

[Note: parts of this message were removed to make it a legal post.]

First thing that strikes me odd is that you define methods on the
singleton class, i.e. instance methods of the newly created class.
What do you need a class for then - especially since your new class
inherits Object (which means, it does not inherit particular instance
functionality)?


Because I'm building these classes for another API, namely Resque. I'm
creating a DSL for building Resque jobs, which the documentation defines
thusly:

Resque jobs are Ruby classes (or modules) which respond to
the perform method. Here's an example:

class Archive
@queue = :file_serve

def self.perform(repo_id, branch = 'master')
repo = Repository.find(repo_id)
repo.create_archive(branch)
end
end


So unfortunately I can't change the form of the classes I'm trying to
generate (with the possible exception of changing them into modules)
 
R

Robert Klemme

Because I'm building these classes for another API, namely Resque. I'm
creating a DSL for building Resque jobs, which the documentation defines
thusly:

Resque jobs are Ruby classes (or modules) which respond to
the perform method. Here's an example:

class Archive
=A0@queue =3D :file_serve

=A0def self.perform(repo_id, branch =3D 'master')
=A0 =A0repo =3D Repository.find(repo_id)
=A0 =A0repo.create_archive(branch)
=A0end
end


So unfortunately I can't change the form of the classes I'm trying to
generate (with the possible exception of changing them into modules)

Then it seems this would do

def awesome_mcjobify(state, options =3D {}, &action)
cl =3D Class.new do
def self.perform(id)
@action[@cl.find(id)]
end
end

cl.instance_variable_set "@action", action
cl.instance_variable_set "@cl", @subject_class

cl
end

However, it seems that https://github.com/defunkt/resque uses classes
in order to be able to distribute them to different machines. In that
case you would need to make sure the class has a name and is known at
all nodes. In that case it might be wise to also add a String
argument to the method call.

Btw, if my assumption is correct you might get away with simple
instances that implement important methods (#name for example) and are
assigned to constants which reflect their name.

Side note: code distribution is the Achilles heel of distributed job
systems. You either have to distribute code along with the request
which bloats queues and introduces security risks. Or you distribute
it via some other channel and then you can get typical versioning
issues...

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 

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,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top