您的位置:首页 > 其它

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

2005-12-22 09:43 435 查看

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):
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:
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:
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:
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:
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:
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.
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:
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:
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:
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.
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.
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:
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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐