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.
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.