PTOM: November 2008: Visitor Design Pattern

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.

image

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.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Jason Meridth

Continuously learning software developer trying to not let best be the enemy of better
This entry was posted in .NET, Design Patterns, ruby. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

Comments are closed.