This article shows a concrete example of the true advantages of using design patterns when implementing software.
Introduction
Do you hear a lot about software design, software architecture, and
design patterns? But you hardly see any striking advantage to use them
in your projects? Or you think of them like they are unusable academic
nonsense? Or you simply don't have the time to cope with them? What a
pity! And that is for many reasons.
This article will show you a concrete example of why you definitely
should have a closer look at design patterns again and again. Consider
that the complexity of software is steadily increasing. So all of us
need methods for keeping our code easy readable, highly maintainable,
and easily extensible without having to give up the flexibility of
modern programming languages. Design patterns are the very basics which
provide us exactly this. Unfortunately, most articles describe design
patterns without really pointing out their advantages.
This article is intended to change this. It will show you on a
concrete example how you can keep your projects easy extensible and
maintainable by using a single design pattern. After reading it, you
will know, what the visitor pattern is intended for, where and why you
should use it, and what advantages it gives to your projects. In short –
you will know what the true meaning of such keywords like
maintainability, extensibility, and reusability of code is, and how you
can easily add these valuable issues to your own projects.
Visitor Pattern
This pattern is a robust and highly scalable way for implementing
case distinction in your code. Let us construct some very simple example
here. Let us assume that we need to implement a simple insurance
software. We have an insurance policy which is related to some person.
The policy fee is dependent on the gender of a person. Let us assume
that women have an initial fee discount of 20%.
Then we need some business logic for our application. So we might
need a fee calculator and some component for printing bills for our
policies.
At this place, you may object that we do not need extra components
for such a bill printer and a fee calculator. Indeed, we could implement
methods, like
CalculateFee():void
and
PrintBill():string
in the
Policy
class. But, always keep in mind that such services often need to be
adjusted to the current needs of customers. Take the bill printer for
instance. The insurer might say someday, that there is no need to print
separate bills for different policies possessed by the same person. So
if some person has bought more than one insurance policy, the bill
should contain all his policies. So if the functionality for printing
bills is located in the
Policy
class, it might be a mess to implement the new customer’s requirement, because in this case, each
Policy
object has to know each other
Policy
object in order to find other policies which are referred to the same
Person
.
But we do not want such smart policies. We want dumb policies and smart
services on them. Therefore, separate your business logic from your
data thoroughly.
How you can implement it
OK. Now, let’s have a closer look at how we could implement our
application. The entities are coded quickly. We only need an abstract
Person
with properties for the
Name
and the
Address
, a
Man
and a
Woman
as concrete persons, and a
Policy
with the property for the
Person
to which the policy is attached to. For simplicity reasons, let us assume, that both the
Name
as well as the
Address
of the
Person
are simple strings. Our entities are shown in the following picture:
For our business logic, we need the
FeeCalculator
and the
BillPrinter
components (see the following picture for more details):
Now, both components will need to do a case distinction in order to
determine who the corresponding person to the given policy is. Relying
on this determination, they do their job in a slightly different
fashion. So, the
FeeCalculator
will give a 20% discount to the initial fee only if the person in the given policy is a
Woman
. Classically, such case distinctions are coded using an
if…then…else
statement. It then looks like this (please consider, that all code
snippets in the article are pure pseudocode, but you also can download a
source project with the working code and experiment with it as much as
you want):
Hide Copy Code
public double CalculateFee(Policy p)
{
if(p.Person.GetType().Equals(typeof(Man)))
return p.InitialFee;
else if(p.Person.GetType().Equals(typeof(Woman)))
return p.InitialFee * 0.8;
else
return 0.0;
}
But also, the
BillPrinter
needs to do such a case
distinction for creating an appropriate letterhead with ‘Dear Ms.’ or
‘Dear Mr.’. It could look like this:
Hide Copy Code
public void PrintBill(Policy p)
{
String bill = "";
bill += p.Person.Address.ToString();
if(p.Person.GetType().Equals(typeof(Man)))
bill += "Dear Mr " + p.Person.Name;
else if(p.Person.GetType().Equals(typeof(Woman)))
bill += "Dear Ms " + p.Person.Name;
bill += "The fee for your police is: ";
bill += FeeCalculator.CalculateFee(p).ToString();
}
As the next step, you get a new requirement to implement a component
which shows us some statistics, like how many policies have been sold to
male or female persons. The code here is pretty similar to the code in
the previous two components:
Hide Copy Code
public int PoliciesSoldOnFemale(Policy[] policies)
{
int count = 0;
foreach(Policy current in policies)
if(current.Person.GetType().Equals(typeof(Woman)))
count++;
return count;
}
Well, this implementation looks pretty straightforward and pretty
similar by now. But what about the maintenance and extensibility of such
code? Let us check this by adding some new requirements. The
calculation of the fee should now offer a 50% discount to under-aged
persons regardless of the gender.
Therefore, we need to modify our entities first. This is shown in the following picture:
Now, we need to adjust our
CalculateFee
method by adding more
else…if
branches. But as you might already have noticed, our other components
need adjustments as well. So, our printer refuses to print bills for
under-aged persons. Our statistics also skip under-aged persons by
calculating the numbers of sold policies on males and females. So our
business logic is out of date. Even worse – it is producing wrong
results (as with the statistics example - as it only counts women and
doesn't count girls in its
PoliciesSoldOnFemale
method). It
gets even worse if we have to do boxed case distinctions. Imagine that
our fee calculation should also regard the age of the person (for
example, the fee for a vehicle insurance is higher for younger persons
because of the lack in driving experience and thus higher risk of an
accident, while the fee for health insurance should be climbing with a
higher age). In this case, we might add some age categories as an
enumeration to the
Person
and make a case distinction with
switch…case
. But this makes our application even more dependent on the structure of our entities.
Now, imagine that your application has hundreds of business logic
classes and you programmed only 10 of them by yourself. So you do not
know what classes might need an adjustment after modifying your entity
classes. The biggest problem is that the compiler still accepts the new
code as a valid application. You only have a chance to notice such
errors at runtime. In other words, you have to do extended and detailed
testing every time you modify your entities before you can say your
application is working well.
How you should implement it
As you have seen, making case distinction by
if…then…else
or by
switch…case
is error prone. How can we avoid this? Well, surprise, surprise, by
using a Visitor pattern as it is described by GOF (Gang Of Four - Design
Patterns). The main idea behind this pattern is that, in our case, our
business logic has to work with an abstract person and often needs to
distinguish which concrete person it is. But the concrete person object
alone knows if it is a man or a woman object. So our printer service can
ask the person object: “Hey, look, I provide you method X for printing a
bill if you are a man and method Y if you are a woman. So tell me who
you are and which of these methods I have to use to print a bill for
you”. Here is the code for the new bill printing service which can print
bills for men and women. First, the adjusted entities:
Hide Shrink Copy Code
public abstract class Person
{
public string Name;
public string Address;
public abstract void AcceptPersonVisitor(IPersonVisitor visitor);
}
public class Man : Person
{
public override void AcceptPersonVisitor(IPersonVisitor visitor)
{
visitor.HandleMan(this);
}
}
public class Woman : Person
{
public override void AcceptPersonVisitor(IPersonVisitor visitor)
{
visitor.HandleWoman(this);
}
}
public interface IPersonVisitor
{
void HandleMan(Man visitee);
void HandleWoman(Woman visitee);
}
And finally, the new printing service itself:
Hide Shrink Copy Code
public class BillPrinter
{
public void PrintBill(Policy p)
{
BillPrinterVisitor visitor = new BillPrinterVisitor(p);
p.Person.AcceptPersonVisitor(visitor);
}
}
public class BillPrinterVisitor: IPersonVisitor
{
private Policy p;
private string bill = "";
public BillPrinterVisitor (Policy p){
this.p = p;
bill += p.Person.Address.ToString();
}
public void HandleMan(Man visitee)
{
this.bill += "Dear Mr. " + visitee.Name;
this.PrintRest();
}
public void HandleWoman(Woman visitee)
{
this.bill += "Dear Ms. " + visitee.Name;
this.PrintRest();
}
private void PrintRest()
{
bill += "The fee for your police is: ";
bill += FeeCalculator.CalculateFee(this.p).ToString();
}
}
As we can see, the
BillPrinterVisitor
provides a method
HandleMan()
for printing a bill if the person is a man, and a method
HandleWoman()
for printing a bill for a woman. And in the method
PrintBill()
, the person object is being asked: “Tell me who you are and execute the right method for you” by calling the
AcceptPersonVisitor()
method. Now, look at the implementation of this method in the
Man
and
Woman
classes. You will notice, that if
p.Person
is a woman, the
HandleWoman()
method of the visitor is called. Otherwise, if
p.Person
is a man, the
HandleMan()
method is called.
The true advantage of the visitor patter is the following. Let us
again modify our entities as it is shown in the last picture (by
distinguishing between under-aged and full-aged persons). Now, besides
the abstract
Person
class, we have the following new classes:
Hide Copy Code
public abstract class Fullage : Person{}
public abstract class Underage : Person{}
public class Boy : Underage
{
public override void AcceptPersonVisitor(IPersonVisitor visitor)
{
visitor.HandleBoy(this);
}
}
public class Girl : Underage
{
public override void AcceptPersonVisitor(IPersonVisitor visitor)
{
visitor.HandleGirl(this);
}
}
Oops, but we do not have methods
HandleGirl()
and
HandleBoy()
in our visitor interface. So you can already see at compile time, that
there is something wrong here. The project wouldn’t even compile if we
didn’t add these methods to our visitor. Let us do it. Oops, the project
still can not be compiled. That is because all implementing visitors,
like
BillPrinterVisitor
in our case, do not implement the new added methods. Now, imagine, that all your business logic components, such as
FeeCalculator
and
Statistics
in our case, are implemented using visitors. Now, we have a complete
overview of which business logic classes might be delivering wrong
results and thus need to be adjusted. The compiler says it to us. Isn't
it just so damn clever?
Now, we can go ahead and implement a visitor for age categories.
Therefore, let us come back again to the requirement to implement a fee
calculator for a car insurance policy. As you can recall, the
requirement was, that the fee for young persons between 18 and 25 should
be higher, due to lack in driving experience. To do just this, we first
must add
AgeCategory
to the
Person
class. Consider, that concrete classes do not need any modification, because of the inheritance.
Hide Copy Code
public abstract class Person
{
public string Name;
public string Address;
public AgeCategory Age;
public abstract void AcceptPersonVisitor(IPersonVisitor visitor);
}
Now, we must add some age categories:
Hide Shrink Copy Code
public abstract class AgeCategory
{
public abstract void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor);
}
public class Child : AgeCategory
{
public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
visitor.HandleChild(this);
}
}
public class YoungAged : AgeCategory
{
public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
visitor.HandleYoungAged(this);
}
}
public class MatureAged : AgeCategory
{
public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
visitor.HandleMatureAged(this);
}
}
public class ElderAged : AgeCategory
{
public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
visitor.HandleElderAged(this);
}
}
Now, we add the visitor interface for age categories:
Hide Copy Code
public interface IAgeCategoriesVisitor
{
void HandleChild(Child visitee);
void HandleYoungAged(YoungAged visitee);
void HandleMatureAged(MatureAged visitee);
void HandleElderAged(ElderAged visitee);
}
Next, we define an age categories visitor for a car insurance policy:
Hide Copy Code
public class CarInsuranceAgeFeeDiscountVisitor : IAgeCategoriesVisitor {
private double discount = 0.0;
public double GetDiscount() { return this.discount; }
public void HandleChild(Child visitee){
throw new ChildrenOughtNotDriveCarsException();
}
public void HandleYoungAged(YoungAged visitee){
this.discount = -.5;
}
public void HandleMatureAged(MatureAged visitee){
this.discount = .2;
}
public void HandleElderAged(ElderAged visitee){
this.discount = 0.0;
}
}
Then, we define a visitor for the
Person
to which the policy is referred.
Hide Copy Code
public class PersonDependantFeeCalculator : IPersonVisitor
{
public double InitialFee;
...
public void HandleWoman(Woman visitee)
{
CarInsuranceAgeFeeDiscountVisitor visitor = new
CarInsuranceAgeFeeDiscountVisitor();
visitee.Age.AcceptAgeCategoryVisitor(visitor);
this.InitialFee = this.InitialFee * (0.8 – visitor.GetDiscount());
}
}
And last, we define the fee calculator, which calculates the fee
dependant on the gender and the age of the person determined by using
visitors.
Hide Copy Code
public class FeeCalculator : IPersonVisitor{
public double CalculateFee(Policy p)
{
PersonDependantFeeCalculator visitor =
new PersonDependantFeeCalculator();
p.Person.AcceptPersonVisitor(visitor);
return visitor.InitialFee;
}
}
As you can see, you don’t have any boxed case distinctions anymore. In my experience, I have seen up to six times boxed
if...then...else
branches with hundreds of lines of code per single method. Dude, I
programmed this way myself some time ago. And I claim that even those
people who programmed such code cannot completely overview the control
flow in this code any more in six months time.
Conclusion
So my suggestion is: use Visitors whenever you can. Using the Visitor pattern, you have:
So, again,
use the Visitor pattern whenever you can. Use
if…then…else
only if you are 100% sure that the same case distinction won’t appear somewhere else in the code (i.e., you might use
switch
if you are sure of that, i.e., F5-key should only be processed in the
current dialogue, or the behaviour on pressing the F1-key is not always
the same and differs from dialogue to dialogue).
Try it out and you will love it. Promised.
Please feel free downloading the source of the project and
experimenting with it. At the beginning, it is pretty difficult to
understand how entities interact with their visitors. It was a big help
for me to debug through such visitor calls and especially to have a look
at the call stack to fully understand how this pattern works. But once
you get it, you wouldn't want to miss it ever more.
Once you have tried out the Visitor pattern, consider having a closer
look at other very powerful design patterns. Especially, take a look at
the Observer and Singleton design patterns. There are some very useful
articles about these patterns here on the CodeProject as well.