Using Reflection for the Visitor Pattern

Summary

This is a reposted article which used to live on dotnetdevs.com, originally published on June 7, 2004.

The Gang-of-Four Visitor pattern provides a formalized way to de-couple code using double dispatch. The traditional static-language implementation has limitations that can be overcome with reflection.

The source code for the Reflection Visitor Sample application is available here.

Introduction

Design Patterns is perhaps the book that's had the single greatest influence on my development career. The patterns, sometimes referred to as the Gang of Four Patterns, or GoF patterns (so named for the book's four authors), provide the foundation for excellent modern software design. Unlike most other patterns books, developers will find these patterns to be universally applicable, regardless of application type or language and environment choice.

This article focuses on one of the more interesting patterns: the Visitor. Specifically, I cover what the Visitor pattern is used for, and the alternatives to it. I discuss two visitor implementations: one that uses static typing, and one that uses reflection.

What is a Visitor?

The visitor pattern allows you to define new, polymorphic operations on a class structure, without touching the source code for the classes themselves. I've taken a Java-based example from the web and translated it into C#. The class hierarchy is:

public class Transaction
{
  [...definition of things common to transactions...]
}

public class DepositTransaction : Transaction
{
  [...things specific to deposits here...]
}

public class WithdrawalTransaction : Transaction
{
  [...things specific to withdrawals here...]
}

In addition, there are three user interface classes that we've developed: DefaultTransactionUI, DepositUI, and WithdrawalUI. These UI classes display transactions in our application.

The problem we're faced with is: When you have a Transaction in hand, what's the best way to decide which UI component to create?

The "Conditional" Solution

One solution to this problem is to write conditionals that test for type and take the appropriate action. This might look like:

public UIComponent CreateUIForTransaction(Transaction t)
{
  if (t is DepositTransaction)
    return new DepositUI(t as DepositTransaction);
  if (t is WithdrawalTransaction)
    return new WithdrawalUI(t as WithdrawalTransaction);
  return new DefaultTransactionUI(t);
}

As you can see, this solution isn't all that elegant, and doesn't really feel very object oriented. The use of conditionals and type-casting strongly suggest a refactoring is in order.

The "Extension" Solution

Another solution is to extend the classes themselves:

public class Transaction
{
  public virtual UIComponent CreateUI()
  {
    return new DefaultTransactionUI(this);
  }
}

public class DepositTransaction : Transaction
{
  public override UIComponent CreateUI()
  {
    return new DepositUI(this);
  }
}

public class WithdrawalTransaction : Transaction
{
  public virtual UIComponent CreateUI()
  {
    return new WithdrawalUI(this);
  }
}

This feels a little more object oriented, but it comes with a couple of problems.

First, it creates bi-directional coupling between the transaction classes and their UI classes. This is a potential warning sign of bad design.

Second, it mixes layers that shouldn't be mixed. Transaction is a business object, and the UI components are presentation objects. Not only do you end up tying these disparate layers together, you've forced a single presentation layer for the business object. It completely ignores the possibility of multiple UIs, or of a variety of context-specific display UIs.

The "Static Visitor" Solution

You solve the problems above by decoupling the classes via an intermediary, which we call a visitor class. The typical static-typed visitor implementation looks like this:

public interface ITransactionVisitor
{
  void VisitDefault(Transaction t);
  void Visit(DepositTransaction dt);
  void Visit(WithdrawalTransaction wt);
}

public class TransactionUIVisitor : ITransactionVisitor
{
  public UIComponent result;

  public void VisitDefault(Transaction t)
  {
    result = new DefaultTransactionUI(t);
  }

  public void Visit(DepositTransaction dt)
  {
    result = new DepositUI(dt);
  }

  public void Visit(WithdrawalTransaction wt)
  {
    result = new WithdrawalUI(dt);
  }
}

Then you end up extending your classes like so:

public class Transaction
{
  public virtual void DoVisit(ITransactionVisitor v)
  {
    v.VisitDefault(this);
  }
}

public class DepositTransaction : Transaction
{
  public override void DoVisit(ITransactionVisitor v)
  {
    v.Visit(this);
  }
}

public class WithdrawalTransaction : Transaction
{
  public override void DoVisit(ITransactionVisitor v)
  {
    v.Visit(this);
  }
}

To use the visitor, you create a class that's derived from ITransactionVisitor (like our example TransactionUIVisitor above), then call DoVisit on any Transaction-based object. Now you can see that we can re-write CreateUIForTransaction in terms of the visitor:

public UIComponent CreateUIForTransaction(Transaction t)
{
  TransactionUIVisitor v = new TransactionUIVisitor();
  t.DoVisit(v);
  return t.result;
}

Now we have a framework of decoupled code; namely, we have a single DoVisit function declared inside our business objects that lets us create visitors of any type, to perform any action, without needing to further extend our classes. We also have a decoupled CreateUIForTransaction function; when we add new types of transactions or new types of UI elements, we can modify TransactionUIVisitor.

While this goes a long ways towards making us happy, we are left with a couple things we're still not pleased with.

First, we have a lot of boiler-plate code in our Transaction classes. At the base level, we define our default DoVisit, and at every derived level, we re-define simply so we get static type dispatching from the compiler. Frankly, this is boring and mechanical code.

Second, we have an interface that needs to grow every time we add a class we might want to visit. While this seems like a trivial thing now with only two derived classes, it can become quite significant when you have a large heterogenous object model that all derives from a single class (or small handful of classes).

Third, we quickly find ourselves needing an abstract visitor class which provides a default implementation for all the Visit methods, like so:

public abstract class AbstractTransactionVisitor : ITransactionVisitor
{
  public virtual void Visit(DepositTransaction dt)
  {
    VisitDefault(dt);
  }

  public virtual void Visit(WithdrawalTransaction wt)
  {
    VisitDefault(dt);
  }
}

We need this because not every visitor is going to be interested in every single class; sometimes, you might only be interested in a few of the dozens (or perhaps even hundreds) of potential classes. Even worse, you'll need to keep up on whole derivation trees; the best action may be to delegate to the parent class type rather than to directly call VisitDefault, if you have a deep hierarchy. Yet more boring and mechanical -- and now, error prone -- code.

The "Reflection Visitor" Solution

You can eliminate the grunt work of the Static Visitor solution by using reflection. In order to do this, we write a method that does the dispatching for us:

public class ReflectionUtility
{
  public static bool VisitHelper(object visitor, object target)
  {
    Type[] types = new Type[] { target.GetType() };
    MethodInfo mi = visitor.GetType().GetMethod("Visit", types);

    if (mi == null)
      return false;

    mi.Invoke(visitor, new object[] { target });
    return true;
  }
}

This general purpose method can be called to do the work of visiting, as we'll see below. The VisitHelper method looks for a Visit method with a compatible parameter. If it finds one, it calls it, and then returns true; if it doesn't find one, it returns false.

Now we can go back to our Transaction classes. We'll re-write DoVisit() in Transaction as a non-virtual method, and remove DoVisit() from the derived classes, since it's not needed any more:

public class Transaction
{
  public void DoVisit(ITransactionVisitor v)
  {
    if (!ReflectionUtility.VisitHelper(v, this))
      v.VisitDefault(this);
  }
}

And we can cut all the cruft out of the visitor interface:

public interface ITransactionVisitor
{
  void VisitDefault(Transaction t);
}

Because we use reflection to find the Visit method rather than static-time compilation, we have a simple interface that never has to change, and derived classes that don't need any additional support. All the boring, mechanical, and error-prone work is eliminated.

Writing a visitor is simpler, too. We don't need an abstract base class to do all the forwarding to VisitDefault (or Visit for parent classes), because our visitors only ever have to implement VisitDefault(). Any Visit() they wish to add now is non-virtual, and not part of the interface. We use reflection to discover it, not the static type system.

Conclusion

Hopefully I've shed some light on what the Visitor pattern is used for, and how it can be enhanced on the .NET platform by using reflection.

I do want to say a word about performance. In our production code, we heavily use reflection based Visitors, so we cache the results of reflection. There's no question that reflection comes with a runtime performance penalty, which we can at least mitigate with a caching strategy. I strongly suggest that any heavy use of reflection, Visitor or not, should employ some form of caching (which is outside the scope of this article).

Kudos

You can't do these things alone. Thanks to Grant Killian for being my technical advisor for this article, and for providing the basis for the sample application. Thanks to Bernard Vander Beken for being my technical advisor for this article as well.

posted on Wednesday, June 06, 2007 9:01 AM

转载于:https://www.cnblogs.com/rickiedu/archive/2009/06/08/1498442.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值