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

Recent Entries