Practical Uses of the Visitor Pattern

When I first read about the visitor pattern I did not understand why it worked or how it would be useful.  Now, many years later, I finally understand why it works, but until recently I haven't encountered a situation where the visitor pattern was superior to alternative patterns.

Today I came across a problem in which the visitor pattern actually seemed to offer some value in real code.  A data contract specifies a message containing a member with an abstract data type.  The caller is expected to provide a concrete implementation of that abstract type (one of several known DTO types) in the message.  When the message is received, the concrete object must be mapped onto a corresponding domain object.  Then after the request has completed, the domain object must be mapped back onto the DTO type for inclusion in the response message.  Both the DTO type and the domain object type are polymorphic, so the concrete implementations are not at all important to the domain.

The goals are to:
  • Perform the mapping back and forth with little or no knowledge of the concrete types involved
  • Make it as simple and straightforward as possible to define new concrete types
  • Leverage the compiler as much as possible to ensure type safety and that all types are handled.
My first thought was to create a mapper class in the domain with public ToDto and FromDto methods that consisted of a set of is/instanceof checks to determine the type of the input object, cast it, and then call the appropriate strongly typed method.  Something like this:

public IDtoType ToDto(IDomainType domainObject) {
    if(domainObject == null) return null;
    else if (domainObject is DomainTypeA) return ToDto((DomainTypeA)domainObject);
    else if (domainObject is DomainTypeB) return ToDto((DomainTypeB)domainObject);
    ...
}

However, this fails in a couple respects.  First, if DomainTypeB extends DomainTypeA then we have problem.  Second, if DomainTypeC is added, there is no compiler warning or error to check to see if support  for it has been added in the mapper class.  Third, there is no way to unit test that support exists for DomainTypeC.

Enter the visitor pattern.  By defining two "conversion" visitors DtoToDomainVisitor and DomainToDtoVisitor that are accepted by IDtoType and IDomainType (respectively), I get the following benefits:
  • Every concrete implementation of IDtoType and IDomainType must implement the abstract Accept methods
  • Every implementation of the Accept method requires a corresponding Visit method on the visitor for that specific type
  • Ergo, adding concrete types will result in a compiler error until both the Accept method and Visit method override are implemented.
It's a bit more code, but I think the compile-time verification is worth it.  Here is what it might look like:

interface IDtoType {
    ...
    void Accept(IDtoTypeVisitor visitor);
}

interface IDtoTypeVisitor {
    void Visit(DtoTypeA dto);
    void Visit(DtoTypeB dto);
    ...
}

interface IDomainType {
   ...
   void Accept(IDomainTypeVisitor visitor);
}

interface IDomainTypeVisitor {
   void Accept(DomainTypeA domainObject);
   void Accept(DomainTypeB domainObject);
   ...
}

class DtoToDomainVisitor : IDtoTypeVisitor {
   public IDomainType DomainObject {get; set;}
   public void Visit(DtoTypeA dto ) {
      DomainObject = new DomainTypeA(dto.Value1, dto.Value1, ...);
   }

   public void Visit(DtoTypeB dto) {
      DomainObject = new DomainTypeB(dto.Value1, dto.Value2, ...);
   }
}

class DomainToDtoVisitor : IDomainTypeVisitor {
   public IDtoType Dto {get; set;}
   public void Visit(DomainTypeA domainObject) {
      Dto = new DtoTypeA { Value1 = domainObject.Value1, Value2 = domainObject.Value2, ... };
   }

   public void Visit(DomainTypeB domainObject) {
      Dto = new DtoTypeB { Value1 = domainObject.Value1, Value2 = domainObject.Value2, ... };
   }
}

static class Mapper {
   public static IDtoType ToDto(IDomainType domainObject) {
      var visitor = new DomainToDtoVisitor();
      domainObject.Accept(visitor);
      return visitor.Dto;
   }

   public static IDomainType ToDomainObject(IDtoType dto) {
      var visitor = new DtoToDomainConverter();
      dto.Accept(visitor);
      return visitor.DomainObject;
   }
}

And then, to round it out, the Accept methods in the concrete types:
class DtoTypeA : IDtoType {
   ...
   void Accept(IDtoTypeVisitor visitor) {
       visitor.Visit(this); // Calls override IDtoType.Visitor(DtoTypeA)
   }
}

class DtoTypeB : IDtoType {
   ...
   void Accept(IDtoTypeVisitor visitor) {
       visitor.Visit(this); // Calls override IDtoType.Visitor(DtoTypeB)
   }
}

class DomainTypeA : IDomainType {
   ...
   void Accept(IDomainTypeVisitor visitor) {
       visitor.Visit(this); // Calls override IDomainType.Visitor(DomainTypeA)
   }
}

class DomainTypeB : IDomainType {
   ...
   void Accept(IDomainTypeVisitor visitor) {
       visitor.Visit(this); // Calls override IDomainType.Visitor(DomainTypeB)
   }
}

When DtoTypeC is added, here is what happens:
  • DtoTypeC must implement IDtoType.Accept(IDtoTypeVisitor)
  • The Accept(IDtoTypeVisitor) implementation in DtoTypeC has no Visit(DtoTypeC method to call, so there is a compiler error until a Visit(DtoTypeC) override is added to IDtoTypeVisitor
  • Once the Visit(DtoTypeC) is added to IDtoTypeVisitor, a compiler error occurs until it is implemented in all implementations of IDtoTypeVisitor.
  • Once the Visit(DtoTypeC) overrides are added to the implementations, the compiler is satisfied, and all types are accounted for.
And the same happens for DomainTypeC.  It's a little harder to follow, but it offers a nice separation of concerns and hides the complexity of conversion from the domain objects.

Comments

Popular posts from this blog

Non-nullable Reference Types

Objects Are People, Too

Named Locks and Lock Striping