35. Visitor

image

© Jennifer M. Kohnke

“’Tis some visitor,” I muttered, “tapping at my chamber door;

Only this and nothing more.”

—Edgar Allen Poe, The Raven

You need to add a new method to a hierarchy of classes, but the act of adding it will be painful or damaging to the design. This is a common problem. For example, suppose that you have a hierarchy of Modem objects. The base class has the generic methods common to all modems. The derivatives represent the drivers for many different modem manufacturers and types. Suppose also that you have a requirement to add a new method, named configureForUnix, to the hierarchy. This method will configure the modem to work with the UNIX operating system. The method will do something different in each modem derivative, because each modem has its own particular idiosyncrasies for setting its configuration, and dealing with UNIX.

Unfortunately, adding configureForUnix begs a terrible set of questions. What about Windows, what about OSX, what about Linux? Must we really add a new method to the Modem hierarchy for every new operating system that we use? Clearly, this is ugly. We’ll never be able to close the Modem interface. Every time a new operating system comes along, we’ll have to change that interface and redeploy all the modem software.

The VISITOR family allows new methods to be added to existing hierarchies without modifying the hierarchies. The patterns1 in this family are

VISITOR

ACYCLIC VISITOR

DECORATOR

EXTENSION OBJECT

Visitor

Consider the Modem hierarchy in Figure 35-1. The Modem interface contains the generic methods that all modems can implement. Three derivatives are shown: one that drives a Hayes modem, one that drives a Zoom modem, and one that drives the modem card produced by Ernie, one of our hardware engineers. How can we configure these modems for UNIX without putting the ConfigureForUnix method in the Modem interface? We can use a technique called dual dispatch, the mechanism at the heart of the VISITOR pattern.

Figure 35-1. Modem hierarchy

image

Figure 35-2 shows the VISITOR structure, and Listings 35-1 through 35-5 show the corresponding C# code. Listing 35-6 shows the test code that both verifies that VISITOR works and demonstrates how another programmer should use it.

Figure 35-2. Visitor

image

Note that the visitor hierarchy has a method in for every derivative of the visited (Modem) hierarchy. This is a kind of 90° rotation: from derivatives to methods.

The test code shows that to configure a modem for UNIX, a programmer creates an instance of the UnixModemConfigurator class and passes it to the Accept function of the Modem. The appropriate Modem derivative will then call Visit(this) on Modem-Visitor, the base class of UnixModemConfigurator. If that derivative is a Hayes, Visit(this) will call public void Visit(Hayes), which will deploy to the public void Visit(Hayes) function in UnixModemConfigurator, which then configures the Hayes modem for Unix.


Listing 35-1. Modem.cs

public interface Modem
{
  void Dial(string pno);
  void Hangup();
  void Send(char c);
  char Recv();
  void Accept(ModemVisitor v);
}



Listing 35-2. HayesModem.cs

public class HayesModem : Modem
{
  public void Dial(string pno){}
  public void Hangup(){}
  public void Send(char c){}
  public char Recv() {return (char)0;}
  public void Accept(ModemVisitor v) {v.Visit(this);}

  public string configurationString = null;
}



Listing 35-3. ZoomModem.cs

public class ZoomModem
{
  public void Dial(string pno){}
  public void Hangup(){}
  public void Send(char c){}
  public char Recv() {return (char)0;}
  public void Accept(ModemVisitor v) {v.Visit(this);}

  public int configurationValue = 0;
}



Listing 35-4. ErnieModem.cs

public class ErnieModem
{
  public void Dial(string pno){}
  public void Hangup(){}
  public void Send(char c){}
  public char Recv() {return (char)0;}
  public void Accept(ModemVisitor v) {v.Visit(this);}

  public string internalPattern = null;
}



Listing 35-5. UnixModemConfigurator.cs

public class UnixModemConfigurator : ModemVisitor
{
  public void Visit(HayesModem m)
  {
    m.configurationString = "&s1=4&D=3";
  }

  public void Visit(ZoomModem m)
  {
    m.configurationValue = 42;
  }

  public void Visit(ErnieModem m)
  {
    m.internalPattern = "C is too slow";
  }
}



Listing 35-6. ModemVisitorTest.cs

[TestFixture]
public class ModemVisitorTest
{
  private UnixModemConfigurator v;
  private HayesModem h;
  private ZoomModem z;
  private ErnieModem e;

  [SetUp]
  public void SetUp()
  {
    v = new UnixModemConfigurator();
    h = new HayesModem();
    z = new ZoomModem();
    e = new ErnieModem();
  }

  [Test]
  public void HayesForUnix()
  {
    h.Accept(v);
    Assert.AreEqual("&s1=4&D=3", h.configurationString);
  }

  [Test]
  public void ZoomForUnix()
  {
    z.Accept(v);
    Assert.AreEqual(42, z.configurationValue);
  }

  [Test]
  public void ErnieForUnix()
  {
    e.Accept(v);
    Assert.AreEqual("C is too slow", e.internalPattern);
  }
}


Having built this structure, new operating system configuration functions can be added by adding new derivatives of ModemVisitor without altering the Modem hierarchy in any way. So the VISITOR pattern substitutes derivatives of ModemVisitor for methods in the Modem hierarchy.

This dual dispatch involves two polymorphic dispatches. The first is the Accept function, which resolves the type of the object that Accept is called on. The second dispatch—the Visit method called from the resolved Accept method—resolves to the particular function to be executed.

The two dispatches of VISITOR form a matrix of functions. In our modem example, one axis of the matrix is the various types of modems; the other axis, the various types of operating systems. Every cell in this matrix is filled in with a function that describes how to initialize the particular modem for the particular operating system.

VISITOR is fast. It requires only two polymorphic dispatches, regardless of the breadth or depth of the visited hierarchy.

Acyclic Visitor

Note that the base class of the visited (Modem) hierarchy depends on the base class of the visitor hierarchy (ModemVisitor). Note also that the base class of the visitor hierarchy has a function for each derivative of the visited hierarchy. This cycle of dependencies ties all the visited derivatives—all the modems—together, making difficult to compile the visitor structure incrementally or to add new derivatives to the visited hierarchy.

The VISITOR pattern works well in programs in which the hierarchy to be modified does not need new derivatives very often. If Hayes, Zoom, and Ernie were the only Modem derivatives that were likely to be needed or if the incidence of new Modem derivatives was expected to be infrequent, VISITOR would be appropriate.

On the other hand, if the visited hierarchy is highly volatile, such that many new derivatives will need to be created, the visitor base class (e.g., ModemVisitor) will have to be modified and recompiled along with all its derivatives every time a new derivative is added to the visited hierarchy.

ACYCLIC VISITOR can be used to solve these problems.2 (See Figure 35-3.) This variation breaks the dependency cycle by making the Visitor base class (ModemVisitor) degenerate, that is, without methods. Therefore, this class does not depend on the derivatives of the visited hierarchy.

Figure 35-3. Acyclic Visitor

image

The visitor derivatives also derive from visitor interfaces. There is one visitor interface for each derivative of the visited hierarchy. This is a 180° rotation from derivatives to interfaces. The Accept functions in the visited derivatives cast the visitor base class to the appropriate visitor interface. If the cast succeeds, the method invokes the appropriate visit function. Listings 35-7 through 35-16 show the code.


Listing 35-7. Modem.cs

public interface Modem
{
  void Dial(string pno);
  void Hangup();
  void Send(char c);
  char Recv();
  void Accept(ModemVisitor v);
}



Listing 35-8. ModemVisitor.cs

public interface ModemVisitor
{
}



Listing 35-9. ErnieModemVisitor.cs

public interface ErnieModemVisitor : ModemVisitor
{
  void Visit(ErnieModem m);
}



Listing 35-10. HayesModemVisitor.cs

public interface HayesModemVisitor : ModemVisitor
{
  void Visit(HayesModem m);
}



Listing 35-11. ZoomModemVisitor.cs

public interface ZoomModemVisitor : ModemVisitor
{
  void Visit(ZoomModem m);
}



Listing 35-12. ErnieModem.cs

public class ErnieModem
{
  public void Dial(string pno){}
  public void Hangup(){}
  public void Send(char c){}
  public char Recv() {return (char)0;}
  public void Accept(ModemVisitor v)
  {
    if(v is ErnieModemVisitor)
      (v as ErnieModemVisitor).Visit(this);
  }

  public string internalPattern = null;
}



Listing 35-13. HayesModem.cs

public class HayesModem : Modem
{
  public void Dial(string pno){}
  public void Hangup(){}
  public void Send(char c){}
  public char Recv() {return (char)0;}
  public void Accept(ModemVisitor v)
  {
    if(v is HayesModemVisitor)
      (v as HayesModemVisitor).Visit(this);
  }

  public string configurationString = null;
}



Listing 35-14. ZoomModem.cs

public class ZoomModem
{
  public void Dial(string pno){}
  public void Hangup(){}
  public void Send(char c){}
  public char Recv() {return (char)0;}
  public void Accept(ModemVisitor v)
  {
    if(v is ZoomModemVisitor)
      (v as ZoomModemVisitor).Visit(this);
  }

  public int configurationValue = 0;
}



Listing 35-15. UnixModemConfigurator.cs

public class UnixModemConfigurator
  : HayesModemVisitor, ZoomModemVisitor, ErnieModemVisitor
{
  public void Visit(HayesModem m)
  {
    m.configurationString = "&s1=4&D=3";
  }

  public void Visit(ZoomModem m)
  {
    m.configurationValue = 42;
  }

  public void Visit(ErnieModem m)
  {
    m.internalPattern = "C is too slow";
  }
}



Listing 35-16. ModemVisitorTest.cs

[TestFixture]
public class ModemVisitorTest
{
  private UnixModemConfigurator v;
  private HayesModem h;
  private ZoomModem z;
  private ErnieModem e;

  [SetUp]
  public void SetUp()
  {
    v = new UnixModemConfigurator();
    h = new HayesModem();
    z = new ZoomModem();
    e = new ErnieModem();
  }

  [Test]
  public void HayesForUnix()
  {
    h.Accept(v);
    Assert.AreEqual("&s1=4&D=3", h.configurationString);
  }

  [Test]
  public void ZoomForUnix()
  {
    z.Accept(v);
    Assert.AreEqual(42, z.configurationValue);
  }

  [Test]
  public void ErnieForUnix()
  {
    e.Accept(v);
    Assert.AreEqual("C is too slow", e.internalPattern);
  }
}


This breaks the dependency cycle and makes it easier to add visited derivatives and to do incremental compilations. Unfortunately, it also makes the solution much more complex. Worse still, the timing of the cast can depend on the width and breadth of the visited hierarchy and is therefore difficult to characterize.

For hard real-time systems, the large and unpredictable execution time of the cast may make the ACYCLIC VISITOR inappropriate. For other systems, the complexity of the pattern may disqualify it. But for those systems in which the visited hierarchy is volatile and incremental compilation important, this pattern can be a good option.

Earlier, I explained how the VISITOR pattern created a matrix of functions, with the visited type on one axis and the function to be performed on the other. ACYCLIC VISITOR creates a sparse matrix. The visitor classes do not have to implement Visit functions for each visited derivative. For example, if Ernie modems cannot be configured for UNIX, the UnixModemConfigurator will not implement the ErnieVisitor interface.

Uses of Visitor

Report generation

The VISITOR pattern is commonly used to walk large data structures and to generate reports. The value of the VISITOR in this case is that the data structure objects do not have to have any report-generation code. New reports can be added by adding new VISITORs rather than by changing the code in the data structures. This means that reports can be placed in separate components and individually deployed only to those customers needing them.

Consider a simple data structure that represents a bill of materials (BOM) (see Figure 35-4). We could generate an unlimited number of reports from this data structure. We could generate a report of the total cost of an assembly or a report that listed all the pieceparts in an assembly.

Figure 35-4. Structure of bill of materials report generator

image

Each of these reports could be generated by methods in the Part class. For example, ExplodedCost and PieceCount could be added to the Part class. These properties would be implemented in each derivative of Part such that the appropriate reporting was accomplished. Unfortunately, every new report that the customers wanted would force us to change the Part hierarchy.

The Single-Responsibility Principle (SRP) told us that we want to separate code that changes for different reasons. The Part hierarchy may change as new kinds of parts are needed. However, it should not change because new kinds of reports are needed. Thus, we’d like to separate the reports from the Part hierarchy. The VISITOR structure shown in Figure 35-4 shows how this can be accomplished.

Each new report can be written as a new visitor. We write the Accept function of Assembly to visit the visitor and also call Accept on all the contained Part instances. Thus, the entire tree is traversed. For each node in the tree, the appropriate Visit function is called on the report. The report accumulates the necessary statistics. The report can then be queried for the interesting data and presented to the user.

This structure allows us to create an unlimited number of reports without affecting the part hierarchy. Moreover, each report can be compiled and distributed independently of all the others. This is nice. Listings 35-17 through 35-23 show how this looks in C#.


Listing 35-17. Part.cs

public interface Part
{
  string PartNumber { get; }
  string Description { get; }
  void Accept(PartVisitor v);
}



Listing 35-18. Assembly.cs

public class Assembly : Part
{
  private IList parts = new ArrayList();
  private string partNumber;
  private string description;

  public Assembly(string partNumber, string description)
  {
    this.partNumber = partNumber;
    this.description = description;
  }

  public void Accept(PartVisitor v)
  {
    v.Visit(this);
    foreach(Part part in Parts)
      part.Accept(v);
  }
  public void Add(Part part)
  {
    parts.Add(part);
  }

  public IList Parts
  {
    get { return parts; }
  }

  public string PartNumber
  {
    get { return partNumber; }
  }

  public string Description
  {
    get { return description; }
  }
}



Listing 35-19. PiecePart.cs

public class PiecePart : Part
{
  private string partNumber;
  private string description;
  private double cost;

  public PiecePart(string partNumber,
    string description,
    double cost)
  {
    this.partNumber = partNumber;
    this.description = description;
    this.cost = cost;
  }

  public void Accept(PartVisitor v)
  {
    v.Visit(this);
  }

  public string PartNumber
  {
    get { return partNumber; }
  }

  public string Description
  {
    get { return description; }
  }

  public double Cost
  {
    get { return cost; }
  }
}



Listing 35-20. PartVisitor.cs

public interface PartVisitor
{
  void Visit(PiecePart pp);
  void Visit(Assembly a);
}



Listing 35-21. ExplosiveCostExplorer.cs

public class ExplodedCostVisitor : PartVisitor
{
  private double cost = 0;

  public double Cost
  {
    get { return cost; }
  }

  public void Visit(PiecePart p)
  {
    cost += p.Cost;
  }

  public void Visit(Assembly a)
  {}
}



Listing 35-22. PartCountVisitor.cs

public class PartCountVisitor : PartVisitor
{
  private int pieceCount = 0;
  private Hashtable pieceMap = new Hashtable();

  public void Visit(PiecePart p)
  {
    pieceCount++;
    string partNumber = p.PartNumber;
    int partNumberCount = 0;
    if (pieceMap.ContainsKey(partNumber))
      partNumberCount = (int)pieceMap[partNumber];

    partNumberCount++;
    pieceMap[partNumber] = partNumberCount;
  }

  public void Visit(Assembly a)
  {
  }

  public int PieceCount
  {
    get { return pieceCount; }
  }

  public int PartNumberCount
  {
    get { return pieceMap.Count; }
  }

  public int GetCountForPart(string partNumber)
  {
    int partNumberCount = 0;
    if (pieceMap.ContainsKey(partNumber))
      partNumberCount = (int)pieceMap[partNumber];
    return partNumberCount;
  }
}



Listing 35-23. BOMReportTest.cs

[TestFixture]
public class BOMReportTest
{
  private PiecePart p1;
  private PiecePart p2;
  private Assembly a;

  [SetUp]
  public void SetUp()
  {
    p1 = new PiecePart("997624", "MyPart", 3.20);
    p2 = new PiecePart("7734", "Hell", 666);

    a = new Assembly("5879", "MyAssembly");
  }

  [Test]
  public void CreatePart()
  {
    Assert.AreEqual("997624", p1.PartNumber);
    Assert.AreEqual("MyPart", p1.Description);
    Assert.AreEqual(3.20, p1.Cost, .01);
  }

  [Test]
  public void CreateAssembly()
  {
    Assert.AreEqual("5879", a.PartNumber);
    Assert.AreEqual("MyAssembly", a.Description);
  }

  [Test]
  public void Assembly()
  {
    a.Add(p1);
    a.Add(p2);
    Assert.AreEqual(2, a.Parts.Count);
    PiecePart p = a.Parts[0] as PiecePart;
    Assert.AreEqual(p, p1);
    p = a.Parts[1] as PiecePart;
    Assert.AreEqual(p, p2);
  }

  [Test]
  public void AssemblyOfAssemblies()
  {
    Assembly subAssembly = new Assembly("1324", "SubAssembly");
    subAssembly.Add(p1);
    a.Add(subAssembly);

    Assert.AreEqual(subAssembly, a.Parts[0]);
  }

  private class TestingVisitor : PartVisitor
  {
    public IList visitedParts = new ArrayList();

    public void Visit(PiecePart p)
  {
    visitedParts.Add(p);
  }

  public void Visit(Assembly assy)
  {
    visitedParts.Add(assy);
  }
}


  [Test]
  public void VisitorCoverage()
  {
    a.Add(p1);
    a.Add(p2);

    TestingVisitor visitor = new TestingVisitor();
    a.Accept(visitor);

    Assert.IsTrue(visitor.visitedParts.Contains(p1));
    Assert.IsTrue(visitor.visitedParts.Contains(p2));
    Assert.IsTrue(visitor.visitedParts.Contains(a));
  }

  private Assembly cellphone;

  private void SetUpReportDatabase()
  {
    cellphone = new Assembly("CP-7734", "Cell Phone");
    PiecePart display = new PiecePart("DS-1428",
                                      "LCD Display",
                                      14.37);
    PiecePart speaker = new PiecePart("SP-92",
                                      "Speaker",
                                      3.50);
    PiecePart microphone = new PiecePart("MC-28",
                                          "Microphone",
                                           5.30);
    PiecePart cellRadio = new PiecePart("CR-56",
                                      "Cell Radio",
                                      30);
    PiecePart frontCover = new PiecePart("FC-77",
                                          "Front Cover",
                                          1.4);
    PiecePart backCover = new PiecePart("RC-77",
                                      "RearCover",
                                      1.2);
    Assembly keypad = new Assembly("KP-62", "Keypad");
    Assembly button = new Assembly("B52", "Button");
    PiecePart buttonCover = new PiecePart("CV-15",
                                          "Cover",
                                          .5);
    PiecePart buttonContact = new PiecePart("CN-2",
                                          "Contact",
                                          1.2);
    button.Add(buttonCover);
    button.Add(buttonContact);
    for (int i = 0; i < 15; i++)
      keypad.Add(button);
    cellphone.Add(display);
    cellphone.Add(speaker);
    cellphone.Add(microphone);
    cellphone.Add(cellRadio);
    cellphone.Add(frontCover);

    cellphone.Add(backCover);
    cellphone.Add(keypad);
  }

  [Test]
  public void ExplodedCost()
  {
    SetUpReportDatabase();
    ExplodedCostVisitor v = new ExplodedCostVisitor();
    cellphone.Accept(v);
    Assert.AreEqual(81.27, v.Cost, .001);
  }

  [Test]
  public void PartCount()
  {
    SetUpReportDatabase();
    PartCountVisitor v = new PartCountVisitor();
    cellphone.Accept(v);
    Assert.AreEqual(36, v.PieceCount);
    Assert.AreEqual(8, v.PartNumberCount);
    Assert.AreEqual(1, v.GetCountForPart("DS-1428"), "DS-
      1428");
    Assert.AreEqual(1, v.GetCountForPart("SP-92"), "SP-92");
    Assert.AreEqual(1, v.GetCountForPart("MC-28"), "MC-28");
    Assert.AreEqual(1, v.GetCountForPart("CR-56"), "CR-56");
    Assert.AreEqual(1, v.GetCountForPart("RC-77"), "RC-77");
    Assert.AreEqual(15, v.GetCountForPart("CV-15"), "CV-15");
    Assert.AreEqual(15, v.GetCountForPart("CN-2"), "CN-2");
    Assert.AreEqual(0, v.GetCountForPart("Bob"), "Bob");
  }
}


Other uses

In general, the VISITOR pattern can be used in any application having a data structure that needs to be interpreted in various ways. Compilers often create intermediate data structures that represent syntactically correct source code. These data structures are then used to generate compiled code. One could imagine visitors for each processor and/or optimization scheme. One could also imagine a visitor that converted the intermediate data structure into a cross-reference listing or even a UML diagram.

Many applications make use of configuration data structures. One could imagine the various subsystems of the application initializing themselves from the configuration data by walking it with their own particular visitors.

Whatever visitors are used, the data structure being used is independent of the uses to which it is being put. New visitors can be created, existing visitors can be changed, and all can be redeployed to installed sites without the recompilation or redeployment of the existing data structures. This is the power of the VISITOR.

Decorator

The VISITOR pattern gave us a way to add methods to existing hierarchies without changing those hierarchies. Another pattern that accomplishes this is DECORATOR.

Consider once again the Modem hierarchy in Figure 35-1. Imagine that we have an application that has many users. Sitting at a computer, each user can ask the system to call out to another computer, using the computer’s modem. Some of the users like to hear their modem’s dial. Others like their modems to be silent.

We could implement this by querying the user preferences at every location in the code where the modem is dialed. If the user wants to hear the modem, we set the speaker volume high; otherwise, we turn it off:

...
Modem m = user.Modem;
if (user.WantsLoudDial())
  m.Volume = 11; // it's one more than 10, isn't it?
m.Dial(...);
...

The specter of seeing this stretch of code duplicated hundreds of times throughout the application conjures images of 80-hour weeks and heinous debugging sessions. It is something to be avoided.

Another option would be to set a flag in the modem object itself and have the Dial method inspect it and set the volume accordingly:

...
public class HayesModem : Modem
{
  private bool wantsLoudDial = false;

  public void Dial(...)
  {
    if (wantsLoudDial)
    {
      Volume = 11;
    }
    ...
  }
  ...
}

This is better but must still be duplicated for every derivative of Modem. Authors of new derivatives of Modem must remember to replicate this code. Depending on programmers’ memories is pretty risky business.

We could resolve this with the TEMPLATE METHOD3 pattern by changing Modem from an interface to a class, having it hold the wantsLoudDial variable, and having it test that variable in the dial function before it calls the DialForReal function:

...
public abstract class Modem
{
  private bool wantsLoudDial = false;

  public void Dial(...)
  {
    if (wantsLoudDial)
    {
      Volume = 11;
    }
    DialForReal(...)
  }

  public abstract void DialForReal(...);
}

This is better still, but why should Modem be affected by the whims of the user in this way? Why should Modem know about loud dialing? Must it then be modified every time the user has some other odd request, such as logging out before hangup?

Once again, the Common Closure Principle (CCP) comes into play. We want to separate those things that change for different reasons. We can also invoke the Single-Responsibility Principle (SRP), since the need to dial loudly has nothing to do with the intrinsic functions of Modem and should therefore not be part of Modem.

DECORATOR solves the issue by creating a completely new class: LoudDialModem. LoudDialModem derives from Modem and delegates to a contained instance of Modem, catching the Dial function and setting the volume high before delegating. Figure 35-5 shows the structure.

Figure 35-5. DECORATOR: LoudDialModem

image

Now the decision to dial loudly can be made in one place. At the place in the code where the user sets preferences, a LoudDialModem can be created if loud dialing is requested, and the user’s modem can be passed into it. LoudDialModem will delegate all calls made to it to the user’s modem, so the user won’t notice any difference. The Dial method, however, will first set the volume high before delegating to the user’s modem. The LoudDialModem can then become the user’s modem without anybody else in the system being affected. Listings 35-24 through 35-27 show the code.


Listing 35-24. Modem.cs

public interface Modem
{
  void Dial(string pno);
  int SpeakerVolume { get; set; }
  string PhoneNumber { get; }
}



Listing 35-25. HayesModem.cs

public class HayesModem : Modem
{
  private string phoneNumber;
  private int speakerVolume;

  public void Dial(string pno)
  {
    phoneNumber = pno;
  }

  public int SpeakerVolume
  {
    get { return speakerVolume; }
    set { speakerVolume = value; }
  }

  public string PhoneNumber
  {
    get { return phoneNumber; }
  }
}



Listing 35-26. LoudDialModem.cs

public class LoudDialModem : Modem
{
  private Modem itsModem;

  public LoudDialModem(Modem m)
  {
    itsModem = m;
  }

  public void Dial(string pno)
  {
    itsModem.SpeakerVolume = 10;
    itsModem.Dial(pno);
  }

  public int SpeakerVolume
  {
    get { return itsModem.SpeakerVolume; }
    set { itsModem.SpeakerVolume = value; }
  }

  public string PhoneNumber
  {
    get { return itsModem.PhoneNumber; }
  }



Listing 35-27. ModemDecoratorTest.cs

[TestFixture]
public class ModemDecoratorTest
{
  [Test]
  public void CreateHayes()
  {
    Modem m = new HayesModem();
    Assert.AreEqual(null, m.PhoneNumber);
    m.Dial("5551212");
    Assert.AreEqual("5551212", m.PhoneNumber);
    Assert.AreEqual(0, m.SpeakerVolume);
    m.SpeakerVolume = 10;
    Assert.AreEqual(10, m.SpeakerVolume);
  }

  [Test]
  public void LoudDialModem()
  {
    Modem m = new HayesModem();
    Modem d = new LoudDialModem(m);
    Assert.AreEqual(null, d.PhoneNumber);
    Assert.AreEqual(0, d.SpeakerVolume);
    d.Dial("5551212");
    Assert.AreEqual("5551212", d.PhoneNumber);
    Assert.AreEqual(10, d.SpeakerVolume);
  }
}


Sometimes, two or more decorators may exist for the same hierarchy. For example, we may wish to decorate the Modem hierarchy with LogoutExitModem, which sends the string 'exit' whenever the Hangup method is called. This second decorator will have to duplicate all the delegation code that we have already written in LoudDialModem. We can eliminate this duplicate code by creating a new class, ModemDecorator, that supplies all the delegation code. Then the actual decorators can simply derive from ModemDecorator and override only those methods that they need to. Figure 35-6 and Listings 35-28 and 35-29 show the structure.

Figure 35-6. ModemDecorator

image


Listing 35-28. ModemDecorator.cs

public class ModemDecorator
{
  private Modem modem;

  public ModemDecorator(Modem m)
  {
    modem = m;
  }

  public void Dial(string pno)
  {
    modem.Dial(pno);
  }

  public int SpeakerVolume
  {
    get { return modem.SpeakerVolume; }
    set { modem.SpeakerVolume = value; }
  }

  public string PhoneNumber
  {
    get { return modem.PhoneNumber; }
  }

  protected Modem Modem
  {
    get { return modem; }
  }
}



Listing 35-29. LoudDialModem.cs

public class LoudDialModem : ModemDecorator
{
  public LoudDialModem(Modem m) : base(m)
  {}

  public void Dial(string pno)
  {
    Modem.SpeakerVolume = 10;
    Modem.Dial(pno);
  }
}


Extension Object

Still another way to add functionality to a hierarchy without changing it is to use the EXTENSION OBJECT pattern. This pattern is more complex than the others but is also much more powerful and flexible. Each object in the hierarchy maintains a list of special extension objects. Each object also provides a method that allows the extension object to be looked up by name. The extension object provides methods that manipulate the original hierarchy object.

For example, let’s assume that we have a BOM system again. We need to develop the ability for each object in this hierarchy to create an XML representation of itself. We could put toXML methods in the hierarchy, but this would violate CCP. It may be that we don’t want BOM stuff and XML stuff in the same class. We could create XML by using a VISITOR, but that doesn’t allow us to separate the XML-generating code for each type of BOM object. In a VISITOR, all the XML-generating code for each BOM class would be in the same VISITOR object. What if we want to separate the XML generation for each different BOM object into its own class?

EXTENSION OBJECT provides a nice way to accomplish this goal. The code that follows shows the BOM hierarchy with two kinds of extension object: one kind converts BOM objects into XML; the other kind converts BOM objects into CSV (commaseparated-value) strings. The first kind is accessed by GetExtension(“XML”); the second, by GetExtension(“CSV”). The structure is shown in Figure 35-7 and was taken from the completed code. The «marker» stereotype denotes a marker interface; that is, an interface with no methods.

Figure 35-7. Extension Object

image

The code is in Listings 35-30 through 35-41. It is important to understand that I did not simply write this code from scratch. Rather, I evolved the code from test case to test case. The first source file (Listing 35-30) shows all the test cases. They were written in the order shown. Each test case was written before there was any code that could make it pass. Once each test case was written and failing, the code that made it pass was written. The code was never more complicated than necessary to make the existing test cases pass. Thus, the code evolved in tiny increments from working base to working base. I knew that I was trying to build the EXTENSION OBJECT pattern and used that to guide the evolution.


Listing 35-30. BomXmlTest.CS

[TestFixture]
public class BomXmlTest
{
  private PiecePart p1;
  private PiecePart p2;
  private Assembly a;

  [SetUp]
  public void SetUp()
  {
    p1 = new PiecePart("997624", "MyPart", 3.20);
    p2 = new PiecePart("7734", "Hell", 666);
    a = new Assembly("5879", "MyAssembly");
  }

  [Test]
  public void CreatePart()
  {
    Assert.AreEqual("997624", p1.PartNumber);
    Assert.AreEqual("MyPart", p1.Description);
    Assert.AreEqual(3.20, p1.Cost, .01);
  }

  [Test]
  public void CreateAssembly()
  {
    Assert.AreEqual("5879", a.PartNumber);
    Assert.AreEqual("MyAssembly", a.Description);
  }

  [Test]
  public void Assembly()
  {
    a.Add(p1);
    a.Add(p2);
    Assert.AreEqual(2, a.Parts.Count);
    Assert.AreEqual(a.Parts[0], p1);
    Assert.AreEqual(a.Parts[1], p2);
  }

  [Test]
  public void AssemblyOfAssemblies()
  {
    Assembly subAssembly = new Assembly("1324", "SubAssembly");
    subAssembly.Add(p1);
    a.Add(subAssembly);

    Assert.AreEqual(subAssembly, a.Parts[0]);

  }

  private string ChildText(
    XmlElement element, string childName)
  {
    return Child(element, childName).InnerText;
  }

  private XmlElement Child(XmlElement element, string childName)
  {
    XmlNodeList children =
      element.GetElementsByTagName(childName);
    return children.Item(0) as XmlElement;
  }

  [Test]
  public void PiecePart1XML()
  {
    PartExtension e = p1.GetExtension("XML");
    XmlPartExtension xe = e as XmlPartExtension;
    XmlElement xml = xe.XmlElement;
    Assert.AreEqual("PiecePart", xml.Name);
    Assert.AreEqual("997624",
      ChildText(xml, "PartNumber"));
    Assert.AreEqual("MyPart",
      ChildText(xml, "Description"));
    Assert.AreEqual(3.2,
      Double.Parse(ChildText(xml, "Cost")), .01);
  }

  [Test]
  public void PiecePart2XML()
  {
    PartExtension e = p2.GetExtension("XML");
    XmlPartExtension xe = e as XmlPartExtension;
    XmlElement xml = xe.XmlElement;
    Assert.AreEqual("PiecePart", xml.Name);
    Assert.AreEqual("7734",
      ChildText(xml, "PartNumber"));
    Assert.AreEqual("Hell",
      ChildText(xml, "Description"));
    Assert.AreEqual(666,
      Double.Parse(ChildText(xml, "Cost")), .01);
}

  [Test]
  public void SimpleAssemblyXML()
  {
    PartExtension e = a.GetExtension("XML");
    XmlPartExtension xe = e as XmlPartExtension;
    XmlElement xml = xe.XmlElement;
    Assert.AreEqual("Assembly", xml.Name);
    Assert.AreEqual("5879",
      ChildText(xml, "PartNumber"));

    Assert.AreEqual("MyAssembly",
      ChildText(xml, "Description"));
    XmlElement parts = Child(xml, "Parts");
    XmlNodeList partList = parts.ChildNodes;
    Assert.AreEqual(0, partList.Count);
  }

  [Test]
  public void AssemblyWithPartsXML()
  {

    a.Add(p1);
    a.Add(p2);
    PartExtension e = a.GetExtension("XML");
    XmlPartExtension xe = e as XmlPartExtension;
    XmlElement xml = xe.XmlElement;
    Assert.AreEqual("Assembly", xml.Name);
    Assert.AreEqual("5879",
      ChildText(xml, "PartNumber"));
    Assert.AreEqual("MyAssembly",
      ChildText(xml, "Description"));

    XmlElement parts = Child(xml, "Parts");
    XmlNodeList partList = parts.ChildNodes;
    Assert.AreEqual(2, partList.Count);

    XmlElement partElement =
      partList.Item(0) as XmlElement;
    Assert.AreEqual("PiecePart", partElement.Name);
    Assert.AreEqual("997624",
      ChildText(partElement, "PartNumber"));

    partElement = partList.Item(1) as XmlElement;
    Assert.AreEqual("PiecePart", partElement.Name);
    Assert.AreEqual("7734",
      ChildText(partElement, "PartNumber"));
  }

  [Test]
  public void PiecePart1toCSV()
  {
    PartExtension e = p1.GetExtension("CSV");
    CsvPartExtension ce = e as CsvPartExtension;
    String csv = ce.CsvText;
    Assert.AreEqual("PiecePart,997624,MyPart,3.2", csv);
  }

  [Test]
  public void PiecePart2toCSV()
  {
    PartExtension e = p2.GetExtension("CSV");
    CsvPartExtension ce = e as CsvPartExtension;
    String csv = ce.CsvText;
    Assert.AreEqual("PiecePart,7734,Hell,666", csv);
  }

  [Test]
  public void SimpleAssemblyCSV()
  {
    PartExtension e = a.GetExtension("CSV");
    CsvPartExtension ce = e as CsvPartExtension;
    String csv = ce.CsvText;
    Assert.AreEqual("Assembly,5879,MyAssembly", csv);
  }

  [Test]
  public void AssemblyWithPartsCSV()
  {
    a.Add(p1);
    a.Add(p2);
    PartExtension e = a.GetExtension("CSV");
    CsvPartExtension ce = e as CsvPartExtension;
    String csv = ce.CsvText;

    Assert.AreEqual("Assembly,5879,MyAssembly," +
      "{PiecePart,997624,MyPart,3.2}," +
      "{PiecePart,7734,Hell,666}"
      , csv);
  }

  [Test]
  public void BadExtension()
  {
    PartExtension pe = p1.GetExtension(
      "ThisStringDoesn'tMatchAnyException");
    Assert.IsTrue(pe is BadPartExtension);
  }
}



Listing 35-31. Part.cs

public abstract class Part
{
  Hashtable extensions = new Hashtable();

  public abstract string PartNumber { get; }
  public abstract string Description { get; }

  public void AddExtension(string extensionType,
    PartExtension extension)
  {
    extensions[extensionType] = extension;
  }

  public PartExtension GetExtension(string extensionType)
  {
    PartExtension pe =
      extensions[extensionType] as PartExtension;
    if (pe == null)
      pe = new BadPartExtension();
    return pe;
  }
}



Listing 35-32. PartExtension.cs

public interface PartExtension
{
}



Listing 35-33. PiecePart.cs

public class PiecePart : Part
{
  private string partNumber;
  private string description;
  private double cost;

  public PiecePart(string partNumber,
    string description,
    double cost)
  {
    this.partNumber = partNumber;
    this.description = description;
    this.cost = cost;
    AddExtension("CSV", new CsvPiecePartExtension(this));
    AddExtension("XML", new XmlPiecePartExtension(this));
  }

  public override string PartNumber
  {
    get { return partNumber; }
  }

  public override string Description
  {
    get { return description; }
  }

  public double Cost
  {
    get { return cost; }
  }
}



Listing 35-34. Assembly.cs

public class Assembly : Part
{
  private IList parts = new ArrayList();
  private string partNumber;
  private string description;

  public Assembly(string partNumber, string description)
  {
    this.partNumber = partNumber;

    this.description = description;
    AddExtension("CSV", new CsvAssemblyExtension(this));
    AddExtension("XML", new XmlAssemblyExtension(this));
  }

  public void Add(Part part)
  {
    parts.Add(part);
  }

  public IList Parts
  {
    get { return parts; }
  }

  public override string PartNumber
  {
    get { return partNumber; }
  }

  public override string Description
  {
    get { return description; }
  }
}



Listing 35-35. XmlPartExtension.cs

public abstract class XmlPartExtension : PartExtension
{
  private static XmlDocument document = new XmlDocument();

  public abstract XmlElement XmlElement { get; }

  protected XmlElement NewElement(string name)
  {
    return document.CreateElement(name);
  }

  protected XmlElement NewTextElement(
    string name, string text)
  {
    XmlElement element = document.CreateElement(name);
    XmlText xmlText = document.CreateTextNode(text);
    element.AppendChild(xmlText);
    return element;
  }
}



Listing 35-36. XmlPiecePartExtension.cs

public class XmlPiecePartExtension : XmlPartExtension
{
  private PiecePart piecePart;

  public XmlPiecePartExtension(PiecePart part)
  {
    piecePart = part;
  }

  public override XmlElement XmlElement
  {
    get
    {
      XmlElement e = NewElement("PiecePart");
      e.AppendChild(NewTextElement(
        "PartNumber", piecePart.PartNumber));
      e.AppendChild(NewTextElement(
        "Description", piecePart.Description));
      e.AppendChild(NewTextElement(
        "Cost", piecePart.Cost.ToString()));

      return e;
    }
  }
}



Listing 35-37. XmlAssemblyExtension.cs

public class XmlAssemblyExtension : XmlPartExtension
{
  private Assembly assembly;

  public XmlAssemblyExtension(Assembly assembly)
  {
    this.assembly = assembly;
  }

  public override XmlElement XmlElement
  {
    get
    {
      XmlElement e = NewElement("Assembly");
      e.AppendChild(NewTextElement(
        "PartNumber", assembly.PartNumber));
      e.AppendChild(NewTextElement(
        "Description", assembly.Description));

      XmlElement parts = NewElement("Parts");
      foreach(Part part in assembly.Parts)

      {
        XmlPartExtension xpe =
          part.GetExtension("XML")
          as XmlPartExtension;
        parts.AppendChild(xpe.XmlElement);
      }
      e.AppendChild(parts);

      return e;
    }
  }
}



Listing 35-38. CsvPartExtension.cs

public interface CsvPartExtension : PartExtension
{
  string CsvText { get; }
}



Listing 35-39. CsvPiecePartExtension.cs

public class CsvPiecePartExtension : CsvPartExtension
{
  private PiecePart piecePart;

  public CsvPiecePartExtension(PiecePart part)
  {
    piecePart = part;
  }

  public string CsvText
  {
    get
    {
      StringBuilder b =
        new StringBuilder("PiecePart,");
      b.Append(piecePart.PartNumber);
      b.Append(",");
      b.Append(piecePart.Description);
      b.Append(",");
      b.Append(piecePart.Cost);
      return b.ToString();
    }
  }
}



Listing 35-40. CsvAssemblyExtension.cs

public class CsvAssemblyExtension : CsvPartExtension
{
  private Assembly assembly;

  public CsvAssemblyExtension(Assembly assy)
  {
    assembly = assy;
  }

  public string CsvText
  {
    get
    {
      StringBuilder b =
        new StringBuilder("Assembly,");
      b.Append(assembly.PartNumber);
      b.Append(",");
      b.Append(assembly.Description);

      foreach(Part part in assembly.Parts)
      {
        CsvPartExtension cpe =
          part.GetExtension("CSV")
          as CsvPartExtension;
        b.Append(",{");
        b.Append(cpe.CsvText);
        b.Append("}");
      }
      return b.ToString();
    }
  }
}



Listing 35-41. BadPartExtension.cs

public class BadPartExtension : PartExtension
{
}


Note that the extension objects are loaded into each BOM object by that object’s constructor. This means that, to some extent, the BOM objects still depend on the XML and CSV classes. If even this tenuous dependency needs to be broken, we could create a FACTORY4 object that creates the BOM objects and loads their extensions.

The fact that the extension objects can be loaded into the object creates a great deal of flexibility. Certain extension objects can be inserted or deleted from objects depending upon the state of the system. It would be easy to get carried away with this flexibility. For the most part, you probably won’t find it necessary. Indeed, the original implementation of PiecePart.GetExtention(String extensionType) looked like this.

public PartExtension GetExtension(String extensionType)
{
  if (extensionType.Equals("XML"))
    return new XmlPiecePartExtension(this);

  else if (extensionType.Equals("CSV"))
    return new XmlAssemblyExtension(this);

  return new BadPartExtension();
}

I wasn’t particularly thrilled with this, because it was virtually identical to the code in Assembly.GetExtension. The Hashtable solution in Part avoids this duplication and is simpler. Anyone reading it will know exactly how extension objects are accessed.

Conclusion

The VISITOR family of patterns provides us with a number of ways to modify the behavior of a hierarchy of classes without having to change them. Thus, they help us maintain the Open/Closed Principle. They also provide mechanisms for segregating various kinds of functionality, keeping classes from getting cluttered with many different functions. As such, they help us maintain the Common Closure Principle. It should be clear that LSP and DIP are also applied to the structure of the VISITOR family.

The VISITOR patterns are seductive. It is easy to get carried away with them. Use them when they help, but maintain a healthy skepticism about their necessity. Often, something that can be solved with a VISITOR can also be solved by something simpler.5

Bibliography

[GOF95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.

[PLOPD3] Robert C. Martin, Dirk Riehle, and Frank Buschmann, eds. Pattern Languages of Program Design 3, Addison-Wesley, 1998.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.0.240