To illustrate this, consider a simple calculator application. You could tackle such an application any number of ways, but we can come up with an interesting solution using class methods.

For starters, we need our base CalculatorBase class, which will provide the plumbing for dealing with input, handling if the input is numeric versus operations, and overall flow. I am not interested in these details for this post, so I will leave them as a homework problem for you to play with. Don't worry, it's fun!

First, your base calculator needs to expose a way to define operations:

class CalculatorBase

def self.operation(op, arity, &block)

define_method(op.to_sym) { |*args|

unless args.size == arity

raise "Wrong number of arguments"

end

block.call *args

}

end

end

From this, you can declaratively define what operations your calculator may support:

class Calculator < CalculatorBase

operation(:+, 2) { |x, y| x + y }

operation(:-, 2) { |x, y| x - y }

operation(:/, 2) { |x, y| x / y }

operation(:*, 2) { |x, y| x * y }

operation(:sin, 1) { |x| Math.sin x }

operation(:cos, 1) { |x| Math.cos x }

operation(:tan, 1) { |x| Math.tan x }

end

Now, you can invoke operations with ease:

calc = Calculator.new

calc.+ 2, 3

calc.sin 3.14159

However, I feel we can do better than this. There is a lot of repetition going on with the arity. To make our calculator implementation more domain specific, let's allow binary and unary operators to be defined easier:

class CalculatorBase

def self.operation(op, arity, &block)

define_method(op.to_sym) { |*args|

unless args.size == arity

raise "Wrong number of arguments"

end

block.call *args

}

end

def self.binary(op, &block)

operation op, 2, &block

end

def self.unary(op, &block)

operation op, 1, &block

end

end

With these new methods, we can make our operation declarations a bit easier:

class Calculator < CalculatorBase

binary(:+) { |x, y| x + y }

binary(:-) { |x, y| x - y }

binary(:/) { |x, y| x / y }

binary(:*) { |x, y| x * y }

unary(:sin) { |x| Math.sin x }

unary(:cos) { |x| Math.cos x }

unary(:tan) { |x| Math.tan x }

end

This isn't a particularly difficult approach in Ruby, but I really like the results whenever I am able to employ it. It can remove a lot of repetitive method declaration, and make your main class very clearly declare the behaviors it employs.

Of course, with great power comes great responsibility. It is easy to obfuscate the behavior you are creating with this pattern. Used appropriately, you can design an API and then speak the language of your domain in a much easier to comprehend fashion.

## No comments:

Post a Comment