Conditional Execution Between Rails and Merb
Just another snippet of code I hope will make my life easier. Since I have a Merb app piggybacking my Rails one, there are times when I need framework-specific code executed. Naturally, it's more often best to bust out that logic into its own place and conditionally include it, but... well... there are cases when you can't (or it's not worth it).
For example, I'm sending out email notifications directly from my model. Someone in IRC commented that in doing so I've made my model too "smart", but since the email needs to go out any time I change the model's state, I think it's appropriate.
One way of doing it might be:
class Submission < ActiveRecord::Base
#...
private
def send_pending_notification
if defined?(Merb)
# Use Merb mailer here
else
# Use Rails mailer here
end
end
end
But in the unlikely event that the Merb constant disappears or is changed, I have to go through and move all these conditionals around. It might sound far-fetched, but that very thing is happening in Rails right now. Constants like RAILS_ROOT are being changed to Rails.root. Moreover, if I want to load this model into a completely different codebase, I now have to worry about the Rails logic being executed when I might not actually have the Rails environment loaded.
So, I created a helper object and method:
class Orchestration
def merb(&block)
if defined?(Merb)
block.call if block_given?
end
end
def rails(&block)
if defined?(RAILS_ROOT)
block.call if block_given?
end
end
end
def orchestrate
block_given? ? yield(Orchestration.new) : Orchestration.new
end
It needs a little prettying up, but it effectively allows me to do this anywhere in code:
class Submission < ActiveRecord::Base
#...
private
def send_pending_notification
orchestrate do |is|
is.merb do
# Use Merb mailer here
end
is.rails do
# Use Rails mailer here
end
end
end
end
Yes, it's slower, but I think it makes conditional execution a little nicer. Now I can change the means of determining the framework in one place. Additionally, if I loaded this model outside Rails and Merb, I would know that send_pending_notification would never run any meaningful code.
Another example - in my Submission model, I'm referring to a Country model, which in turn loads a plugin called has_permalink. Since my Merb app only ever creates Submissions, and never edits Country, I don't necessarily need has_permalink loaded into the app.
One way of doing it:
class Country < ActiveRecord::Base
unless defined?(Merb)
has_permalink
end
end
The nicer way:
class Country < ActiveRecord::Base
orchestrate.rails { has_permalink }
end
And before anyone gives me any crap for style, it behaves almost exactly like Rails' respond_to, so shutcha face!
Update: I've changed Orchestration to use registering instead.
class Orchestration
@@registered_types = {}
def self.register(conditional_type, conditional_check)
@@registered_types[conditional_type.to_sym] = conditional_check
end
def method_missing(meth, *args, &block)
if @@registered_types[meth]
if @@registered_types[meth].call(*args)
block.call if block_given?
end
end
end
end
def orchestrate
block_given? ? yield(Orchestration.new) : Orchestration.new
end
Orchestration.register(:rails, Proc.new { defined?(RAILS_ROOT) })
Orchestration.register(:merb, Proc.new { defined?(Merb)})
Orchestration.register(:opposite, Proc.new {|shouldit| !shouldit })
orchestrate.opposite(true) { puts "Hi!"}
# no output
orchestrate.opposite(false) { puts "Hi!"}
# puts "Hi!"
This will allow other types to register themselves without having to modify the actual class.
Comments