Question

I have an interface the defines a group of conditions. it is one of several such interfaces that will live with other models.

These conditions will be called by a message queue handler to determine completeness of an alert. All the alert calls will be the same, and so I seek to DRY up the enqueue calls a bit, by abstracting the the conditions into their own methods (i question if methods is the right technique). I think that by doing this I will be able to test each of these conditions.

class Loan
  module AlertTriggers
    def self.included(base)
      base.extend           LifecycleScopeEnqueues

      # this isn't right
      Loan::AlertTriggers::LifecycleScopeEnqueues.instance_method.each do |cond|

        class << self
          def self.cond
            ::AlertHandler.enqueue_alerts(
              {:trigger => Loan.new}, 
              cond
            )
          end
        end

      end
    end
  end

  module LifecycleScopeEnqueues
    def student_awaiting_cosigner 
        lambda { |interval, send_limit, excluding|
          excluding ||= ''
          Loan.awaiting_cosigner.
            where('loans.id not in (?)', excluding.map(&:id) ).
            joins(:petitions).
            where('petitions.updated_at > ?', interval.days.ago).
            where('petitions.updated_at <= ?', send_limit.days.ago) 
        }
    end
  end

I've considered alternatives, where each of these methods act like a scope. Down that road, I'm not sure how to have AlertHandler be the source of interval, send_limit, and excluding, which it passes to the block/proc when calling it.


It was suggested to me (offline) that a scope is a lambda, and so may be a more-suitable solution - as per @BorisStitnicky inference that pliers can be used as a hammer, but should not. I'm open to answers along this line as well.

Was it helpful?

Solution

You know, this might not be the answer you seek, but I'll try my best. It is better to answer like this than to drop comments. In your code, you are doing a few quite unusual things. Firstly, you are defining a module inside a class. I've never done or seen this before, so much that I hit irb to try it out. Secondly, you are defining a method that returns a lambda. That reminds me a lot of what I've benn doing when I was just learning Ruby. Lambdas have quite specific applications and should be generally avoided in this form when possible. If you want to use lambdas like this, at least assign them to a variable, or better, a constant:

STUDENT_AWAITING_COSIGNER = lambda { |interval, send_limit, excluding|
  # do your SQL magic
}

I'm having troubles understanding your vocabulary: In particular, I'm not sure whether the thing that you are calling "scope" is a Ruby scope, or some other kind of scope.

But personally, I don't think you should use lambdas at all. I would dare to say, that yor code needs much more than just DRY up a bit. I think you shouldn't set up subnamespaces in a class. Why don't you use eg. an instance variable? Also, public class methods are just a cherry on the pie. First solve the problem without them, and then you can decide to add them to make your interface more convenient. All in all, I would simply do something along these lines:

class Loan
  attr_reader :alerts

  def initialize( whatever_options )
    @alerts = Array( whatever_options[ :alerts ] )
  end

  def check_alerts
    @alerts.each &:test
  end
end

# Then I would set up an alert class:
class Alert
  def test( interval, send_limit, excluding = '' )
    Loan.awaiting_cosigner.
      where('loans.id not in (?)', excluding.map(&:id) ).
      joins(:petitions).
      where('petitions.updated_at > ?', interval.days.ago).
      where('petitions.updated_at <= ?', send_limit.days.ago) 
  end
end
# I used your prescription statically, if you have different kind
# of alerts, you would have to make the class sufficiently flexible
# to handle them all.

# and then I would eg. supply alerts to a Loan upon instantiation
# (this can be also done later, if you make it so)
my_little_loan = Loan.new( alerts: Alert.new )
# and when the time comes to check whether the alerts alert or not:
my_little_loan.check_alerts

Obviously, this is just an outline of how I humbly think that these kinds of problems should be solved in Ruby with simplicity. You need to put in your own effort to make it work for you in your particular complicated case.

OTHER TIPS

One way to handle this is to use a namespace (within the module) that is expected by (or revealed to) the other model / part of the domain.

In this case AlertHandler needs not be passed a block. Instead it can know about the existence of the namespace LifecycleScopeEnqueues (which instead maybe read more actionably as Lifecycle_EnqueuingScopes). Thus, whatever is happening inside of AlertHandler.enqueue_alerts:

class AlertHandler
  def enqueue_alerts(options, condition)
    trigger = options[:trigger]
    handler = options[:trigger_handler].capitalize

    interval, send_limit, excluding = handler_metrics(handler, condition)

    range = "#{trigger.class.name}".constantize.send(condition, [interval, send_limit, excluding])

    # do other things
  end
end

Alerts for all of these scopes can still be 'enqueued' via one reflective method (add-mixed with the code in the question)

class Loan
  module AlertTriggers
    def self.included(base)
      base.extend     ClassMethods
    end

    module  ClassMethods
      def enqueue_lifecycle_reminders
        Loan::AlertTriggers::LifecycleScopeEnqueues.instance_method.each do |cond|
          ::AlertHandler.enqueue_alerts(
              {:trigger => Loan.new}, 
              cond
          )
        end
      end
    end
  end
end

This approach also allows for testing of the scopes/conditions in Loan::AlertTriggers::LifecycleScopeEnqueues via:

  • per-method
  • duck-typing
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top