Practical .NET

Simplify Code with the Visitor Pattern

You can't make a complicated problem simple. But, by leveraging the right tools in your language (and the Visitor pattern) you can ensure that your code doesn't get as complicated as your problem.

In an earlier column I discussed how inheritance can let you use simple code to solve a complicated problem. In that column I outlined how inheritance can simplify the code required to handle two products with different pricing schemes.

But then, like life in a Thomas Hardy novel, I pointed out that things can get worse. It turns out that your company, in addition to having two kinds of products (products and services) also has two types of customers: PremiumCustomers and TypicalCustomers. PremiumCustomers get a 10 percent discount on Products and TypicalCustomers aren't allowed to buy ServiceProducts at any price.

Leveraging Overloading
Fortunately, you can use method overloading to simplify your code. First, you create a CustomerBase class that includes Buy methods for each type of Product. One method accepts a Product object and the other method accepts a ServiceProduct object. You add whatever code to those methods that you think should be the default behavior for each ProductBase type (in this case both methods have the same default implementation):

Public MustInherit Class CustomerBase
  Public Overridable Function Buy(prod As Product, Quantity As Integer) As Decimal
    Return prod.GetPrice(Quantity)
  End Function
  Public Overridable Function Buy(prod As ServiceProduct, Quantity As Integer) As Decimal
    Return prod.GetPrice(Quantity)
  End Function
End Class

The right Buy method will be called at runtime based on the type of Product passed to the Buy method.

With that CustomerBase class in place, your next step is to create a class for each kind of Customer. Where necessary, you override the default CustomerBase methods with any special code. The PremiumCustomer, for example, needs to override the Buy method for Products to support the discount:

Public Class PremiumCustomer
    Inherits CustomerBase
  Public Overrides Function Buy(prod As Product, Quantity As Integer) As Decimal
    Return prod.GetPrice(Quantity) * 0.9
  End Function   
End Class

The TypicalCustomer, on the other hand, needs to override the Buy method for the ServiceProduct to prevent a sale being made:

Public Class TypicalCustomer
    Inherits CustomerBase
  Public Overrides Function Buy(prod As ServiceProduct) As Decimal
    Throw New Exception("Not an allowed purchase")
  End Function
End Class

Now, when a PremiumCustomer buys a Product, they get a 10 percent discount; when a TypicalCustomer attempts to buy a ServiceProduct, an exception is thrown.

The code that uses Customers and Products doesn't change much. This code retrieves a customer (who might be a TypicalCustomer or a PremiumCustomer) and a product (which might be a Product or a ServiceProduct). The code then lets the two objects work out the price:

Dim cust As CustomerBase
cust = GetCustomer("A123")
Dim prod As ProductBase
prod = GetProduct("RTFM")
Dim Amount As Decimal
Amount = cust.Buy(prod, 3)

It's certainly true that there is some additional complexity: You have more classes. However, the code in any particular class and (in the application as a whole) has stayed simple -- there are no If statements anywhere, for example.

There is one problem: As you add new Product classes, you need to do two additional things. First, you must add a new method to the CustomerBase class that accepts the new Product class and has a base implementation for calculating the price for that new Product. Second, you must add any overrides for that new method in any Customer class that needs a special implementation of the GetPrice method for that type of Product.

Simplifying Maintenance
That does suggest that if you have a lot of Product types compared to Customer types (or if you intend to add lots of Product types) you could end up with Customer classes with a lot of specialized Buy methods.

In the scenario where new Product types are common you can, instead, implement the Visitor pattern. First, you need to add a Discount property to the CustomerBase object: For TypicalCustomers the Discount property is set to one (no discount); For PremiumCustomers, it's set to 90 percent (a 10 percent discount). Now you only need one Buy method to work with any Product because the Customer has all the information the GetPrice method needs. With this design, inside the Buy method, you'll pass the Customer object to the GetPrice method along with the quantity purchased so that the Product class has access to the information on the Customer object:

Public MustInherit Class CustomerBase
  Public Property Discount As Decimal
  Public Overridable Function Buy(prod As ProductBase, Quantity As Integer) As Decimal
    Return prod.GetPrice(Me, Quantity)
  End Function
End Class

Now, in the ProductBase class there will be one GetPrice method for each Customer type (with a default implementation) and you'll count on method overloading to call the right version based on the Customer type. As it happens, the default implementation for the two methods is, again, identical:

Public MustInherit Class ProductBase
  Protected basePrice As Integer
  Public Overridable Function GetPrice(cust As TypicalCustomer, Quantity As Integer) As Decimal
    Return MyBase.basePrice * cust.Discount * Quantity
  End If

  Public Overridable Function GetPrice(cust As PremiumCustomer, Quantity As Integer) As Decimal
    Return MyBase.basePrice * cust.Discount * Quantity
  End If
End Class

Thanks to the new base versions of the GetPrice method, the Product class doesn't have to override either of the two default methods, so it achieves complete simplicity:

Public Class Product
    Inherits ProductBase

End Class

The ServiceProduct class, on the other hand, now has to override both default versions of the GetPrice method. For a PremiumCustomer, ServiceProduct adds on the CalloutCharge to the default calculation; for a TypicalCustomer, it throws an exception. The ServiceProduct class now looks like this:

Public Class ServiceProduct
    Inherits ProductBase
  Private calloutCharge As Decimal

  Public Overrides Function GetPrice(cust As PremiumCustomer, Quantity As Integer) As Decimal
    Return MyBase.getPrice + 50
  End Function

  Public Overrides Function GetPrice(cust As TypicalCustomer, Quantity As Integer) As Decimal
    Throw New Exception("Not Allowed")
  End Function
End Class

Overall, the level of complexity hasn't changed: While the Product class has gained a method, the ServiceProduct has lost one. Furthermore, all of the methods are very straightforward (and still no If statements!).

With this design, when you add a new Customer, you add a default GetPrice method for it in the ProductBase class and then add overrides for that method in any Product that needs it. A relatively few number of tests will let you check that your new methods are doing the right thing (and, by the way, at this point you've implemented something very much like the Visitor pattern).

But, regardless of which solution you take, as you add new Products or Customers you never touch a line of existing, working code. Instead, you add new functionality by adding new code: You've satisfied Vogel's First Rule of Programming: "Don't screw with working code."

Take the rest of the day off. You've earned it.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube