Visitor Pattern with C# - Real World Tax Example

2023, Aug 19

The Visitor pattern is a behavioural design pattern, allowing new operations to be created or executed by the object, without modifying its structure. For this to happen a separate algorithm (the visitor) handles the operation. When it comes to real-world scenarios, this article shows a tax example to exemplify the pattern in C#.

Understanding the problem

How is the Income Tax calculated in your country? The New Zealand tax rate is calculated in tiers, by applying a percentage over a value range:

Personal income

  • 39% from $180,001
  • 33% from $70,001 to $180,000
  • 30%: $48,001 to $70,000
  • 17.5%: $14,001 to $48,000
  • 10.5%: $0 to $14,000

So if you earn $80,000, it means that 4 tiers will be applied to your income: 10.5%, 17.5%, 30% and 33%, as below:

Income tax rate Income Tax
Tier1 - Income up to $14,000.00, taxed at 10.5% 14,000.00 1,470.00
Tier2 - Income over $14,000.00 and up to $48,000.00, taxed at 17.5% 34,000.00 5,950.00
Tier3 - Income over $48,000.00 and up to $70,000.00, taxed at 30% 22,000.00 6,600.00
Tier4 - Income over $70,000.00 and up to $180,000.00, taxed at 33% 10,000.00 3,300.00
Tier5 - Remaining income over $180,000.00, taxed at 39% 0.00 0.00
Total 80,000.00 17,320.00

And you will pay $17,320.00

The example above was introduced in my previous article about the Decorator Pattern.

Solving using the pattern

Before the calculation starts, we need an object to hold the state, to get the results of Tax calculation. Let's call it IncomeTax.cs:

public class IncomeTax
{
    public decimal Income { get; set; }
    public decimal Tax { get; set; }
}

The basic idea of this pattern is that a "host" receives a "visitor", so we need to define a contract for both, the host that accepts the visitor1 and the visitor that does the work. Let's call both ITaxCalculator.cs and ITaxVisitor.cs:

public interface ITaxCalculator
{
    void CalculateTax(ITaxVisitor visitor);
}

public interface ITaxVisitor
{
    decimal ApplyTax(IncomeTax income);
}

We need to define an implementation for the host that will apply the Visitor Calculations. Let's call it TaxCalculator.cs:

public class TaxCalculator : ITaxCalculator
{
    private IncomeTax _incomeTax;

    public TaxCalculator(IncomeTax incomeTax)
    {
        _incomeTax = incomeTax;
    }

    public void CalculateTax(ITaxVisitor taxVisitor)
    {
        _incomeTax.Tax = taxVisitor.ApplyTax(_incomeTax);
    }

    public IncomeTax GetIncomeTax()
    {
        return _incomeTax;
    }
}

The CalculateTax method is responsible for applying visitor calculations.

From here we can start creating our visitors, for the different "tax tiers".

FirstTierTaxVisitor.cs

public class FirstTierTaxVisitor : ITaxVisitor
{
    public decimal ApplyTax(IncomeTax incomeTax)
    {
        if (incomeTax.Income > Constants.Tax.FIRST_TIER_INCOME) // $14,000.00
        {
            incomeTax.Tax = Constants.Tax.FIRST_TIER_INCOME * Constants.Tax.FIRST_TIER_RATE; // $14,000.00 * 10.5%
        }
        else
        {
            incomeTax.Tax = incomeTax.Income * Constants.Tax.FIRST_TIER_RATE; // Income provided * 10.5% 
        }

        return incomeTax.Tax;
    }
}

SecondTierTaxVisitor.cs

public class SecondTierTaxVisitor : ITaxVisitor
{
    public decimal ApplyTax(IncomeTax incomeTax)
    {
        if (incomeTax.Income > Constants.Tax.SECOND_TIER_INCOME) // $48,000.00
        {
            incomeTax.Tax += (Constants.Tax.SECOND_TIER_INCOME - Constants.Tax.FIRST_TIER_INCOME) * Constants.Tax.SECOND_TIER_RATE; // ($48,000.00 - $14,000.00) * 17.5%
        }
        else if (incomeTax.Income is > Constants.Tax.FIRST_TIER_INCOME and <= Constants.Tax.SECOND_TIER_INCOME) // $14,000.00 < x <= $48,000.00
        {
            incomeTax.Tax += (incomeTax.Income - Constants.Tax.FIRST_TIER_INCOME) * Constants.Tax.SECOND_TIER_RATE; // (Income - $14,000.00) * 17.5%
        }

        return incomeTax.Tax;
    }
}

ThirdTierTaxVisitor.cs

public class ThirdTierTaxVisitor : ITaxVisitor
{
    public decimal ApplyTax(IncomeTax incomeTax)
    {
        if (incomeTax.Income > Constants.Tax.THIRD_TIER_INCOME) // $70,000.00
        {
            incomeTax.Tax += (Constants.Tax.THIRD_TIER_INCOME - Constants.Tax.SECOND_TIER_INCOME) * Constants.Tax.THIRD_TIER_RATE; // ($70,000.00 - $48,000.00) * 30%
        }
        else if (incomeTax.Income is > Constants.Tax.SECOND_TIER_INCOME and <= Constants.Tax.THIRD_TIER_INCOME) // $48,000.00 < x <= $70,000.00
        {
            incomeTax.Tax += (incomeTax.Income - Constants.Tax.SECOND_TIER_INCOME) * Constants.Tax.THIRD_TIER_RATE; // (Income - $48,000.00) * 30%
        }

        return incomeTax.Tax;
    }
}

FourthTierTaxVisitor.cs

public class FourthTierTaxVisitor : ITaxVisitor
{
    public decimal ApplyTax(IncomeTax incomeTax)
    {
        if (incomeTax.Income > Constants.Tax.FOURTH_TIER_INCOME) // $180,000.00
        {
            incomeTax.Tax += (Constants.Tax.FOURTH_TIER_INCOME - Constants.Tax.THIRD_TIER_INCOME) * Constants.Tax.FOURTH_TIER_RATE; // ($180,000.00 - $70,000.00) * 33%
        }
        else if (incomeTax.Income is > Constants.Tax.THIRD_TIER_INCOME and <= Constants.Tax.FOURTH_TIER_INCOME) // $70,000.00 < x <= $180,000.00
        {
            incomeTax.Tax += (incomeTax.Income - Constants.Tax.THIRD_TIER_INCOME) * Constants.Tax.FOURTH_TIER_RATE; // (Income - $70,000.00) * 33%
        }

        return incomeTax.Tax;
    }
}

FifthTierTaxVisitor.cs

public class FifthTierTaxVisitor : ITaxVisitor
{
    public decimal ApplyTax(IncomeTax incomeTax)
    {
        if (incomeTax.Income > Constants.Tax.FOURTH_TIER_INCOME) // $180,000.00
        {
            incomeTax.Tax += (incomeTax.Income - Constants.Tax.FOURTH_TIER_INCOME) * Constants.Tax.FIFTH_TIER_RATE; // (Income - $180,000.00) * 39%
        }

        return incomeTax.Tax;
    }
}

So we end up with Five (5) different visitors, that are calculating the Income Tax.

Using the pattern

Considering that now all the visitors were defined, they now can visit the host TaxCalculator, and then get our tax calculation:

var incomeTax = new IncomeTax()
{
    Income = 80000m
};

var visitors = new List<ITaxVisitor>
{
    new FirstTierTaxVisitor(),
    new SecondTierTaxVisitor(),
    new ThirdTierTaxVisitor(),
    new FourthTierTaxVisitor(),
    new FifthTierTaxVisitor()
};

var taxCalculator = new TaxCalculator(incomeTax);

foreach (var visitor in visitors)
{
    taxCalculator.CalculateTax(visitor);
}

Console.WriteLine($"Income Tax: {taxCalculator.GetIncomeTax().Tax}");

In this example, you could see a practical (real-world) example of using visitors to solve a real-world problem. The example is available on PlayGoKids repository.