Saturday, October 07, 2006

BNL: The Problem - Update

The concept behind this 'chapter' is basically the same as the last time I put it on my blog; however, the example is largely new. As always, feedback welcome.

Business Natural Languages: Introduction
Business Natural Languages: The Problem
Business Natural Languages: The Solution
Business Natural Languages: DRY code, DAMP BNL
Business Natural Langauges: Moving to the web and multiple contexts
Business Natural Languages: Limitations
Business Natural Languages: Common Pieces

The Problem
To demonstrate the value in implementing a Business Natural Language we will start with an existing ruby application and then introduce a simple Business Natural Language. This approach should provide a good example of how to include a Business Natural Language in an application.

For the sample application imagine you've been contracted to replace a payroll calculation system for a consulting company. The consulting company chose to custom develop the software for calculating payroll because of the varying compensation packages. They have given you profiles of 2 employees and the business rules required to generate compensation.

Profiles:
John Jones
compensate $2500 for each deal closed in the past 30 days
compensate $500 for each active deal that closed more than 365 days ago
compensate 5 percent of gross profits if gross profits are greater than $1,000,000
compensate 3 percent of gross profits if gross profits are greater than $2,000,000
compensate 1 percent of gross profits if gross profits are greater than $3,000,000

Jackie Johnson
compensate $3000 for each deal closed in the past 30 days
compensate $800 for each active deal that closed more than 365 days ago
compensate 5 percent of gross profits if gross profits are greater than $1,000,000
compensate 5 percent of gross profits if gross profits are greater than $2,000,000

The application need only be run monthly; therefore, manual execution of the application is acceptable. The existing application is executed at the command line and the results are displayed to the screen. The payroll department records the application results in another system that is outside of the scope of your work. A preprocess is run to put monthly sales information in a known location in an agreed upon format, this is also outside the scope of your work. The only input files we will need for our sample application are jjones.yml and jjohnson.yml.

File: jjones.yml
employee: jjones
deals_this_month: 7
year_old_deals: 2
gross_profit: 1400000
File: jjohnson.yml
employee: jjohnson
deals_this_month: 12
year_old_deals: 1
gross_profit: 2200000
The SalesInfo class currently reads the sales data and exposes it via class method calls that are handled by method_missing.

File: sales_info.rb
require 'yaml'

class SalesInfo

@employee_sales_info = Dir['*.yml'].collect do |filename|
YAML::load(File.open(filename))
end

def self.method_missing(sym, *args)
hash = @employee_sales_info.find { |hash| hash.value?(sym.to_s) }
super if hash.nil?
EmployeeInfo.new(hash)
end

class EmployeeInfo
attr_reader :employee, :deals_this_month, :year_old_deals, :gross_profit
def initialize(hash)
hash.each_pair do |key, value|
instance_variable_set :"@#{key}", value
end
end
end

end
The ruby file that executes the current payroll process is very straightforward:

File: process_payroll.rb
require 'jjones'
require 'jjohnson'

puts "John Jones compensation: #{JJones.calculate}"
puts "Jackie Johnson compensation: #{JJohnson.calculate}"
Each employee ruby file is also fairly straightforward. They contain the logic to calculate compensation, expressed as ruby.

File: jjones.rb
require 'sales_info'

class JJones
def self.calculate
total = SalesInfo.jjones.deals_this_month * 2500
total += SalesInfo.jjones.year_old_deals * 500
profit = SalesInfo.jjones.gross_profit
total += profit * 0.05 if profit > 1000000
total += profit * 0.03 if profit > 2000000
total += profit * 0.01 if profit > 3000000
total
end
end
File: jjohnson.rb
require 'sales_info'

class JJohnson
def self.calculate
total = SalesInfo.jjohnson.deals_this_month * 3000
total += SalesInfo.jjohnson.year_old_deals * 800
profit = SalesInfo.jjohnson.gross_profit
total += profit * 0.05 if profit > 1000000
total += profit * 0.05 if profit > 2000000
total
end
end
The existing application produces the following output.

John Jones compensation: 88500.0
Jackie Johnson compensation: 256800.0

The code produces accurate results; however, the code has limited ability to easily be extended for new employees. Additionally, employees are required to re-negotiate their compensation structure every year. Following a compensation change a programmer must be involved to change the employees logic. The current process is clearly sub-optimal

2 comments:

  1. Anonymous5:42 PM

    Jay, this is looking good. I've been having lots of fun with BNLs lately.. and I'm actually giving a talk at the london rug (hope that goes well(!)) on my experiences.
    I'm looking forward to update to the other articles...

    But one thing that has stumped me is numbers. I blindly start on a number lib to handle writing number as words.. but this is proving a little harder than anticipated (without doing any string manipulation). How did you get around the problem? In the current rev of the articles you just defined 'thirty' and such as numerics....

    ReplyDelete
  2. Anonymous3:55 PM

    I naively thought I would handle numbers with words; however, it simply didn't work. To solve this problem we simply added a preprocess that will prepend an underscore to numbers and handle extracting the number in method missing.

    ReplyDelete

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