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)” endend
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)” endend
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 endendclass 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) endendclass 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) endendclass 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)} endend
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.
