Extending the Visitor Design Pattern

The primary purpose of the Gamma/Helm Visitor design pattern is to promote code reuse. Matthew extends the Visitor pattern by adding conditional intelligence.


October 01, 1996
URL:http://www.drdobbs.com/tools/extending-the-visitor-design-pattern/184409974

October 1996: Extending the Visitor Design Pattern

Extending the Visitor Design Pattern

Conditional components give you more flexibility

Matthew Nguyen

Matthew is a consultant specializing in C++. He can be contacted at mnguyen@ clark.net.


The Visitor design pattern, presented in Design Patterns, Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, 1995), promotes loose coupling between data and functionality. Its primary purpose is to abstract functionality that can be applied to elements within a data structure. Think of a Visitor object as a door-to-door salesman; the data elements are represented by potential customers on his route. He must visit each of them to accomplish his task: for instance, selling insurance.

The visitor concept may take on many forms (a mailman, milkman, newspaper boy, and so on). In all abstracts, the visitor may engage with the consumer to perform some discrete action. A negotiation generally takes place in which the visitor presents himself and is either accepted or rejected by the consumer. For binary data, this separation offers many benefits. For one, it hides the implementation of the data structure (as well as the traversal method of its elements) from the user. With certain data structures, traversal may be complex, as with some recursive structures, and certainly, numerous requirements to access them only compound the problem. Iterators may offer a solution, but they are often limited by their ignorance of the datatypes upon which they traverse. With visitors, the datatypes are known upon entering the requisite Visit() function.

A Visitor object is generally tied to the elements that it traverses. An element must be able to accept a Visitor object and, in turn, offer itself to be visited. This negotiation provides proper type checking, and thereby offers an elegant solution to datatype discrimination without the need for creative casting and/or RTTI. Figure 1 outlines this exchange.

The approach is straightforward. The abstraction of functionality is encapsulated within each discrete visitor and is applied to each element through iteration. The method allows the construction of lightweight element container classes as element functionality is removed from the method's responsibility. New functionality can easily be added to a data structure through the construction of new Visitor objects. Example 1(a) is the template for a generalized Visitor design pattern.

Loop and End Loop outline the traversal, and Visitor.Visit() outlines the executing statement(s). It is within the Visit() function that analysis is performed. A standard statement may be composed of a variety of commands. These commands represent actions that may be applied to the attributes of the element. Commands may also be composed of conditionals, typically implemented by if/then/else logic, to accomplish some set of tasks across a specific-element domain. Herein is the weakness. By fusing the conditional within the statement, and thus within Visitor.Visit(), reusability is restricted for this object. The Visit() function in Example 1(b) demonstrates this shortcoming. Here, Element.ApplyDiscount() represents an atomic function that can be reused; yet when combined with the embedded conditional, limits the application of this visitor with respect to other services (for instance, ApplyDiscounts to all Elements that are green). While this statement represents a single command, you can visualize that a much larger grouping of commands, with a collective function deemed atomic, would have been wasted on this single-visit action.

By recognizing this anomaly, a way to separate the condition from the statement can be devised through the addition of conditional intelligence. Conditional abstraction can be implemented in many ways. It can be achieved through parameter passing, as in VisitAllElements(Visitor& float fCeilingPrice, colorType cType). However, this approach provides a very strict view of the data structure and would result in the implementation of many VisitAllElements() functions accommodating the numerous variations of the attributes of ElementType. An alternative method can be found to generalize the solution. Listing One extends the template in Example 1.

The extensions allow for conditional abstraction. However, prior to visitation, conditions may be attached on-the-fly to the Visitor object. This separation allows for the generation of reusable conditional components, as well as reusable statement components. These atoms may easily be combined to form more diverse and/or complex operations. With this approach, the Visitor class would export the methods in Listing Two. Thus, an implementation for DiscountElementVisitor would implement the Visit() function in Listing Three.

Notice that the conditional has been completely removed and the atomic function Element.ApplyDiscount() remains. AddConditional() and ClearConditionals() maintain the list of conditionals kept by the visitor. AddConditional() accepts a Conditional object and appends it to the internal array of known conditionals. ClearConditionals() is provided to reuse a visitor within a function and merely empties the internal list. The Operator object is used to evaluate the set of conditionals in the ApplyConditionals() function. Typically, these tests are made through simple && and || operations. Much like the conditionals, abstracting these logical operators allows more flexibility. It is useful to install some default operator with the creation of each visitor. This base action prevents the need to set the operator each time. Listing Four is the generalized Operator class.

GetInitialValue() is important as it seeds the tally test flag. The AND operator seeds this flag with a value of TRUE, while the OR operator would initialize with a FALSE value. The test flag is then continually used to determine consecutive conditional relationships. Visitor.ApplyConditionals() uses the Operator class to evaluate its internal conditionals through iteration; see Listing Five.

The implementation of the Conditional class is easily coded. Each Conditional object must be able to receive an Element and test it against some known criteria returning the result; see Listing Six. Through this interface, a typical conditional can be implemented, as in Listing Seven.

As seen, Visitor.ApplyConditionals() iterates through its list of attached conditions, calling Conditional.ApplyCondition() on the received Element and using the Operator object to evaluate multiple conditions. Upon a successful return, the Visitor.Visit() function would then be invoked, or else the Visitor.ElseVisit() function would trap.

Using these Visitor and Conditional objects then becomes a matter of mixing and matching to taste. Listing Eight uses these atomic objects to perform a variety of actions across the element list. The Visitor.ElseVisit() was found to be useful

under some circumstances, but its implementation makes the Visitor less generic, as it must know (to some degree) the implementation of the applied conditions. It was provided to allow the visitor to be flexible across all implementations.

The Visitor design pattern provides a nice abstraction when dealing with multiple elements of known types. Through conditional components, it is possible to develop a library of reusable conditionals and reusable visitors. These simple components, in turn, allow for the creation of complex components built from reusable objects. The cost analysis for this approach requires up-front development time for each Conditional object and each Visitor object which, in turn, depend on the attributes of Element. However, as the component library grows, development time would be consumed in the application of this extension. It is important to understand that the implementation of each Conditional and Visitor object should attempt to retain its atomic properties; that is, restrain from overloading these objects with too many conditions or commands that may restrict generic use. For complex operations that require the use of dissimilar functions, it is better to create multiple visitors, each implemented with discrete methods. A Composite Visitor would then be constructed to combine them for execution in one pass. This action allows multiple combinations of functionality to be applied through composition, but retains the generic nature of an atomic entity for each visitor.

Example 1: (a) Template of a generalized Visitor design pattern; (b) the Visit() function.

(a)
Loop on ElementList
     Visitor.Visit( Element (i) )
End Loop

(b)
void DiscountElementVisitor:: Visit ( ElementType& Element )
{
     if ( Element.price > SomeCeilingPrice )
          Element. ApplyDiscount (  Some DiscountValue );
}

Figure 1: Visitor negotiation process.

Listing One


Loop on ElementList
    if ( Visitor.ApplyConditionals( Element(i) )
        Visitor.Visit( Element(i) )
    else
        Visitor.ElseVisit( Element(i) )
End Loop



Listing Two


class Visitor
{
public :
    Visitor(){ m_pOperator = &globalAndOperator };   // install default
    virtual~Visitor(){};                             // operator
    virtual bool ApplyConditionals( ElementType& );
    void AddConditional( Conditional& rCondition )
        { m_ConditionalArray.Add( &rCondition ); }
    void ClearConditionals()
        { m_ConditionalArray.Clear(); }
    void SetOperator( Operator& rop )
        { m_pOpertator = &rop; }
    virtual int Visit( ElementType& ) = 0;
    virtual int ElseVisit( ElementType& )   // catch all else
        { return 0; }                       // not processed
private :
    ArrayPtr            m_ConditionalArray;
    Operator*           m_pOperator;
};

Listing Three


class DiscountElementVisitor : public Visitor
{
public :
    DiscountElementVisitor( float fValue )
    {
        m_fDiscount = fValue;
    }
    ~DiscountElementVisitor()

   {
    }
    int Visit( ElementType& ret )
    {
        // apply discount to all Elements that we see
        ret.ApplyDiscount( m_fDiscount );
        return 1;   // processed
    }
private :
    float m_fDiscount;
};

Listing Four


class Operator
{
public :     Operator() {};
    virtual~Operator() {};
    virtual bool GetInitialValue() = 0;
    virtual bool Evaluate( bool bTest, bool& rbStatus ) = 0;
};
class OperatorAND  : public Operator    // implement an AND operation
{
public :
    OperatorAND() {};
    ~OperatorAND() {};
    bool GetInitialValue() { return true; }
    bool Evaluate( bool bTest, bool& rbStatus )
    {
        rbStatus = bTest && rbStatus;
        return ( rbStatus );
    }
};

Listing Five


bool Visitor:: ApplyConditionals( ElementType& ret )
{   bool bStatus = true;
    bool bContinue = true;
    Conditional* pConditional;

    // check for the case when NO conditionals have been installed.
    // Allow process to continue in this case.

    if ( 0 == m_ConditionalArray.GetSize() )
        return true;
    // seed our test value.
    bStatus = m_pOperator->GetInitialValue();
    pConditional = m_ConditionalArray.GetStart();

   while (  bContinue && pConditional )
    {
        // evaluate Element with pConditional and set bStatus for
        // running tally. return false to bContinue on failure else true

        bContinue =
            m_pOperator->Evaluate
            (
              pConditional->    ApplyCondition(ret),
              bStatus
            );
        pConditional = m_ConditionalArray.GetNext();
    }
    return bStatus;
}

Listing Six


class Conditional       // base
{
public :
    Conditional()} :
    virtual~Conditional()
    virtual bool ApplyCondition( ElementType& ) =0 ;
};

Listing Seven


class ColorElementConditional : public Conditional
{
public :
    ColorElementConditional( colorType ct )
        { m_color = ct; }
    ~ColorElementConditional()
        { };
    bool ApplyCondition( ElementType& ret )
        {
            return ( m_color == ret.GetColor() ); //apply test
        }
private :
    colorType m_color;
};

Listing Eight


// discount on green items
void SomeObject:: ApplyDiscount1()
{
    DiscountElementVisitor dev( fSomeDiscount );
    ColorElementConditional cec( colorTypeGreen );

   dev.AddConditional( cec );
    VisitAllElements( dev );
}
// discount on certain priced items
void SomeObject:: ApplyDiscount2()
{
    DiscountElementVisitor dev( fSomeDiscount );
    PriceElementConditional pec( fSomeCeilingPrice );
    dev.AddConditional( pec );
    VisitAllElements( dev );
}
// discount on both
void SomeObject:: ApplyDiscount3()
{
    DiscountElementVisitor dev( fSomeDiscount );
    ColorElementConditional cec( colorTypeGreen );
    PriceElementConditional pec( fSomeCeilingPrice );
    dev.AddConditional( cec );
    dev.AddConditional( pec );
    VisitAllElements( dev );
}
// discount on either
void SomeObject:: ApplyDiscount4()
{
    DiscountElementVisitor dev( fSomeDiscount );
    ColorElementConditional cec( colorTypeGreen );
    PriceElementConditional pec( fSomeCeilingPrice );
    OperatorOR oor;
     dev.AddConditional( cec );
    dev.AddConditional( pec );
    dev.SetOperator( oor );       // use an OR operator

    VisitAllElements( dev );
}

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.