2

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.

New Alexandria
  • 6,951
  • 4
  • 57
  • 77
  • And what's the question? – Boris Stitnicky Aug 27 '12 at 23:33
  • @BorisStitnicky "Can I use a method as a lambda?" – New Alexandria Aug 28 '12 at 14:22
  • Can I use pliers as a hammer? – Boris Stitnicky Aug 29 '12 at 10:06
  • You know, you want to determine completeness of something. An alert, whatever it means. So you need an object to whom you pass the alert, and the object tells you whether it is satisfied with its completeness, whatever it means. Obviously, you need to define a method on that object to do the classification. Or you can have that object be a lambda, and in that case #call method is the one you will call in order for it to perform what you taught it to perform. It's no more difficult that this. – Boris Stitnicky Aug 29 '12 at 10:11
  • It seems unreasonable to have a whole object to hold the classification/validation methods. It's a class of `helper` at worst. – New Alexandria Aug 29 '12 at 13:01
  • Sorry to break it to you, but in Ruby, everything is an object. You cannot have a half object to hold or do something, you can only have a whole object. A lambda, an unbound method, all is an object. And a class is an object tooo, and so is a class of helper, even at its worst, whatever it means. Your question is only whether you'll have one object or a collection of objects to hold your conditions, or criteria, or however you will call that which they need to hold. – Boris Stitnicky Aug 31 '12 at 10:55
  • @BorisStitnicky I don't understand what kind of objects I should instantiate for these scopes / scopes-that-do-enqueuing. Is it a `LoanAlertTrigger` object, that lives at the same namespace-level as `Loan` ? – New Alexandria Sep 04 '12 at 14:46

2 Answers2

2

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.

New Alexandria
  • 6,951
  • 4
  • 57
  • 77
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
  • I've upvoted your answer because you've put in a fair bit of thought here, used the domain language, and are thinking along the lines of making each the (poorly-named) `LifecycleScopeEnqueues` as their own methods. However, I do not think that `Alert` should have so much code that depends on domain knowledge form `Loan` as I think this goes again [SRP](http://en.wikipedia.org/wiki/Single_responsibility_principle). Instead I prefer to have these methods in a namespace of `Loan`. Encapsulation this way let me reflectively act on the group of them, ala `Loan.enqueue_lifecycle_reminders` – New Alexandria Sep 06 '12 at 16:16
  • 1
    You might actually be right in a way, but beware not to trap yourself in thinking that SRP in Ruby is identical with single namespace principle. – Boris Stitnicky Sep 07 '12 at 12:18
1

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
New Alexandria
  • 6,951
  • 4
  • 57
  • 77