Chapter 22. Chain of Responsibility

The Chain of Responsibility pattern allows a number of classes to attempt to handle a request without any of them knowing about the capabilities of the other classes. It provides a loose coupling between these classes; the only common link is the request that is passed between them. The request is passed along until one of the classes can handle it.

One example of such a chain pattern is a Help system like the one shown in Figure 22-1. This is a simple application where different kinds of help could be useful, where every screen region of an application invites you to seek help but in which there are window background areas where more generic help is the only suitable result.

A simple application where different kinds of help could be useful

Figure 22-1. A simple application where different kinds of help could be useful

When you select an area for help, that visual control forwards its ID or name to the chain. Suppose you selected the “New” button. If the first module can handle the New button, it displays the help message. If not, it forwards the request to the next module. Eventually, the message is forwarded to an “All buttons” class that can display a general message about how buttons work. If there is no general button help, the message is forwarded to the general help module that tells you how the system works in general. If that doesn't exist, the message is lost, and no information is displayed. This is illustrated in Figure 22-2.

A simple Chain of Responsibility

Figure 22-2. A simple Chain of Responsibility

There are two significant points we can observe from this example: first, the chain is organized from most specific to most general, and second, there is no guarantee that the request will produce a response in all cases. We will see shortly that you can use the Observer pattern to provide a way for a number of classes to be notified of a change.

Applicability

The Chain of Responsibility is a good example of a pattern that helps keep knowledge separate of what each object in a program can do. In other words, it reduces the coupling between objects so that they can act independently. This also applies to the object that constitutes the main program and contains instances of the other objects. You will find this pattern helpful in the following situations.

  • There are several objects with similar methods that could be appropriate for the action the program is requesting. However, it is more appropriate for the objects to decide which one is to carry out the action than it is for you to build this decision into the calling code.

  • One of the objects may be most suitable, but you don't want to build in a series of if-else or switch statements to select a particular object.

  • There might be new objects that you want to add to the possible list of processing options while the program is executing.

  • There might be cases when more than one object will have to act on a request, and you don't want to build knowledge of these interactions into the calling program.

Sample Code

The help system we just described is a little involved for a first example. Instead, let's start with a simple visual command-interpreter program (Figure 22-3) that illustrates how the chain works. This program displays the results of typed-in commands. While this first case is constrained to keep the example code tractable, we'll see that this Chain of Responsibility pattern is commonly used for parsers and even compilers.

A simple visual command interpreter program that acts on one of four panels, depending on the command you type in.

Figure 22-3. A simple visual command interpreter program that acts on one of four panels, depending on the command you type in.

In this example, the commands can be any of the following.

  • Image filenames

  • General filenames

  • Color names

  • All other commands

In the first three cases, we can display a concrete result of the request, and in the fourth case, we can only display the request text itself.

In the preceding example system, we do the following.

  1. We type in “Mandrill” and see a display of the image Mandrill.jpg.

  2. Then we type in a filename, and that filename is displayed in the center list box.

  3. Next, we type in “blue,” and that color is displayed in the lower center panel.

Finally, if we type in anything that is neither a filename nor a color, that text is displayed in the final, right-hand list box. This is shown in Figure 22-4.

How the command chain works for the program in Figure 22-3

Figure 22-4. How the command chain works for the program in Figure 22-3

To write this simple chain of responsibility program, we start with an ab stract Chain class.

'Interface class Chain
'--------
Public Sub addChain(c As Chain)
End Sub
'--------
Public Sub sendToChain(mesg As String)
End Sub
'--------
Public Function getChain() As Chain
End Function
'--------
Public Sub setControl(c As Control)
End Sub
'--------
Public Function hasChain() As Boolean
End Function

The addChain method adds another class to the chain of classes. The getChain method returns the current class to which messages are being forwarded. These two methods allow us to modify the chain dynamically and add additional classes in the middle of an existing chain. The sendToChain method forwards a message to the next object in the chain.

Our main program assembles the Chain classes and sets a reference to a control into each of them. We start with the ImgChain class, which takes the message string and looks for a .jpg file of that name. If it finds one, it displays it in the Image control, and if not, it sends the command on to the next element in the chain.

'Class ImgChain
Implements Chain
Private chn As Chain
Private hasLink As Boolean
Private fl As File
Private img As Image
'--------
Private Sub Chain_addChain(c As Chain)
  Set chn = c
  hasLink = True
End Sub
'--------
Private Function Chain_getChain() As Chain
  Set Chain_getChain = chn
End Function
'--------
Private Function Chain_hasChain() As Boolean
  Chain_hasChain = hasLink
End Function
'--------
Private Sub Chain_sendToChain(mesg As String)
  fl.setFilename App.Path & "" & mesg & ".jpg"
  If fl.exists Then
    img.Picture = LoadPicture(fl.getFilename)
  Else
    chn.sendToChain mesg
  End If
End Sub
'--------
Private Sub Chain_setControl(c As Control)
  Set img = c
End Sub
'--------
Private Sub Class_Initialize()
  hasLink = False
  Set fl = New File
End Sub

In a similar fashion, the ColorChain class simply interprets the message as a color name and displays it if it can. This example only interprets three colors, but you could implement any number.

Private Sub Chain_sendToChain(mesg As String)
Dim colr As Long, found As Boolean
  found = True
  Select Case LCase(mesg) 'look for a color name
    Case "red"
      colr = vbRed
    Case "blue"
      colr = vbBlue
    Case "green"
      colr = vbGreen
    Case Else
    'if not found send it on
      If hasLink Then
        chn.sendToChain mesg
        found = False   'not found
      End If
  End Select
  'if found change the color
  If found Then
    pc.BackColor = colr
  End If
End Sub

The List Boxes

Both the file list and the list of unrecognized commands are ListBoxes. If the message matches part of a filename, the filename is displayed in the fileList box, and if not, the message is sent on to the NoComd chain element.

Private Sub Chain_sendToChain(mesg As String)
Dim fls As String
  ChDir App.Path    'current directory
  fls = Dir(mesg & "*.*")   'look for match
  If Len(fls) > 0 Then
    lst.AddItem fls     'add it to list
  Else
    If hasLink Then
      chn.sendToChain mesg  'or send to Nocmd class
    End If
  End If
End Sub

The NoCmd Chain class is very similar. It, however, has no class to which to send data.

'Class NoCmd
Implements Chain
Private lst As ListBox
'--------
Private Sub Chain_addChain(c As Chain)
End Sub
'--------
Private Function Chain_getChain() As Chain
End Function
'--------
Private Function Chain_hasChain() As Boolean
  Chain_hasChain = False
End Function
'--------
Private Sub Chain_sendToChain(mesg As String)
  lst.AddItem mesg
End Sub
'--------
Private Sub Chain_setControl(c As Control)
  Set lst = c
End Sub

Finally, we link these classes together in the Form_Load routine to create the Chain.

'set up Chain of Responsibility
Dim colr As Chain
Dim fls As Chain
Dim nocom As Chain
  'Image chain
  Set chn = New ImgChain
  chn.setControl imgJpg
  'Color chain
  Set colr = New ColorChain
  colr.setControl pcColor
  chn.addChain colr
  'File chain
  Set fls = New FileChain
  fls.setControl lsFiles
  colr.addChain fls
  'No Command
  Set nocom = New NoCmd
  nocom.setControl lsNocomds
  fls.addChain nocom

You can see the relationship between these classes in the UML diagram in Figure 22-5.

The class strcuture of the Chain of Responsibility program

Figure 22-5. The class strcuture of the Chain of Responsibility program

The Sender class is the initial class that implements the Chain interface. It receives the button clicks and obtains the text from the text field. It passes the command on to the Imager class, the FileList class, the ColorImage class, and finally to the RestList class. Note that FileList is a subclass of RestList and implements the Chain interface because the parent RestList class does.

Programming a Help System

As we noted at the beginning of this discussion, help systems provide good examples of how the Chain of Responsibility pattern can be used. Now that we've outlined a way to write such chains, we'll consider a help system for a window with several controls. The program (Figure 22-6) pops up a help dialog message when the user presses the F1 (help) key. The message depends on which control is selected when the F1 key is pressed.

A simple help demonstration

Figure 22-6. A simple help demonstration

In this example, the user has selected the Quit key, which does not have a specific help message associated with it. Instead, the chain forwards the help request to a general button help object that displays the message shown on the right.

To write this help chain system, we begin with a general Chain interface class that has empty implementations of all of the Chain interface methods.

'Interface class Chain
'--------
Public Sub addChain(c As Chain)
End Sub
'--------
Public Sub sendToChain(c As Control)
End Sub
'--------
Public Function getChain() As Chain
End Function
'--------
Public Function hasChain() As Boolean

End Function

Note that this chain does not need to have a copy of a reference to any kind of control. This is passed in using the sendToChain method.

Then you need to create specific classes for each of the help message categories you want to produce. As we illustrated earlier, we want help messages for the following.

  • The New button

  • The File button

  • A general button

  • A general visual control (covering the check boxes)

In VB, one control will always have the focus, and thus we don't need a case for the Window itself. Therefore, we write the above four classes and combine them into a chain as follows.

Private Sub Form_Load()
Dim butc As Chain
Dim filc As Chain
Dim cchn As Chain
'create chain of responsibility
  Set chn = New NewChain
  Set filc = New FileChain
  Set butc = New ButtonChain
  Set cchn = New ControlChain
  chn.addChain filc
  filc.addChain butc
  butc.addChain cchn
End Sub

Receiving the Help Command

Now we need to assign keyboard listeners to look for the F1 keypress. At first, you might think we need five such listeners—for the three buttons and the two check boxes. However, we can make control arrays of the buttons and the check boxes, and then we need to listend for a KeyDown event in two places, and both call the same method.

Private chn As Chain
'--------
Private Sub btNew_KeyDown(Index As Integer, keyCode As Integer, _
            Shift As Integer)
  callChain btNew(Index), keyCode
End Sub
'--------
Private Sub callChain(c As Control, keyCode As Integer)
If keyCode = vbKeyF1 Then   'respond to F1 only
  chn.sendToChain c
End If
End Sub

For the File button, the chain class is implemented as follows.

Implements Chain
Private chn As Chain
Private hasLink As Boolean
'--------
Private Sub Chain_addChain(c As Chain)
  Set chn = c
  hasLink = True
End Sub
'--------
Private Function Chain_getChain() As Chain
  Set Chain_getChain = chn
End Function
'--------
Private Function Chain_hasChain() As Boolean
  Chain_hasChain = hasLink
End Function
'--------
Private Sub Chain_sendToChain(c As Control)
  If c.Caption = "File" Then
    MsgBox "Use to open a file"
  Else
    If hasLink Then
      chn.sendToChain c
    End If
  End If
End Sub
'--------
Private Sub Class_Initialize()
  hasLink = False
End Sub

At first you might think that you could just as easily have made a separate KeyDown event method for each of the controls on the form, instead of having only a couple of such events and sending them all through the same chain. And, in fact, that is how VB programs are usually written. The advantage to this Chain of Responsibility approach is that you can decide the order in which controls are checked for membership in various classes and control and easily change this order within your program. This provides a considerably more versatile and flexible system than if each control called its own event method. We show the complete class diagram for this help system in Figure 22-7.

The class diagram for the Help system

Figure 22-7. The class diagram for the Help system

A Chain or a Tree?

Of course, a Chain of Responsibility does not have to be linear. The Smalltalk Companion suggests that it is more generally a tree structure with a number of specific entry points all pointing upward to the most general node, as shown in Figure 22-8.

The chain of responsibility implemented as a tree structure

Figure 22-8. The chain of responsibility implemented as a tree structure

However, this sort of structure seems to imply that each button, or its handler, knows where to enter the chain. This can complicate the design in some cases and may preclude the need for the chain at all.

Another way of handling a tree-like structure is to have a single entry point that branches to the specific button, menu, or other widget types and then “unbranches,” as previously, to more general help cases. There is little reason for that complexity—you could align the classes into a single chain, starting at the bottom, and going left to right and up a row at a time until the entire system had been traversed, as shown in Figure 22-9.

The same chain of responsibility implemented as a linear chain

Figure 22-9. The same chain of responsibility implemented as a linear chain

Chain of Responsibility in VB.NET

We can implement the Chain of Responsibility in VB7 in a very similar manner. However, it is convenient to make the Chain class an abstract class instead of an interface. Remember that an abstract class has one or more methods that must be implemented in the derived classes. We mark this method as MustOverride and mark the class as MustInherit, as we illustrate here.

Public MustInherit Class Chain
    Protected chn As Chain
    Private hasLink As Boolean
    '-----
    Public Sub New()
        hasLink = False
    End Sub
    '-----
    Public Sub addChain(ByVal c As chain)
        chn = c
        haslink = True  'mark as available
    End Sub
    '-----
    'will fill this in in derived classes
    Public MustOverride Sub sendToChain(_
                        ByVal mesg As String)
    '-----
    Public Function getChain() As chain
        Return chn
    End Function
    '-----
    Public Function hasChain() As Boolean
        Return hasLink
    End Function
End Class

Then we can easily derive classes, and we only need to implement the sendToChain method. For example, here is the FileChain class.

Public Class FileChain
    Inherits Chain
    Private flist As ListBox
    '-----
    Public Sub new(ByVal lbox As ListBox)
        MyBase.new()
        flist = lbox
    End Sub
    '-----
    Public Overrides Sub sendToChain( _
                         ByVal mesg As String)
        Dim fname As String
        Dim files As File()
        fname = mesg & "*.*"
        files = Directory.GetFilesInDirectory( _
            Directory.CurrentDirectory, fname)
        'add them all to the listbox
        If files.Length > 0 Then
            Dim i As Integer
            For i = 0 To files.Length - 1
                flist.Items.Add(files(i).Name)
            Next
        Else
            If haschain Then
                chn.sendToChain(mesg)
            End If
        End If
    End Sub
End Class

Since we initialize the chains in their constructors, the code in the form constructor that sets them up is a little simpler.

Private Sub setUpChain()
        Dim clrchain As New ColorChain(pnlcolor)
        Dim flchain As New FileChain(lsfiles)
        Dim nochain As New NoCmd(lsnocomd)

        chn = New ImageChain(picImage)
        chn.addChain(clrchain)
        clrchain.addChain(flchain)
        flchain.addChain(nochain)
    End Sub

The final display is shown in Figure 22-10.

The VB7 version of the image Chain of Responsibility

Figure 22-10. The VB7 version of the image Chain of Responsibility

Kinds of Requests

The request or message passed along the Chain of Responsibility may well be a great deal more complicated than just the string or Control that we conveniently used on these examples. The information could include various data types or a complete object with a number of methods. Since various classes along the chain may use different properties of such a request object, you might end up designing an abstract Request type and any number of derived classes with additional methods.

Examples in VB

Under the covers, VB form windows receive various events, such as MouseMove, and then forward them to the controls the form contains. However, only the final control ever receives the message in VB, whereas in some other languages, each containing control does as well. This is a clear implementation of Chain of Responsibility pattern. We could also argue that, in general, the VB.NET class inheritance structure itself exemplifies this pattern. If you call for a method to be executed in a deeply derived class, that method is passed up the inheritance chain until the first parent class containing that method is found. The fact that further parents contain other implementations of that method does not come into play.

We will also see that the Chain of Responsibility is ideal for implementing Interpreters and use one in the Interpreter pattern we discuss later.

Consequences of the Chain of Responsibility

  1. The main purpose for this pattern, like a number of others, is to reduce coupling between objects. An object only needs to know how to forward the request to other objects.

  2. Each VB object in the chain is self-contained. It knows nothing of the others and only need decide whether it can satisfy the request. This makes both writing each one and constructing the chain very easy.

  3. You can decide whether the final object in the chain handles all requests it receives in some default fashion or just discards them. However, you do have to know which object will be last in the chain for this to be effective.

  4. Finally, since VB cannot provide multiple inheritance, the basic Chain class needs to be an interface rather than an abstract class so the individual objects can inherit from another useful hierarchy, as we did here by deriving them all from Control. This disadvantage of this approach is that you often have to implement the linking, sending, and forwarding code in each module separately or, as we did here, by subclassing a concrete class that implements the Chain interface.

Thought Question

Thought Question

Suggest how you might use a Chain of Responsibility to implement an e-mail filter.

Programs on the CD-ROM

ChainHelpChain VB6 program showing how a help system can be implemented
ChainPicChain VB6 chain of file and image displays
ChainVBNetChain VB7 chain of file and image displays
..................Content has been hidden....................

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