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 derivativesall the modemstogether, 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.

[2] [PLOPD3], p. 93

Figure 35-3. Acyclic Visitor


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


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.




Agile Principles, Patterns, and Practices in C#
Agile Principles, Patterns, and Practices in C#
ISBN: 0131857258
EAN: 2147483647
Year: 2006
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net