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.
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.
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.
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.
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.
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.
We type in “Mandrill” and see a display of the image Mandrill.jpg.
Then we type in a filename, and that filename is displayed in the center list box.
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.
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
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 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.
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.
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
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
Suggest how you might use a Chain of Responsibility to implement an e-mail filter.