Chapter 15. Visitor

Imagine you have some plumbing problems in your house but you don't know how to fix them. Even though you are the owner of the house, that doesn't mean you know all the ins and outs of it. Therefore, the most efficient way to solve this problem is to call in an expert to get it fixed as soon as possible.

In software design, a class can become extremely complex if the architect stuffs too many methods into it to extend its functionality. A better approach is to have an external class that knows how to extend it without changing the original code much.

The Visitor pattern can be easily described by using examples from your everyday life. In the case of the household plumbing problem, you don't want to learn how to fix plumbing (add more methods to a class). So you call in a plumber (visitor). When he arrives and rings your doorbell, you open the door and let him in (accept). Then he enters the house and fixes the plumbing (visits).

This chapter will cover the concepts of the Visitor pattern and how the plumber example relates to the pattern. We will design and implement a visitor that renders user strokes for the TouchPainter app that first appeared in Chapter 2.

What is the Visitor Pattern?

There are two key roles/components involved in the Visitor pattern, a visitor and an element that it visits. An element can be any object, but it is usually a node in a part-whole structure (see the Composite pattern in Chapter 13). A part-whole structure contains composite and leaf nodes or any other complex object structure. The element itself is not only limited to these kinds of structures. A visitor that knows every element in a complex structure can visit each of the element's nodes and perform any operations based on the element's attributes, properties, or operations. The class diagram in Figure 15-1 illustrates their static relationships.

A class diagram illustrating the static structure of the Visitor pattern

Figure 15-1. A class diagram illustrating the static structure of the Visitor pattern

The Visitor protocol declares a couple of similar-looking visit* methods that are used for visiting and processing objects of different Element types. ConcreteVisitor (1 or 2) implements the protocol and its abstract methods. A visit* operation defines appropriate operations that target a particular Element type. The Client creates a ConcreteVisitor (1 or 2) object and passes it to an Element object structure in which there is a method that accepts a generic Visitor type. The operation in each accept* method in the Element classes is almost the same. There is only a single statement that allows a Visitor object to visit the calling concrete Element object. The only difference between concrete Element classes is that the type of visit* message actually used is defined in each concrete Element class. Each time an acceptVisitor: message is passed to an Element structure, each node will be forwarded with the same message. The correct version of visit* method will be used, based on the actual type of the Element object determined at runtime.

Note

The Visitor pattern represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates. [13]

The class diagram in Figure 15-2 shows how the house contractor scenario can fit in the previous visitor-element relationships.

A class diagram of the contractor examples as visitors

Figure 15-2. A class diagram of the contractor examples as visitors

This contractor version of the Visitor pattern reflects an implementation of the pattern in terms of the house contractor examples. Plumber and Electrician are visitors. House is a complex structure that contains Fixable abstraction items that a Contractor can visit and fix. A Plumber can visit the Plumbing structure of the House to fix it with its proprietary visitPlumbing: operation. Likewise, an Electrician can visit the Electrical components of the same House for the same reason with his own visitElectrical: operation. A general Contractor seems to know how to fix both Plumbing and Electrical, but in fact, it sub-contracts those jobs to a real person who knows how to get the job done, just like in the real world. You don't even need to care about the details; you just open the door to let him in and you pay for it after the job is done. Contractors (visitors) can perform certain technical jobs without the house owners needing to learn new skills (modifying existing code).

When Would You Use the Visitor Pattern

You can use the Visitor pattern in the following scenarios:

  • A complex object structure contains many other objects with differing interfaces (e.g. a composite), and you want to perform operations based on their concrete types.

  • You need to perform many unrelated operations on objects in a composite structure without polluting their classes with these operations. You can keep related operations together in one visitor class and use it when the operations defined in the visitor are needed.

  • You often need to add new operations to a complex structure and the classes that define the structure rarely change.

Rendering Marks in TouchPainter with Visitors

One of the key features of the TouchPainter app discussed in Chapter 2 is that the user can draw anything on the screen with touches. In Chapter 13, you defined a composite data structure that contains user-created strokes and dots. The main purpose of the composite structure is to maintain the abstraction for clients that manipulate it without exposing its internal representation and the complexity. The high-level abstract interface for all types of leaf and composite nodes declares some primitive operations with which each of them can perform. If you need to add operations to a composite, you need to make changes to all of the interfaces for each node classes. If it happens often, the impact could be huge, especially for large projects. Class designers can try to predict possible feature extensions on a composite and add them to the class design, but this is not really a solution for an open-ended problem like this one.

A better solution is to apply the Visitor pattern to a composite. A visitor can be a consolidation of related operations that can be performed through a composite object based on the type of each node. It's like the plumber example where a plumber is a visitor who visits a house to fix plumbing related problems. Each node in a composite "accepts" a visitor to "visit" the node to fix problems or perform operations.

You can apply the same idea to extend the behavior of a Mark composite. In the code in Chapter 13, you added a drawing operation for each node to perform some basic drawing with an active graphics context on the CanvasView. You can also refactor the drawing algorithm out into a separate visitor called MarkRenderer. A MarkRenderer object can visit each node of a Mark composite and perform any Quartz 2D operations to render dots, vertices, and strokes on the screen. A visual diagram that illustrates how a MarkRenderer walks through a Mark composite to draw lines and dots is shown in Figure 15-3.

A flow diagram of an actual rendering process with visitor MarkRenderer

Figure 15-3. A flow diagram of an actual rendering process with visitor MarkRenderer

When a MarkRenderer object is visiting through the Mark composite structure, it invokes corresponding Quartz 2D operations based on the type of each node visited. Their static relationships are shown in the class diagram in Figure 15-4.

A class diagram of MarkRenderer and its relationships with the Mark composite classes

Figure 15-4. A class diagram of MarkRenderer and its relationships with the Mark composite classes

MarkRenderer implements the MarkVisitor protocol for various visiting operations on a Mark composite. Each visit* method should be only good for visiting one particular type of node and a parameter of a node is provided by the node itself at runtime.

On the side of the Mark family, each node has added a new method, acceptMarkVisitor:. When a client passes a MarkVisitor object to a Mark composite through the acceptMarkVisitor: method, the MarkVisitor object will be passed along the whole structure. The client has no idea how the visitor object gets passed down the pipeline. But every time the visitor object visits a particular node, an acceptMarkVisitor: method of the node will send a corresponding visit* message to the visitor. Based on the visit* method, appropriate operations will be carried out accordingly.

Let's look at how to put the whole thing in code. First, let's look at the MarkVisitor protocol, as shown in Listing 15-1.

Example 15-1. A Protocol Declaration of MarkVisitor in MarkVisitor.h

@protocol Mark;
@class Dot, Vertex, Stroke;

@protocol MarkVisitor <NSObject>

- (void) visitMark:(id <Mark>)mark;
- (void) visitDot:(Dot *)dot;
- (void) visitVertex:(Vertex *)vertex;
- (void) visitStroke:(Stroke *)stroke;

@end

One of the reasons you define the top-level abstract type for MarkVisitor as a protocol is because you are not sure whether future MarkVisitor implementing classes need to subclass other classes for their services.

For now, your MarkRenderer is a subclass of NSObject, as shown in Listing 15-2.

Example 15-2. A Class Declaration of MarkRenderer in MarkRenderer.h

#import "MarkVisitor.h"
#import "Dot.h"
#import "Vertex.h"
#import "Stroke.h"

@interface MarkRenderer : NSObject <MarkVisitor>
{
  @private
  BOOL shouldMoveContextToDot_;

  @protected
  CGContextRef context_;
}

- (id) initWithCGContext:(CGContextRef)context;

- (void) visitMark:(id <Mark>)mark;
- (void) visitDot:(Dot *)dot;
- (void) visitVertex:(Vertex *)vertex;
- (void) visitStroke:(Stroke *)stroke;

@end

MarkRenderer has some private and protected member variables. I'll get to them in a bit. MarkRenderer implements MarkVisitor and its visit* operations. It also declares an initialization method that takes a CGContextRef for drawing things later with all the nodes in a Mark composite. Now let's look at the implementation of MarkRenderer in Listing 15-3. I'll walkyou through several major areas of the code.

Example 15-3. The Implementation of MarkRenderer in MarkRenderer.m

#import "MarkRenderer.h"

@implementation MarkRenderer

- (id) initWithCGContext:(CGContextRef)context
{
  if (self = [super init])
  {
    context_ = context;
    shouldMoveContextToDot_ = YES;
  }
  return self;
}

- (void) visitMark:(id <Mark>)mark
{
  // default behavior
}

The visitMark: method should define any default behavior for any unknown Mark type or be used as a "catch-all" visitor method for any new node classes in the future (see the Note later in the chapter for a discussion on related issues). Your MarkRenderer doesn't implement any default behavior for Mark and leaves that method empty. Let's move forward to the next method, shown in Listing 15-4.

Example 15-4. Obtaining the Location, Size, and Color Information from the Node

- (void) visitDot:(Dot *)dot
{
  CGFloat x = [dot location].x;
  CGFloat y = [dot location].y;
  CGFloat frameSize = [dot size];
  CGRect frame = CGRectMake(x - frameSize / 2.0,
                            y - frameSize / 2.0,
                            frameSize,
                            frameSize);

  CGContextSetFillColorWithColor (context_,[[dot color] CGColor]);
  CGContextFillEllipseInRect(context_, frame);
}

When it visits a Dot node, it obtains the location, size, and color information from the node. Then it draws a filled circle in context at a particular location on the screen, as shown in Listing 15-5.

Example 15-5. Drawing a Filled Circle at a Particular Location

- (void) visitVertex:(Vertex *)vertex
{
  CGFloat x = [vertex location].x;
  eCGFloat y = [vertex location].y;

  if (shouldMoveContextToDot_)
  {
    CGContextMoveToPoint(context_, x, y);
    shouldMoveContextToDot_ = NO;
  }
  else
  {
    CGContextAddLineToPoint(context_, x, y);
  }
}

When it comes to drawing a Vertex, things are done differently. There are three basic parts for drawing a line in Quartz 2D: move the context to a point, add a line to a point, and stroke the whole path. The first two steps should be determined when visiting vertices. Use a BOOL value shouldMoveContextToDot to determine whether it is visiting the Vertex node in a Stroke composite. The path-stroking step is the final step to realize the stroke when the visitor is finished visiting all the vertices in it, as shown in Listing 15-6.

Example 15-6. Drawing a Vertex

- (void) visitStroke:(Stroke *)stroke
{
  CGContextSetStrokeColorWithColor (context_,[[stroke color] CGColor]);
  CGContextSetLineWidth(context_, [stroke size]);
  CGContextSetLineCap(context_, kCGLineCapRound);
  CGContextStrokePath(context_);
  shouldMoveContextToDot_ = YES;
}

@end

Then the visitStroke: method wraps up the stroke plotting process. The Stroke object determines attributes of a whole stroke, like color and line width. You also want to have the line ends appear rounded. The final function call of CGContextStrokePath() concludes the stroke drawing process.

Note

A notable drawback of the Visitor pattern is that a visitor is coupled with target classes. So if a visitor needs to support new classes (such as adding a new node type to the Mark family), the parent visitors as well as other subclasses need to be changed to reflect the new functionality. If you don't add new classes to your target class family often, you should be fine.

Because of unpredictable changes to the visitors in the future, it's a good idea to have a "catch-all" visit method for each visitor to support future target types. You have defined a visitMark: method in the MarkVisitor protocol (in Listing 15-1). The method should be able to cover any future new Mark types. However, it's just a stopgap solution. Should you need to add new nodes often, you may need to bite the bullet and change the visitor interfaces for supporting the new node types.

So far, you're done implementing your visitors for the Mark composite family. If no Mark family member accepts the visit from the visitors, your scheme won't work.

As mentioned previously, every node needs to have the acceptMarkVisitor: method that allows a visitor from any MarkVisitor. Each node type implements the same interface but allows a MarkVisitor to visit it differently. Let's take a look at the implementation of the acceptMarkVisitor: method in Dot in Listing 15-7.

Example 15-7. The Implementation of the acceptMarkVisitor: Method in Dot

- (void) acceptMarkVisitor:(id <MarkVisitor>)visitor
{
  [visitor visitDot:self];
}

The method tells the visitor to visit a Dot (self). Now look at the same method in Vertex in Listing 15-8.

Example 15-8. The Implementation of the acceptMarkVisitor: Method in Vertex

- (void) acceptMarkVisitor:(id <MarkVisitor>)visitor
{
  [visitor visitVertex:self];
}

The visit* message is almost the same as the one in Dot but this time it tells the visitor to visit a Vertex, which is referred as self.

Things look a bit different in Stroke because it is a composite class and its objects need to take care of their children. Its implementation for the same method is shown in Listing 15-9.

Example 15-9. The Implementation of the acceptMarkVisitor: Method in Stroke

- (void) acceptMarkVisitor:(id <MarkVisitor>)visitor
{
  for (id <Mark> dot in children_)
  {
    [dot acceptMarkVisitor:visitor];
  }

  [visitor visitStroke:self];
}

It loops through all of its children and sends each of them a message of acceptMarkVisitor:. The type of node at runtime will determine what kind of visit* method the visitor should use, as discussed Listings 15-7 and 15-8. Then it finally tells the visitor to visit itself with a visitStroke: message. In a tree traversal term, this is called post-ordered traversal. It means that all the child nodes in a tree will be visited before their parents. Why do children go first? You need to make sure all the Dots are processed before any finalized steps performed in the Stroke. Of course, you can define your own traversal strategy for the composite; it all depends on the problem. A composite should be the only entity that knows its own internal structure and traversal strategies. With an iterator (see the Iterator pattern in Chapter 4), clients can enumerate every node in a composite structure without knowing any of its internal representation. There are also pre-order and in-order traversals, but post-ordered traversals are more common in composite objects. It's because operations in composite objects are mostly related to consolidating results from child nodes before finalizing and returning a result from the composite node.

So now you have set up all the pipelines for your plumbers to come in. Let's put the rest together in CanvasView and roll everything out in Listing 15-10.

Example 15-10. The drawRect: Method of CanvasView

- (void)drawRect:(CGRect)rect
{
  // Drawing code
  CGContextRef context = UIGraphicsGetCurrentContext();

  // create a renderer visitor
  MarkRenderer *markRenderer = [[[MarkRenderer alloc] initWithCGContext:context]
                                                                    autorelease];
  // pass this renderer along the mark composite structure
  [mark_ acceptMarkVisitor:markRenderer];
}

CanvasView has been mentioned in the previous chapters. It is responsible for presenting the user-drawn strokes and dots. Any custom drawing algorithm is put in the drawRect: method of UIView. When the view needs to redraw or refresh its content, it will call this method to execute any custom drawing code in it. As usual, before you can draw anything, you need to get a current graphics context. Then you use that context to instantiate a MarkRenderer object. A CanvasView object has a private member variable mark_, which represents the whole abstract composite structure of user's strokes at runtime. CanvasView will render mark_ on the screen by passing the MarkRenderer as a visitor to it with a message of acceptMarkVisitor:. Then the rendering process will be propagated along the structure as the MarkRenderer object pays a visit to each node. No matter how complex a Mark object is, it only takes three lines of code to draw the whole thing on the screen. Isn't it amazing?

What Else Can A Visitor Do?

In this example, you extended the Mark family classes with a visitor that renders their node objects so they can be displayed on the screen. You could also add another visitor that, for example, applies an affine transformation (rotation, scaling, shear, and translation) to a Mark composite by visiting each node. Other possible operations include changing the style of the strokes by applying different styling visitors to a Mark composite object or creating a visitor that debugs the structure. Note that if you put these operations in the Mark interface, you need to change every node class as well. So implementing the Visitor pattern to the composite structure should usually be your last task due to the interface changes in the composite classes.

Can't I Just Use Categories Instead?

Sure, you can use categories to extend the behavior of any Mark family members. In Chapter 13, you learned that you could define a drawWithContext: method in each node to render it on the screen. But instead of defining the method in each node, you can factor it out in individual categories. Each category for each node type implements the same drawWithContext: method. For your example, you need three different categories for your Mark composite and leaf nodes versus just one visitor that can consolidate related algorithms for all nodes in a single place. Also, should you extend the nodes again, you will either need to modify the existing categories or create new ones for them. In terms of the efforts you put in extending the existing Mark interfaces and its node classes, using categories is not a whole lot different from modifying them directly. In many cases, it's not much better than using visitors.

Summary

The Visitor pattern is a very powerful approach to extend functionality of a composite structure. If a composite structure has well thought-out primitive operations and the structure won't be changed in the future, different visitors can access the composite structure the same way but for different purposes. The Visitor pattern lets you separate a composite structure from other related algorithms in other visitor classes with minimal modification possible.

In the next chapter, you are going to see a pattern that can also help extend the object's behavior by "decorating" it from outside.



[13] The original definition appeared in Design Patterns, by the"Gang of Four" (Addison-Wesley, 1994).

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

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