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"
end
end
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:

File: lib/tasks/payroll.rake
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
end
end
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)
arg
end
Bubble methods allow you to add verbosity further making your language read like a natural language.
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
1
end
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 * arg
end
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)
end
end
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, :amount
end
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.

5 comments:

  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...

    ReplyDelete
  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

    ReplyDelete
  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 :(

    ReplyDelete
  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

    ReplyDelete
  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 : )

    ReplyDelete

Note: Only a member of this blog may post a comment.