Click here to Skip to main content
16,017,249 members
Articles / Programming Languages / C#
Article

Dude, are you still programming using if...then...else?

Rate me:
Please Sign up or sign in to vote.
4.09/5 (46 votes)
21 Dec 200512 min read 140.1K   523   96   15
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:

Entities

For our business logic, we need the FeeCalculator and the BillPrinter components (see the following picture for more details):

Business logic

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):

C#
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:

C#
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:

C#
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:

Modified entities which fit new requirements

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:

C#
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);
    }
}
    
// this visitor must be implemented by each service, which wants to do
// something with some person and has to make a case distinction
public interface IPersonVisitor
{
    void HandleMan(Man visitee);
    void HandleWoman(Woman visitee);
}

And finally, the new printing service itself:

C#
public class BillPrinter
{
    public void PrintBill(Policy p)
    {
        // create new visitor for printing the bills
        BillPrinterVisitor visitor = new BillPrinterVisitor(p);
        
        // and then ask the person to call the appropriate method 
        // of the visitor
        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();
    }

    // prints the bill if the visited person is a man
    public void HandleMan(Man visitee)
    {
        this.bill += "Dear Mr. " + visitee.Name;
        this.PrintRest();
    }

    // prints the bill if the visited person is a woman
    public void HandleWoman(Woman visitee)
    {
        this.bill += "Dear Ms. " + visitee.Name;
        this.PrintRest();
    }

    // adds the value of policy fee to the bill regardless
    // if the policy is attached to a man or a woman
    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:

C#
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.

C#
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:

C#
public abstract class AgeCategory
{
    public abstract void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor);
}
// i.e. for persons, which are between 0 and 17 years old
public class Child : AgeCategory
{
    public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
        visitor.HandleChild(this);
    }
}
// i.e for persons, which are between 18 and 25 years old
public class YoungAged : AgeCategory
{
    public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
        visitor.HandleYoungAged(this);
    }
}
// i.e for persons, which are between 26 and 60 years old
public class MatureAged : AgeCategory
{
    public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
        visitor.HandleMatureAged(this);
    }
}
// i.e for persons, which are between 61 and more years old
public class ElderAged : AgeCategory
{
    public override void AcceptAgeCategoryVisitor(IAgeCategoryVisitor visitor){
        visitor.HandleElderAged(this);
    }
}

Now, we add the visitor interface for age categories:

C#
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:

C#
public class CarInsuranceAgeFeeDiscountVisitor : IAgeCategoriesVisitor {
    private double discount = 0.0;
        
    public double GetDiscount() { return this.discount; }
    
    public void HandleChild(Child visitee){
        throw new ChildrenOughtNotDriveCarsException();
    }

    // young people cause more accidents, thus 50% higher insurance fee
    public void HandleYoungAged(YoungAged visitee){
        this.discount = -.5;
    }

    // people in this age are assumed to be more responsible in traffic 
    // as well, thus grant an additional discount of say 20%
    public void HandleMatureAged(MatureAged visitee){
        this.discount = .2;
    }

    // elder people again cause more accidents due to lower reactivity, 
    // thus no discount
    public void HandleElderAged(ElderAged visitee){
        this.discount = 0.0;
    }
}

Then, we define a visitor for the Person to which the policy is referred.

C#
public class PersonDependantFeeCalculator : IPersonVisitor
{
    public double InitialFee;
    ...
    public void HandleWoman(Woman visitee)
    {
        // we first look, what age the woman has and which age discount
        // is to be granted
        CarInsuranceAgeFeeDiscountVisitor visitor = new   
            CarInsuranceAgeFeeDiscountVisitor();
        visitee.Age.AcceptAgeCategoryVisitor(visitor);
        
        // then we calculate the overall discount
        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.

C#
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:

  • Short methods, usually with as much as 5-10 lines of code.
  • Using strong names for your visitors and handling methods elevate the overview of the control flow in your code to a completely new level (you see what happens on the names of your visitors).
  • You have easily extensible and maintainable code.
  • You can add functionality to your entities without having to modify and recompile them. Take different kinds of connections for instance, such as over WLAN, RAS, or the usual 100 Mbit network device. You can realize the Connect() method as a visitor. You, therefore, implement the code for establishing the connection in the handle methods, such as HandleWLAN(Wlan visitee):void, HandleRAS(Ras visitee):void etc. You then might establish the connection like this:
    C#
    rasConnection.AcceptConnectionVisitor(new ConnectionEstablisherVisitor());

    Consider, that you don’t need to modify the WLAN or RAS classes. So you don’t need to recompile them. You implement the behaviour for connection in the visitor.

  • You have truly reusable visitors. Take a look at the handling of key events in your GUI for instance. Imagine that you are designing an application for mobile devices. So you might need tens of different dialogues. But you want the reaction on Enter and Esc key be the same in each dialogue. So you must either code this behaviour in each dialogue (i.e., in a switch…case statement; regard the code duplication here). Now, imagine that you have to change the behaviour on pressing the Enter key. Therefore, you must change the code in every single dialogue, or you can define entities for Enter and Esc keys and a visitor on these entities, which implements the behaviour, and call it from each dialogue in a single line before processing other keys. In this case, you only need to modify the visitor in order to change the behaviour.

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Germany Germany
Maxim has 8 years of experience in .NET Compact Framework and WCF.

Comments and Discussions

 
QuestionBelieve me people still want if..then..else Pin
Member 1416774213-May-19 22:53
Member 1416774213-May-19 22:53 
QuestionJUST A COMMENT Pin
cyrusrynlee24-Jul-17 5:18
cyrusrynlee24-Jul-17 5:18 
GeneralGood extension to Vistor Pattern. Pin
Sameerkumar Namdeo29-Sep-10 19:13
Sameerkumar Namdeo29-Sep-10 19:13 
GeneralGr8 Pin
Atif Shahbaz20-Oct-08 0:18
Atif Shahbaz20-Oct-08 0:18 
Generalgood one, any other articles Pin
satyendrah5-Jan-07 9:05
satyendrah5-Jan-07 9:05 
GeneralUsing Strategy pattern Pin
beatles169213-Nov-06 21:31
beatles169213-Nov-06 21:31 
GeneralJust a tip Pin
Nathan Ridley13-Sep-06 19:23
Nathan Ridley13-Sep-06 19:23 
GeneralThanks Pin
Colin Meier2-Aug-06 1:02
Colin Meier2-Aug-06 1:02 
Questionhmmm .. what if you have 1000 policy types? Pin
Abel AA24-May-06 11:35
Abel AA24-May-06 11:35 
AnswerRe: hmmm .. what if you have 1000 policy types? Pin
BoneSoft7-Jul-06 9:55
BoneSoft7-Jul-06 9:55 
GeneralRe: hmmm .. what if you have 1000 policy types? Pin
Abel AA13-Jul-06 3:18
Abel AA13-Jul-06 3:18 
GeneralNice examples, but... Pin
User 20688521-Dec-05 18:19
User 20688521-Dec-05 18:19 
AnswerRe: Nice examples, but... Pin
Maxim Astafev22-Dec-05 11:10
Maxim Astafev22-Dec-05 11:10 
GeneralRe: Nice examples, but... Pin
Anton Afanasyev13-Nov-06 21:42
Anton Afanasyev13-Nov-06 21:42 
GeneralRe: Nice examples, but... Pin
CoolDadTx12-Sep-07 3:22
CoolDadTx12-Sep-07 3:22 
GeneralAfter reading that I'm going back to if..then..else Pin
compiler21-Dec-05 13:50
compiler21-Dec-05 13:50 
GeneralRe: After reading that I'm going back to if..then..else Pin
jeffb4222-Dec-05 8:27
jeffb4222-Dec-05 8:27 
AnswerRe: After reading that I'm going back to if..then..else Pin
Maxim Astafev22-Dec-05 12:39
Maxim Astafev22-Dec-05 12:39 
GeneralRe: After reading that I'm going back to if..then..else Pin
David O'Neil23-Dec-05 9:41
professionalDavid O'Neil23-Dec-05 9:41 
GeneralWell Thought Pin
aprenot21-Dec-05 12:43
aprenot21-Dec-05 12:43 
AnswerRe: Well Thought Pin
Maxim Astafev22-Dec-05 12:47
Maxim Astafev22-Dec-05 12:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.