Crixx93
Crixx93

Reputation: 777

Decorating a method in Ruby

I have method that takes as a parameter an array of integer numbers, and I would like to use the decorator pattern to validate first if each of the numbers in the array is in a specified range.

I've seen decorator in ruby that extend class or include modules, but this seems a little excesive to me, is there a way to just decorate a method without relaying on a class or module ?

EDIT I have several methods that takes an array as parameter, and need to validate the range of the items. Instead of inline coding the validation in each of this method, I want a decorator for all of this methods, but I was wondering if decorator classes/modules are the only ones that exist in Ruby ?

class MyClass
 ..some code here ...
 def method(array)
   ..some code here...
 end
end

Upvotes: 4

Views: 3889

Answers (2)

Jörg W Mittag
Jörg W Mittag

Reputation: 369536

Here's a simple example of how to wrap methods with a validator:

class Foo
  def foo(a, b, c)
    p a, b, c
  end

  def bar(a, b)
    p a, b
  end

  validate(:foo, :bar) {|*args, &blk| args.reduce(:+) == 6 } 
end

The Module#validate method takes a list of method names, and a block with the validation logic for those methods.

foo = Foo.new

foo.foo(1, 2, 3)
# 1
# 2
# 3

foo.bar(2, 4)
# 2
# 4

foo.foo(2, 3, 4)
# [2, 3, 4] (Validator::ValidationError)

foo.bar(2, 3)
# [2, 3] (Validator::ValidationError)

As you can see, the validator rejected the argument list in the last two calls because they didn't pass the condition in the block.

This is the "magic" which makes it all happen, which isn't actually that magic. We generate a module which overrides the methods we want to validate with a version that raises an exception if the validation fails and then simply calls super. Then we prepend that module to the class/module that we are currently in, i.e. the one that the call to the validate method is in. And that's it, basically.

I chose to also be a good Ruby citizen and wrap the whole thing in a Refinement, so you need to say

using Validator

to actually activate it.

module Validator
  class ValidationError < ArgumentError; end

  refine Module do
    def validate(*methods, &validator)
      prepend(Module.new do
        methods.each do |method|
          define_method(method) do |*args, &blk|
            raise ValidationError, args.inspect unless yield *args
            super(*args, &blk)
          end
        end
      end)
    end
  end
end

Upvotes: 7

tadman
tadman

Reputation: 211670

Normally when you're talking about decorators you'd use that in the context of a class, not a single method.

Here's an example of a class you can decorate:

class ValidatorSet
  def initialize
    @validators = [ ]
  end

  def <<(validator)
    @validators << validator
  end

  def valid?(list)
    @validators.all? do |v|
      if v.respond_to?(:valid?)
        v.valid?(list)
      else
        list.all?(&v)
      end
    end
  end
end

class IsUnderTen
  def valid?(list)
    list.all? { |n| n < 10 }
  end
end

validator = ValidatorSet.new
validator << IsUnderTen.new
validator << lambda { |n| n > 0 }

validator.valid?([ 1, 2, 3, 4, 5 ])
# => true

validator.valid?([ -1 ])
# => false

validator.valid?([ 9, 10, 11, 12 ])
# => false

You see this sort of thing used in the ActiveRecord validation chain.

Upvotes: 3

Related Questions