Monday, July 31, 2006

BNL 03 - The Solution

01 - Introduction
02 - The Problem
03 - The Solution
04 - DAMP BNLs
05 - Multiple Contexts

The Solution
A Business Natural Language should be used to specify expected behavior. The language should be comprised of descriptive and maintainable phrases. The structure of the language should be simple, yet verbose. Imagine each line of the specification as one complete sentence. Because a Business Natural Language resembles a complete language it is important to limit the scope of the problem being solved. An application's requirements can often be split to categories that specify similar functionality. Each category can be a candidate for a simple Business Specific Language. Lucky for us, our current application has a very limited scope: calculating bonuses. Therefore, our application will only require one Business Natural Language.

We've already seen how the requirements were originally specified:

John Jones
bonuses: \$10,000 if the company posts a profit of 1 million
\$20,000 if the company posts a profit of 2 million
\$30,000 if the company posts a profit of 3 million or more
5% of toothbrush profits
bonus pay month: January

And, how these requirements can be specified in ruby:
`if Time.now.month == 1  profit = Profit.find(:first)  bonus = profit.total > 100000000 ? 1000000 : 0  bonus += 1000000 if profit.total > 200000000  bonus += 1000000 if profit.total > 300000000  bonus += profit.toothbrush_in_cents * 0.05  File.open(bonus_log,'a') do |file|    file << "Jim Jones bonus: #{bonus.round} cents\n"   endend`
If we developed a Business Natural Language the requirements could be specified as:

apply bonus of ten thousand dollars if the total profit is greater than one million dollars and the current month is equal to january
apply bonus of ten thousand dollars if the total profit is greater than two million dollars and the current month is equal to january
apply bonus of ten thousand dollars if the total profit is greater than three million dollars and the current month is equal to january
apply bonus of the toothbrush profit times five percent if the current month is equal to january

There isn't any advantage to the above requirements as compared to the original requirements when judged as requirements alone; however, there would be a high amount of value if the requirements could also be executed as part of the application. This is exactly the power that a Business Natural Language provides you.

Lets change our existing application to work with our Business Natural Language. The first thing we can do is create a folder under lib called bonus_logic. Our new bonus_logic folder will be the folder where the subject matter experts will drop their files that determine how bonuses are calculated.

Because we previously executed the legacy application we know that John Jones bonus for last year was: \$92,525.00. Lets drop a file called john_jones.txt into our new bonus_logic folder and see what we need to do to get our application to execute and produce the same result. Remember, the john_jones.txt file contains our specifications:

File: lib/bonus_logic/john_jones.txt
apply bonus of ten thousand dollars if the total profit is greater than one million dollars and the current month is equal to january
apply bonus of ten thousand dollars if the total profit is greater than two million dollars and the current month is equal to january
apply bonus of ten thousand dollars if the total profit is greater than three million dollars and the current month is equal to january
apply bonus of the toothbrush profit times five percent if the current month is equal to january

The rake task will need to be updated to evaluate all files found in bonus_logic:

`namespace :payroll do  desc "run payroll"  task :run => :environment do    output_folder = "#{File.expand_path(File.dirname(__FILE__))}/../../tmp/"    output_file = "#{Time.now.year}-#{Time.now.month}-bonus.log"    output_path = output_folder + output_file    bonus_logic = "#{File.expand_path(File.dirname(__FILE__))}/../bonus_logic/"    File.open(output_file,'w') {}    Dir[bonus_logic+"*.txt"].each do |bonus_spec|      bonus = BonusCalculationContext.evaluate(bonus_spec)      if bonus.applies?        File.open(output_file,'a') { |file| file << "#{bonus.data}\n" }       end    end  endend`
The heart of any Business Natural Language are the context classes that understand execution behavior. However, before we jump right into the context class we should understand the verbosity module. The verbosity module is a module that lets you easily define some keywords of your Business Natural Language. For example, if you wanted to use 'january' in your language you could define the january method. Or, you could simply use the constants class method of the verbosity module. Verbosity has 4 methods:

bubbles, Bubble methods are methods that take an argument and simply return this argument. They are equivalent to:
`def the(arg)  argend`
usage: bubbles :the, :is, :to

constants, Constant methods are methods that take no arguments and return a constant value. They are equivalent to:
`def january  1end`
Constant methods allow you to easily define methods that will always return the same value.

operations, Operation methods are methods that take an argument and append the operator of an operation. They are equivalent to:
`def equal(arg)  "== #{arg}"end`
Operation methods are generally used to specify the operator and right value for an operation. A method to the left an operation method will generally specify the left value and execute an eval.

numerics, Numeric methods are methods that are the word representation of a number. They are equivalent to:
`def two(arg)  2 * argend`
Numeric methods are generally used when chaining words together such as 'ten thousand'.

The implementation of Verbosity is quite simple.

File: app/models/verbosity.rb
`module Verbosity    def bubbles(*methods)    methods.each do |method|      define_method(method) { |args| args }    end  end    def numerics(hash)    hash.each_pair do |name, multiplier|      define_method(name) { |args| multiplier * args }    end  end    def operations(hash)    hash.each_pair do |name, operator|      define_method(name) { |args| "#{operator} #{args}" }    end  end    def constants(hash)    hash.each_pair do |name, value|      define_method(name) { |args| value }    end  end  end`
The real value of Verbosity is hard to see without seeing its usage in BonusCalculationContext. BonusCalculationContext contains the behavior that transforms the Business Natural Langauge to executable code.

File: app/models/bonus_calculation_context.rb
`class BonusCalculationContext  extend Verbosity  bubbles :than, :is, :profit, :the, :to, :of, :bonus  numerics :thousand=>1000, :million=>1000000,            :one=>1, :two=>2, :three=>3, :five=>5, :ten=>10  operations :greater=>">", :equal=>"==", :times=>"*", :less=>"<"  constants :dollars=>100, :percent=>0.01, :january=>7, :february=>7, :march=>7  def initialize    @bonus_amount = 0  end    def self.evaluate(path_to_bnl)    context = BonusCalculationContext.new    context.extract_name(path_to_bnl)    specifications = File.readlines(path_to_bnl)    specifications.each { |spec| context.instance_eval(spec) }    context.resulting_bonus  end    def last_profit    @last_year_profit ||= Profit.find :first  end              def total(arg)    result = eval "#{last_profit.total} #{arg}"    return result.round if result.respond_to? :round    result  end      def month(arg)    ".month #{arg}"  end    def current(arg)    eval "Time.now#{arg}"  end    def toothbrush(arg)    eval("#{last_profit.toothbrush_in_cents} #{arg}").round  end    def drug(arg)    eval("#{last_profit.drug_in_cents} #{arg}").round  end    def apply(amount)    @bonus_amount += amount  end    def extract_name(path)    name_parts = File.basename(path).chomp!('.txt').split('_')    name_parts = name_parts.collect { |part| part.capitalize }    @employee_name = name_parts.join(" ")  end    def resulting_bonus    Bonus.new(@employee_name, @bonus_amount)  endend`
At only 60 lines, BonusCalculationContext isn't extremely complex. The common entry point to BonusCalculationContext is a call to the evaluate class method. The class method evaluate creates a new instance of BonusCalculationContext, extracts the employee name, and loops through each line of the argument file. As each line from the argument file is executed the methods of BonusCalculationContext are being executed and making changes to the bonus_amount instance variable. When BonusCalculationContext.evaluate is finished processing each line it will return the resulting_bonus generated by the BonusCalculationContext instance. The resulting_bonus is a simple class used to hold bonus information.

File: app/models/bonus.rb
`class Bonus  def initialize(who, amount)    @who = who    @amount = amount  end    def applies?    amount > 0  end    def data    "#{who} bonus: #{amount} cents"  end    private  attr_reader :who, :amountend`
At this point we return to the command line and execute 'rake payroll:run'. You should be delighted to see the tmp/2006-7-bonus.log does have John Jones bonus information. [Note: If you aren't seeing the value, ensure that the :january constant is set to the current month; otherwise the if statment will exclude John Jones' information]

Next, Lets convert Jackie Johnson and Joe Noone's business logic to our Business Natural Langauge.

File: lib/bonus_logic/jackie_johnson.txt
apply bonus of the total profit times one percent if the current month is equal to february
apply bonus of the drug profit times two percent if the current month is equal to february

File: lib/bonus_logic/joe_noone.txt
apply bonus of ten thousand dollars if the total profit is less than one million dollars and the current month is equal to march
apply bonus of ten thousand dollars if the total profit is equal to one million dollars and the current month is equal to march
apply bonus of the total profit times one percent if the total profit is greater than to one million dollars and the current month is equal to march

After dropping Jackie and Joe's specifications into the bonus_logic folder you can run 'rake payroll:run' again to see their results. A quick verification check shows that the results are accurate. We still have a lot of work to do, but we've proven our concept. Our application behavior is entirely dependent on the bonus specifications that can be altered by our subject matter experts.

In the upcoming chapters we will discuss moving our application to the web, putting the specifications in a database instead of using flat files, providing syntax checking and interactive recommendations, and many other concepts that will take us through developing a realistic Business Natural Language application.

1. Perhaps it would be a good idea to add a comment explaining why (for testing, I presume?) you're setting "january", "february", and "march" all to 7 (rather than 1, 2, and 3) in the "constants" line of the "app/models/bonus_calculation_context.rb" file...

2. Anonymous5:08 AM

Hi Brent,
I'm not sure why their value is seven. To be honest, it's probably a typo. That blog post is from a long time ago, you're probably better off reading the content on http://bnl.jayfields.com/

It's also getting dated at this point, but it's at least a year more mature than the 2006 versions.

Cheers, Jay

3. Anonymous12:36 PM

Jay, on the http://bnl.jayfields.com site.. the tracksys.com link causes the page(s) to take minutes to load :(

4. Anonymous12:50 PM

Thanks for the heads up. I've removed the tracksy code. All you'll need to do is refresh.

Cheers, Jay

5. The constants are set to 7 due to the fact that Jay was writing this example in July. see his note:

[Note: If you aren't seeing the value, ensure that the :january constant is set to the current month; otherwise the if statment will exclude John Jones' information]

This is an interesting read, Jay. I'll head over to the new site soon. This may have been a gentle intro to BNL so maybe it was for the best : )