views:

1072

answers:

3

I have a command line Ruby app I'm developing and I want to allow a user of it to provide code that will run as a filter on part of the process. Basically, the application does this:

  1. read in some data
  2. If a filter is specified, use it to filter data
  3. process the data

I want the filtering process (step 2) to be as flexible as possible.

My thinking was that the user could provide a Ruby file that set a known constant to point to an object implementing an interface I define, e.g.:

# user's filter

class MyFilter
  def do_filter(array_to_filter)
    filtered_array = Array.new
    # do my filtering on array_to_filter
    filtered_array
end 

FILTER = MyFilter.new

My app's code would then do something like this:

array_that_might_get_filtered = get_my_array()
if (options.filter_file)
  require options.filter_file
  array_that_might_get_filtered = FILTER.do_filter(array_that_might_get_filtered)
end

While this would work, it feels cheesy and it seems like there should be a better way to do it. I also considered having the filter be in the form of adding a method of a known name to a known class, but that didn't seem quite right, either.

Is there a better idiom in Ruby for this?

A: 

Looks like a job for a Strategy Pattern, and since ruby has functions as first-class objects, you might pass the filter function to be memorized by the array, in order to invoke that custom filter function on demand.

VonC
+4  A: 

I'd just use a combination of the command line, and convention.

If a filter is specified, use it to filter data

I'm assuming you'd specify a filter on the command line? So you'd invoke the application like this?

ruby dataprocessor.rb custom_filter

If so, you could define an "api" wherein a class name would have to match what was passed in - pretty much exactly how you've described in your example.

To take it one step further though, you could have some logic which looked for the CustomFilter class using ruby's defined?, and if it was not found, go looking for custom_filter.rb (or any suitable variations) and attempt to load that file, then retry.

This gives you great extensibility, as you can write as many filter classes as you like, chuck them in their own .rb files, and put them anywhere that ruby can find them. You won't have to have an pre-defined constants either, the only constraints will be

  1. The class name must match (a variant of) the file name - This is convention in ruby so you're probably already doing it anyway.
  2. it must have some predefined method, such as your do_filter method

Incidentally, this is pretty similar to what rails does for requiring your models, and is why you can just use SomeModel without having to always do require app/models/some_model first :-)`

Orion Edwards
Much more practical than my general answer. +1
VonC
A: 
# user code
USER_FILTER = lambda { |value| value != 0xDEADBEEF }

# script code
load( user_code );
FILTER = ( const_defined?(:USER_FILTER) ? USER_FILTER : lambda { true } )

output_array = input_array.filter(&FILTER)
rampion