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 == 1If we developed a Business Natural Language the requirements could be specified as:
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
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 doThe 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:
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
bubbles, Bubble methods are methods that take an argument and simply return this argument. They are equivalent to:
def the(arg)Bubble methods allow you to add verbosity further making your language read like a natural language.
arg
end
usage: bubbles :the, :is, :to
constants, Constant methods are methods that take no arguments and return a constant value. They are equivalent to:
def januaryConstant methods allow you to easily define methods that will always return the same value.
1
end
operations, Operation methods are methods that take an argument and append the operator of an operation. They are equivalent to:
def equal(arg)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.
"== #{arg}"
end
numerics, Numeric methods are methods that are the word representation of a number. They are equivalent to:
def two(arg)Numeric methods are generally used when chaining words together such as 'ten thousand'.
2 * arg
end
The implementation of Verbosity is quite simple.
File: app/models/verbosity.rb
module VerbosityThe 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.
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
File: app/models/bonus_calculation_context.rb
class BonusCalculationContextAt 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.
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
File: app/models/bonus.rb
class BonusAt 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]
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
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.
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...
ReplyDeleteHi Brent,
ReplyDeleteI'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
Jay, on the http://bnl.jayfields.com site.. the tracksys.com link causes the page(s) to take minutes to load :(
ReplyDeleteThanks for the heads up. I've removed the tracksy code. All you'll need to do is refresh.
ReplyDeleteCheers, Jay
The constants are set to 7 due to the fact that Jay was writing this example in July. see his note:
ReplyDelete[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 : )