Definition
Visitor Design Pattern – “ Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates. ” dofactory.com
Another term you may hear when working with the Visitor Pattern, is Double Dispatch . How that applies to Visitor is that the accept method takes a visitor instance, which in turn has a visit method that takes the concrete instance (aka “this”). Inside the visitor visit method is where the core of the visitor pattern occurs. It is where you can extend the concrete class without actually editing the concrete class.
Open Closed Principle (OCP)
“…using the visitor pattern helps conformance with the open/closed principle.” (Wikipedia ) The Open/Closed Principle is that “a class is open for extension, closed for modification”. (Thank you Mr. Meyer ) I recently used the Visitor pattern inline with usage of a State Pattern. The Visitor would be able to determine, based on it’s algorithms, which was changing regularly by the needs of the end user, whether an object could move to the next state in the machine. Adhering to our theme, a coffee shop, I decided to generat a paycheck stub . My C# example is showing how to report the coffee shop employees earned wages, deductions via sick days and number of vacation days taken:
C# Code
Test Firsts (NUnit):
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
namespace visitor.pattern.specs
{
[TestFixture]
public class when_calculating_a_hourly_employees_wages
{
private HourlyEmployee jacob;
private EmployeePaycheckVisitor employeePaycheckVisitor;
[Test]
public void Should_be_able_to_view_how_many_hours_worked_and_pay_amount_received()
{
jacob = new HourlyEmployee(âBobâ, 35.00m);
jacob.addTimeWorked(120);
jacob.addSickDaysUsed(1);
jacob.addVacationDaysUsed(2);
employeePaycheckVisitor = new EmployeePaycheckVisitor();
Employee e = jacob;
e.accept(employeePaycheckVisitor);
Assert.That(employeePaycheckVisitor.EarnedWages, Is.EqualTo(3920.00m));
Assert.That(employeePaycheckVisitor.SickDayDeductions, Is.EqualTo(280.00m));
Assert.That(employeePaycheckVisitor.PaycheckSummaryLine, Is.EqualTo(âBob has worked 120 â +
âhours as an hourly employee and has earned $3,920.00 (Sick Days: 1/$280.00 Vacation Days: 2?));
}
[Test]
public void Should_be_able_to_view_how_many_hours_worked_and_pay_amount_received_for_another_hourly_employee()
{
jacob = new HourlyEmployee(âJacobâ, 50.45m);
jacob.addTimeWorked(12);
jacob.addSickDaysUsed(0);
jacob.addVacationDaysUsed(5);
employeePaycheckVisitor = new EmployeePaycheckVisitor();
Employee e = jacob;
e.accept(employeePaycheckVisitor);
Assert.That(employeePaycheckVisitor.EarnedWages, Is.EqualTo(605.40m));
Assert.That(employeePaycheckVisitor.SickDayDeductions, Is.EqualTo(0.00m));
Assert.That(employeePaycheckVisitor.PaycheckSummaryLine, Is.EqualTo(âJacob has worked 12 hours â +
âas an hourly employee and has earned $605.40 (Sick Days: 0/$0.00 Vacation Days: 5?));
}
}
[TestFixture]
public class when_calculating_a_fulltime_employees_wages
{
private FulltimeEmployee jacob;
private EmployeePaycheckVisitor employeePaycheckVisitor;
[Test]
public void Should_be_able_to_view_how_many_hours_worked_amount_deducted_for_sick_days_and_pay_amount_received()
{
jacob = new FulltimeEmployee(âBobâ, 35000);
jacob.addBusinessDaysWorked(120);
jacob.addSickDaysUsed(1);
jacob.addVacationDaysUsed(2);
employeePaycheckVisitor = new EmployeePaycheckVisitor();
Employee e = jacob;
e.accept(employeePaycheckVisitor);
Assert.That(employeePaycheckVisitor.EarnedWages, Is.EqualTo(16018.85m));
Assert.That(employeePaycheckVisitor.SickDayDeductions, Is.EqualTo(135m));
Assert.That(employeePaycheckVisitor.PaycheckSummaryLine, Is.EqualTo(âBob has worked 120 business â +
âdays as a fulltime employee and has earned $16,018.85 (Sick Days: 1/$135.00 Vacation Days: 2?));
}
[Test]
public void Should_be_able_to_view_how_many_hours_worked_amount_deducted_for_sick_days_and_pay_amount_received_for_another_hourly_employee()
{
jacob = new FulltimeEmployee(âJacobâ, 50000);
jacob.addBusinessDaysWorked(12);
jacob.addSickDaysUsed(0);
jacob.addVacationDaysUsed(5);
employeePaycheckVisitor = new EmployeePaycheckVisitor();
Employee e = jacob;
e.accept(employeePaycheckVisitor);
Assert.That(employeePaycheckVisitor.EarnedWages, Is.EqualTo(2307.69m));
Assert.That(employeePaycheckVisitor.SickDayDeductions, Is.EqualTo(0.00m));
Assert.That(employeePaycheckVisitor.PaycheckSummaryLine, Is.EqualTo(âJacob has worked 12 business â +
âdays as a fulltime employee and has earned $2,307.69 (Sick Days: 0/$0.00 Vacation Days: 5?));
}
}
}
I have two test fixtures, one for hourly employees and one for full-time employees. Hopefully, via my rough version of Behavior Driven Development (BDD), you can read my tests without any problems.
Implementation:
using System;
using System.Text;
namespace visitor.pattern
{
public abstract class Employee
{
protected int totalSickDaysUsed;
protected int totalVacationDaysUsed;
public abstract void accept(EmployeePaycheckVisitor visitor);
public void addSickDaysUsed(int numberOfDays)
{
totalSickDaysUsed += numberOfDays;
}
public void addVacationDaysUsed(int numberOfDays)
{
totalVacationDaysUsed += numberOfDays;
}
public int SickDaysUsed
{
get { return totalSickDaysUsed; }
}
public int VacationDaysUsed
{
get { return totalVacationDaysUsed; }
}
}
public class HourlyEmployee : Employee
{
private int totalTimeWorked;
public HourlyEmployee(string name, decimal hourlyRate)
{
totalTimeWorked = totalSickDaysUsed = totalVacationDaysUsed = 0;
Name = name;
HourlyRate = hourlyRate;
}
public override void accept(EmployeePaycheckVisitor visitor)
{
visitor.visit(this);
}
public string Name { get; set; }
public decimal HourlyRate { get; private set; }
public void addTimeWorked(int hoursWorked)
{
totalTimeWorked += hoursWorked;
}
public int TimeWorked
{
get { return totalTimeWorked; }
}
}
public class FulltimeEmployee : Employee
{
private int totalBusinessDaysWorked;
public FulltimeEmployee(string name, decimal annualSalary)
{
Name = name;
AnnualSalary = annualSalary;
totalBusinessDaysWorked = 0;
}
public string Name { get; private set; }
public decimal AnnualSalary { get; private set; }
public override void accept(EmployeePaycheckVisitor visitor)
{
visitor.visit(this);
}
public void addBusinessDaysWorked(int businessDaysWorked)
{
totalBusinessDaysWorked += businessDaysWorked;
}
public int BusinessDaysWorked
{
get { return totalBusinessDaysWorked; }
}
}
public class EmployeePaycheckVisitor
{
private const int FULL_WORK_DAY_HOURS_COUNT = 8;
private const int NUMBER_OF_BUSINESS_DAYS_IN_A_YEAR = 52*5;
public string PaycheckSummaryLine{get; private set;}
public decimal EarnedWages{ get; private set; }
public decimal SickDayDeductions { get; private set; }
public void visit(HourlyEmployee employee)
{
SickDayDeductions = (employee.SickDaysUsed * FULL_WORK_DAY_HOURS_COUNT) * employee.HourlyRate;
EarnedWages = (employee.TimeWorked * employee.HourlyRate) â (SickDayDeductions);
StringBuilder paycheckSummaryLine = new StringBuilder();
paycheckSummaryLine.Append(string.Format(â{0} has worked {1} hours as an hourly employeeâ, employee.Name, employee.TimeWorked));
paycheckSummaryLine.Append(string.Format(â and has earned {0:C}â, EarnedWages));
paycheckSummaryLine.Append(string.Format(â (Sick Days: {0}/{1:C}â, employee.SickDaysUsed, SickDayDeductions));
paycheckSummaryLine.Append(string.Format(â Vacation Days: {0}â, employee.VacationDaysUsed));
PaycheckSummaryLine = paycheckSummaryLine.ToString();
Console.WriteLine(PaycheckSummaryLine);
}
public void visit(FulltimeEmployee employee)
{
decimal dailyWages = employee.AnnualSalary/NUMBER_OF_BUSINESS_DAYS_IN_A_YEAR;
SickDayDeductions = Decimal.Round(employee.SickDaysUsed * dailyWages);
EarnedWages = Decimal.Round((employee.BusinessDaysWorked * dailyWages) â (SickDayDeductions), 2);
StringBuilder paycheckSummaryLine = new StringBuilder();
paycheckSummaryLine.Append(string.Format(â{0} has worked {1} business days as a fulltime employeeâ, employee.Name, employee.BusinessDaysWorked));
paycheckSummaryLine.Append(string.Format(â and has earned {0:C}â, EarnedWages));
paycheckSummaryLine.Append(string.Format(â (Sick Days: {0}/{1:C}â, employee.SickDaysUsed, SickDayDeductions));
paycheckSummaryLine.Append(string.Format(â Vacation Days: {0}â, employee.VacationDaysUsed));
PaycheckSummaryLine = paycheckSummaryLine.ToString();
Console.WriteLine(PaycheckSummaryLine);
}
}
}
Please remember, this is “blog friendly” code. I would definitely break the visitor class apart into a few classes (i.e., WageCalculator, SickPayDeductionCalculator, etc).
Ruby Code
Please understand that I’m a Ruby newbie and I’m petty sure there is a more “Ruby-ish” way to do this. I’ve recently been placed on a Ruby team and I’m making the transition from the static language mind-set to the dynamic language mind-set. I do understand with mix-ins you can dynamically add functionality to a class based on context, so visitor may not be necessary at all. I’m still grokking that concept. I still believe in the Open/Closed principle, but it may be moot with dynamic languages. It’s actually very exciting being a newbie developer again. It feeds my insatiable need to learn.
Test (Using RSpec):
require âruby.visitorâ
describe EmployeePaycheckVisitor do
before do
@visitor = EmployeePaycheckVisitor.new
end
it âshould generate hourly report for hourly employeeâ do
@jason = HourlyEmployee.new(âJasonâ, 55.00)
@jason.addHoursWorked(120)
@jason.addSickDaysUsed(1)
@jason.addVacationDaysUsed(2)
@jason.accept(@visitor)
@visitor.earnedWages.should == 6160.0
@visitor.sickDayDeductions.should == 440.0
@visitor.paycheckInfo.should == âHourly employee Jason worked 120 hours, â +
âearned $6160.00 with 1 sick day(s) ($440.00 deducted) and 2 vacation day(s)â
end
it âshould generate salaried report for salaried employeeâ do
@john = FulltimeEmployee.new(âJohnâ, 58000)
@john.addBusinessDaysWorked(120)
@john.addSickDaysUsed(3)
@john.addVacationDaysUsed(5)
@john.accept(@visitor)
@visitor.earnedWages.should == 26091
@visitor.sickDayDeductions.should == 669
@visitor.paycheckInfo.should == âFulltime employee John worked 120 business days, â +
âearned $26091.00 with 3 sick day(s) ($669.00 deducted) and 5 vacation day(s)â
end
end
require âruby.visitorâ
describe EmployeePaycheckVisitor do
before do
@visitor = EmployeePaycheckVisitor.new
end
it âshould generate hourly report for hourly employeeâ do
@jason = HourlyEmployee.new(âJasonâ, 55.00)
@jason.addHoursWorked(120)
@jason.addSickDaysUsed(1)
@jason.addVacationDaysUsed(2)
@jason.accept(@visitor)
@visitor.earnedWages.should == 6160.0
@visitor.sickDayDeductions.should == 440.0
@visitor.paycheckInfo.should == âHourly employee Jason worked 120 hours, â +
âearned $6160.00 with 1 sick day(s) ($440.00 deducted) and 2 vacation day(s)â
end
it âshould generate salaried report for salaried employeeâ do
@john = FulltimeEmployee.new(âJohnâ, 58000)
@john.addBusinessDaysWorked(120)
@john.addSickDaysUsed(3)
@john.addVacationDaysUsed(5)
@john.accept(@visitor)
@visitor.earnedWages.should == 26091
@visitor.sickDayDeductions.should == 669
@visitor.paycheckInfo.should == âFulltime employee John worked 120 business days, â +
âearned $26091.00 with 3 sick day(s) ($669.00 deducted) and 5 vacation day(s)â
end
end
Implementation:
class Employee
attr_reader :name
attr_accessor :sickDaysUsed
attr_accessor :vacationDaysUsed
def initialize(name)
@name = name
@sickDaysUsed = 0
@vacationDaysUsed = 0
end
def addSickDaysUsed(sickDaysUsed)
@sickDaysUsed += sickDaysUsed
end
def addVacationDaysUsed(vacationDaysUsed)
@vacationDaysUsed += vacationDaysUsed
end
end
class HourlyEmployee < Employee
attr_reader :hourlyRate
attr_reader :hoursWorked
def initialize(name, hourlyRate)
super(name)
@hourlyRate = hourlyRate
end
def addHoursWorked(hoursWorked)
@hoursWorked = hoursWorked
end
def accept(visitor)
visitor.HourlyVisit(self)
end
end
class FulltimeEmployee < Employee
attr_reader :salary
attr_accessor :businessDaysWorked
def initialize(name, salary)
super(name)
@salary = salary
end
def addBusinessDaysWorked(businessDaysWorked)
@businessDaysWorked = businessDaysWorked
end
def accept(visitor)
visitor.FulltimeVisit(self)
end
end
class EmployeePaycheckVisitor
attr_reader :earnedWages
attr_reader :sickDayDeductions
attr_reader :paycheckInfo
def HourlyVisit(hourlyEmployee)
name = hourlyEmployee.name
hours = hourlyEmployee.hoursWorked
@sickDayDeductions = hourlyEmployee.sickDaysUsed * 8 * hourlyEmployee.hourlyRate
@earnedWages = hours * hourlyEmployee.hourlyRate â @sickDayDeductions
@paycheckInfo = %Q{Hourly employee #{name} worked #{hours} hours, earned
$#{â%0.2fâ % @earnedWages} with #{hourlyEmployee.sickDaysUsed} sick day(s)
($#{â%0.2fâ % @sickDayDeductions} deducted) and
#{hourlyEmployee.vacationDaysUsed} vacation day(s)}
end
def FulltimeVisit(fulltimeEmployee)
name = fulltimeEmployee.name
businessDaysWorked = fulltimeEmployee.businessDaysWorked
dailyRate = (fulltimeEmployee.salary / 52) / 5
@sickDayDeductions = fulltimeEmployee.sickDaysUsed * dailyRate
@earnedWages = businessDaysWorked * dailyRate â @sickDayDeductions
@paycheckInfo = %Q{Fulltime employee #{name} worked #{businessDaysWorked}
business days, earned $#{â%0.2fâ % @earnedWages} with
#{fulltimeEmployee.sickDaysUsed} sick day(s) ($#{â%0.2fâ %
@sickDayDeductions} deducted) and #{fulltimeEmployee.vacationDaysUsed}
vacation day(s)}
end
end
What I Learned
I personally use the strategy pattern more than any other pattern. That is why I chose this pattern. So I could understand it better.
A good example I saw recently, using lambdas and all the new .NET features was Chad’s comment (pastie.org ) on Mo’s Recursive Command Post
Hope this helps someone understand the Visitor Design Pattern better.
Post Footer automatically generated by Add Post Footer Plugin for wordpress.