Discovering self-evaluating objects. (pt. 1 of 2)
by Rabbit
Notice: This is part one of a two-part post. Read the second post when you’re done with this one.
Have you ever written a program that sells products? Did you have to write the pricing rules for those products? Was it difficult? Time-consuming? Constantly changing? Screwing up your code and frustrating you to no end? Did people dip their grimy hands into your beautiful algorithm by asking you to make (gasp!) exceptions for this or that product? And don’t forget that special affiliate that everyone loves! He gets an extra percentage on top of everything else.
Stop. Breathe. Calm down. Everything will be okay. Object thinking to the rescue. Introducing the self-evaluating rule.
David West, author of Object Thinking, describes a self-evaluating rule thusly:
…think of a self-evaluating rule as an expression in the form of an ordered sequence of operators, variables, and constants. When the rule is asked to evaluate itself, each of its variables is asked to instantiate itself (become a known value), after which the rule resolves itself (applies the operators in the expression) to a final value — a Boolean true or false, for example.
Let’s take a stab at that, shall we? (You can paste the whole thing into IRB if you want to follow along.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class SelfEvaluatingRule attr_accessor :price, :tax def evaluate (@price * @tax) + @price end end rule = SelfEvaluatingRule.new rule.price = 100 rule.tax = 0.05 rule.evaluate # 105.0 |
Interesting. Okay, not really. It might resemble something you’ve written before, but instead of calling the evaluate method, you called price (or something similar).
Let’s think for a moment… What we have is a collection of variables, and a hard-coded formula that puts those variables to use. Right now it’s pretty crappy code. How can we make it better?
- Enter variable names and values dynamically
- Enter arbitrary rules
- Evaluate changes to our variables and rules without rewriting code
Those sound like solid goals. We’re going to take another stab at it, but before we do, I’d like to show you something that might be new to you…
First, fire up IRB.
Now, type the following, then hit enter:
1 | eval "1 + 1" |
What did you get? 2? Pretty cool, eh? This simple trick forms the basis of everything else we’re going to do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class SelfEvaluatingRule attr_accessor :price, :tax def evaluate(rule) eval(rule.gsub('price', @price.to_s).gsub('tax', @tax.to_s)) end end rule = SelfEvaluatingRule.new rule.price = 100 rule.tax = 0.05 rule.evaluate('(price * tax) + price') # 105.5 |
Not bad. We can now write dynamic rules that accept any values for price and tax, and use them in any formula we want, without having to rewrite our code. So let’s cross out number two on our list of enhancements.
Now let’s tackle those variables…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class SelfEvaluatingRule def initialize(rule) @rule = rule end def variables=(variables) @variables = variables end def evaluate @variables.each do |var, value| @rule.gsub!(var, value.to_s) end eval(@rule) rescue raise StandardError, 'Failed to evaluate' end end rule = SelfEvaluatingRule.new('(price * tax) + price') rule.variables = { 'price' => 100, 'tax' => 0.05 } rule.evaluate # 105 |
See what happened there? We extended our object by giving it the ability (more at responsibility) of maintaining an arbitrary collection of variables. We also specify the rule, though now we’re doing it during instantiation. Is this the best way to do it? Probably not; I can think of at least one more enhancement I’d make to the way rules are handled. What about you? Do you see room for improvement? I encourage you to challenge yourself and make it better!
With this last iteration we have completed all three of our stated goals. Not bad for just a few minutes work, eh?
So there you have it: the basics of a self-evaluating object. It sure feels like we’ve come a long way, but there’s more. Lots more. Currently we’re working with a single rule. However, chances are good that if you have need to write one rule, you’ve need to write many rules. And of course, we’ve been playing around in IRB. What if want to save rules and use them later? Perhaps attach those rules to other objects? There’s a world of possibilities out there!
I’ve written a solution in Rails that handles multiple rules, and even evaluates rules within rules. It’s a pretty neat solution. Though, after writing this post, I’m certain it can be cleaned up considerably.
I’m not sure what part two of this post will contain… whether I will simply post and explain the code I’ve already written, or try to refactor it first and then post. Time shall tell.
Comments
overall, Nice class. The example has some output mistakes, One returns an integer, one returns a float and one returns the wrong number :)
When we first used this though, we have to have spaces between the parenthesis and stuff like that, is that not the case with this? Is this how you improved it? It seems to work fine… (using the in-browser Ruby!) Good work.
[...] This is part two of a two-part post. Read the first post if you haven’t [...]