| Data Acquisition and Online Analysis at the NSCL | ||
|---|---|---|
| Prev | Chapter 5. Best Practices for Building Software | Next |
Most experimental setups at the NSCL have a relatively long lifetime. They are used by a diverse set of users and evolve over time. Specific experiments will have special requirements that must be accomodated on a a one-of basis without compromising the long-term maintainability of the software. In this section we will look at:
Managing your source code tree
Build structures for your software.
Release Management.
Some patterns for managing special requirements.
In this section we will talk about how to place your source code into a CVS respository. Once in a CVS repository, you can track changes to the code, indicate release levels and create branches from the main line of development.
With CVS you will be able to go back in time to any revision of any file in the repository. CVS does this by storing the changes required to move from one revision to another.
We will cover the following topics:
CVS concepts.
Creating a CVS source code repository and loading your current source code into the repository.
Checking out a sandbox from the repository.
Comitting changes to the repository.
Marking a stable release point in the repository (tags).
Creating special versions via CVS branching.
CVS, or Concurrent Version System is a code management system. Software source code placed under the management of a CVS repository can be safely modified, in that you can always return to previous working versions of the software after you have broken the code.
The key concepts for CVS are:
Repositories
Working Directories (sandboxes)
Source file revisions
Tags
Branches
Repositories. A CVS respository is a directory tree. The files in the directory tree consist of managed source files and control files.
A managed source file is a source file that has been added to the repository by you. It contains the current source code of the file as well as information that allows you to transform the file to any other revision, along with logging information you supply to describe why you have changed the file.
Managed source files can also contain tagging information. A tag is a way to say that a specific file revision is special in some way. Special enough to be given a name.
You should always work with a repository directory tree through CVS commands. Never directly edit repository files. Instead, check out a working version of the software, edit the appropriate file(s), and commit the changes.
Control files are special managed source files that CVS creates. Comitting changes to these files allows you to tailor how CVS operates. A discussion of control files is beyond the scope of this document. See the CVS resources for more information.
While CVS can manage almost any file, you should manage sources not build products. Your build system should be able to create a build of the software from a working version of your repository.
Working directories. As you know you should not directly edit files in the repository. That's CVS's job. You will normally Check out a repository, constructing a new directory tree. YOu wi8ll then edit the files in this working directory, and eventually commit them to the repository.
Working directories are also sometimes called sandboxes. Just like the children's sandbox, Working directories are a safe place to play with your code to try out change. If you don't like what you've done you can always just delete the working directory, and create a new one, or revert your code to some older revision.
Revisions. When you commit a change to a file, CVS creates a new revision. A revision is a version of the file that can be retrieved from the repository.
Most of the time, you will be retrieving the head revision. The head revision is the most recently committed set of changes. Sometimes you will have to retrieve a specific revision, to stop going down a blind development alley.
Revisions have log messages associated with them. The purpose of these log messages is to describe why the revision was created. When you create a new revision of a a file, CVS will prompt you for a log message, and pop you into the editor of your choice so that you can create this log.
Revisions have a revision number associated with them. Revision nunbers are a set of period separated numbers. For example, revision 1.63 of a file is the 63'd revision of the main line of the file. Furthermore, youcan name a set of revisions via tagging.
Tags. A tag is a name that can be associated with a file revision. Usually you will not use an isolated tag like this. You will normally tag the all the HEAD revisions of a working directory.
Tags are used for two reasons; most commonly you tag a set of related revisions to mark them as a good point to return to. Suppose, for example, you have just release version 1.2 of your software. You might tag the revisions that make up that version as V1_2.
Tags are also used to create branches. If you think of the revisions of the main line of development as the trunk of a tree, there may be reasons that you will want to create a branch that can be developed independently of this trunk. For example, suppose you have a run comming up with special requirements that involve editing your source code, but these modifications don't represent something of general use to your software. You could create a branch and work on this special need in the branch.
Another example of a branch would be that you have just released V1.2. Someone using version 1.1 discovers an error that is not present in 1.2 You can check out the 1.1 sources, create a branch and fix the problem in that branch. committing changes on the branch will not affect checkouts of V1.2. V1.1 can be independently maintained without necessarily modifying V1.2. This method is used by me to maintain multiple releases of the nscldaq and nsclspectcl software.
Let's assume that we have created a very simple experiment. An ls -R of the top level directory produces:
Example 5-18. CVS example files.
.:
Makefile Readout SpecTcl
./Readout:
CTraditionalEventSegment.cpp Makefile MyEventSegment.h
CTraditionalScalerReadout.cpp MyEventSegment.cpp Skeleton.cpp
./SpecTcl:
Makefile MyEventProcessor.cpp MyEventProcessor.h MySpecTclApp.cpp
![]() | Disclaimer |
|---|---|
Do not read into this an intent to say that this is how you should organize your files |
We want to create a CVS repository up in ../codeRepository and place this software under CVS code management. This requires the following commands.
Example 5-19. Placing our sources into a CVS repository
mkdir ../codeRepository
cvs -d `pwd`/../codeRepository init
cvs -d `pwd`/../codeRepository import myDaq NSCLMe initial
After entering the last line you will be placed in an editor so that
you can log why you are doing this import. By default CVS will
let you edit the log message using vi. Set
the CVSEDITOR environment variable to use whatever
editor you prefer.
The parameters in the cvs import command are a project name (myDaq), a vendor (NSCLMe), and an initial tag for the imported files (initial).
Your code base is now under CVS management in the repository ../codeRepository
Once your code has been imported to a CVS respository, you must check it out to create a Working Directory, or sandbox. This is done as follows:
This will create a directory tree named myDaq which is a close mimic to the directory you imported. You should make changes to your software in the myDaq directory tree. In the next section we will show how to commit changes. For now, suppose that we created a new file. The new file is Readout/newFile.cpp.
To add this file, while your default director is myDaq:
CVS will warn you that you still must commit the file to the repository.After you have made changes to a file in a sandbox, you will want to make CVS know that you have changed the files. CVS will then create a new revision for that file and provide sufficient information so that you can backtrack to an existing file.
You can specify that you want to commit a single file or any list of files. If you don't specify which files to commit, CVS will commit all the modified files in the directory tree starting with your current working directory. CVS will also prompt you for a log message. Usually, you will not commit a bunch of unrelated files at the same time. If you do, the log message will be common rather than having a new message for each committed file.
To commit our new file REadout/newFile.cpp:
cvs will prompt for a log file and, after the file(s) are committed will print the new revision numbers of the files. For this case we got the text /user/fox/Wincluster/DAQDocs/bluebook/codeRepository/myDaq/Readout/newFile.cpp,v <-- Readout/newFile.cpp initial revision: 1.1
Similarly, after you have edited a file that is already in the repository, you can commit it using the cvs commit command.
The strongest reason to use CVS is to be able to roll back your software to a known working state. Usuall you are not going to do this a file at a time. You will usually want to recover a known working version. You can use tags to group a set of related revisions together and give it a name. In this section we will show how to create a tag and how to check out a tagged set of revisions.
Ok, suppose our project is now at version 1.0. Go into the top level of a sandbox and:
CVS will list the set of files it has attached this tag to. Now lets add a few lines to the top level Makefile and commit it. Suppose that the next thing that happens is that you need to get a new build done on your software now but the modifications you just commited left the Makefile unusable.No problem, just create a sandbox from the tage Version1_0, where things used to work just fine:
Example 5-24. Checking out a tag
cd ..
mkdir oops
cd oops
cvs -d `pwd`/codeRepository co -r Version1_0 myDaq
CVS has served as a time machine to look back into our sources as of when everthing worked. Now we can do that build.. and go back to our other sandbox to fix the problem we introduced with the Makefile later. Note that our checkout of Version1_0 in this way is a readonly checkout. We really can't commit modifications to it. See, however the next section which discusses branching.
Related to tagging is branching. A branch is a tag that indicates that you are forking off development in a direction independent of the main line. For example, you've been working on Version1_1 and have made quite a lot of changes. A user tells you that they've found a problem with Version1_0. How do you fix this version without disrupting your work on Version1_1?
First, checkout Version1_0 as before. Next create a branch tag, let's call this Version1_0Maint to indicate that it will be a branch for fixing problems with Version 1.0 while we are working on Version 1.2. cd to your Version1_0 checkout and:
cvs tag -b Version1_0Maint
The -b indicates this tag is the base of a branch. When you check out this
branch you'll be able to commit changes but the changes will be 'off on
the side' in this branch.
Build structures for software are very important. You may need to export your software outside your lab. Bad build structures can make this difficult or impossible. A good buid structure should
Make very clear the boundaries between your software and software you depend on.
Not hard code directories of software you depend on. People attempting to import your software to their institutions may need to change these directories.
Make all internal directory references relatives so that your software can be build from any directory.
Support installation of the products of the build in any target directory.
Build systems should be easy to extend and modify as your project evolves.
I also like to create build systems that can have as a minimum the following targets:
Compiles the software in place.
Removes all products of compilationso that the next build starts from fresh source code.
Install the software in some designated target
Builds a tarball that has only what's needed to build the software.
Builds any test programs that can be run against an installation of software and then runs them
In this section we'll study how to do this using simple Makefiles. For more complext problems you amy want to consider using the gnu autotools to write configure and write your installer. Autotools are beyond the scope of this document, however some pointers are provided in the resources section of this chapter.
One very strong suggestion. Make source code available, but don't make handing out the source code and Makefiles the way you deploy your software for an experiment.
People who have chosen to deploy in source code have found that this is a maintenance nightmare. Since each group winds up with their own copies of your source code, and since each group has chosen to tailor it for their experiments by modifying your source code directly, and then using that modified source code as the basis for their own deployments, it is never possible to ensure that defects get fixed. If an error gets out in the field, you'll keep fixing it, and fixing it and fixing it.
In the section on deploying your software we will touch on this point again in greater detail.
A full discussion of Makefiles is beyond the scope of this document. If you are not familiar with make an Makefiles, see the resources section of this chapter for pointers to Make and Makefile documentation.
Simple software systems can be built using Makefiles. Make liberal use of Makefile Macros and command line macros to get stuff done. Lets look at the skeleton Makefile for SpecTcl and see how this works.
Example 5-25. SpecTcl's skeleton Makefile
INSTDIR=/scratch/fox/SpecTcl/3.0 include $(INSTDIR)/etc/SpecTcl_Makefile.include# If you have any switches that need to be added to the default c++ compilation # rules, add them to the definition below: USERCXXFLAGS=
# If you have any switches you need to add to the default c compilation rules, # add them to the defintion below: USERCCFLAGS=$(USERCXXFLAGS) # If you have any switches you need to add to the link add them below: USERLDFLAGS= # # Append your objects to the definitions below: # OBJECTS=MySpecTclApp.o MyEventProcessor.o
# # Finally the makefile targets. # SpecTcl: $(OBJECTS) $(CXXLD) -o SpecTcl $(OBJECTS) $(USERLDFLAGS) \
$(LDFLAGS) clean: rm -f $(OBJECTS) SpecTcl depend: makedepend $(USERCXXFLAGS) *.cpp *.c help: echo "make - Build customized SpecTcl" echo "make clean - Remove objects from previous builds" echo "make depend - Add dependencies to the Makefile. "


USERCXXFLAGS,
in most cases the Makefile scales by simply adding the names of the
objects that have to be built.

The Makefile we have looked at gets most of its configuration done whe the SpecTcl project iself is configured and built. Suppose our project now consists of SpecTcl, a bunch of SpecTcl tcl scripts and that we want to install all of these in some nice place. SpecTcl's executable will go in somewhere/bin an the scripts in somewhere/tcl. Let's also assume we'll have a document named myspectcl.doc which goes in somplace/docs
Example 5-26. Creating an install target
SCRIPTS=thisscript.tcl thatscript.tcl theothersript.tcl
DOCS=myspectcl.doc
install: SpecTcl $(SCRIPTS) $(DOCS)
mkdir -p $(DEST)/bin $(DEST)/tcl $(DEST)/docs
install -m 0755 SpecTcl $(DEST)/bin
for f in $(SCRIPTS); do \
install $$f $(DEST)/tcl; \
done
for f in $(DOCS); do \
install $$f $(DEST)/docs; \
done
The install target should be invoked with:
make install DEST=target
where target is the top level directory in
which you want your software installed. This is an example of defining
a Make macro from the make command.
Release management is the process of making a new version of software available. New releases of software come out for several reasons:
Additional functionality is being introduced.
Defects have been repaired.
Software is adjusting to changes in software it depends on (e.g. you depend on software that has undergone a new release and this requires changes in your code).
At the NSCL release management is complicated by the fact that users of existing software, based on earlier releases of your software won't want to update their software if their existing software works perfectly well. In managing releases for NSCLDAQ, and NSCLSpecTcl, I have therefore taken an approach that I encourage you to take if you have similar issues: Maintain installations of several versions of your software simultaneously.
The SpecTcl installations at the NSCL show clearly how this works. SpecTcl is installed /usr/opt/spectcl/version where version identifies the version of the software. For example, a listing of /usr/opt/spectcl at the NSCL shows:
drwxrwxr-x 11 fox manager 384 May 23 09:33 2.1
drwxrwxr-x 12 fox manager 408 Sep 7 07:58 2.2
drwxrwxr-x 12 fox manager 408 May 25 13:09 3.0-gcc2.95
drwxrwxr-x 13 fox fox 432 Jul 13 17:16 3.0-gcc3.x
drwxrwxr-x 13 fox manager 456 Jun 8 13:04 3.1
lrwxrwxrwx 1 fox fox 10 Jan 6 2006 current -> 3.0-gcc3.x
Where there are two versions of SpecTcl-3.0, one compiled on gcc-2.95 and another on gcc-3.3. The current symbolic link identifies the version that I feel users should use for new development. 3.1 is a pre-release and 2.1 and 2.2 have been kept for compatibility with users that have not upgraded to newer SpecTcls.
The actual version numbers I use are of the form major.minor-editlevel where major the same pretty well ensures sourcd code compatility. major and minor the same ensures binary compatibility. A change inthe major version indicates a potential for source code changes to make use of it.
What should I 'install'. I have written in a previous section that I think it is a bad idea to install or deploy source code. Your installer should, instead install libraries and header files. By convention, if you are installing in some directory, the libraries belong in the lib subdirectory and headers in the include subdirectory. If there are configuration files that are static they should go in your etc subdirectory as should all Make include files. If you have programs or scripts that look like programs, they belong in bin. Finally scripts belong in scripts or Scripts, Tcl Packages in the TclLibs subdirectory tree.
With the production readout's event segments, SpecTcl's event processors, and some tricks you can play with readout classic, there's no true need for the user to modify your code, except in very rare circumstances. Installing libraries, rather than passing out source code lets you control the maintenance of your software rather than letting it get out of control.
All experiments have special requirements. These requirements make the release of facility support code as a set of libraries challenging. Special requirements are probably the prime motivator that support personnel have in releasing their support software as modifiable source code.
It is possible to build libraries that can be extended by the user to cover their own special requirements. This is done by principally keeping in mind the Open/Closed rule. This rule states that:
Simply put, build your classes so that you don't need to modify them in order to add or modify behavior. Two examples of successful applications of the Open/Closed rule are the NSCL DAQ readout framework(s), and SpecTcl. These are released as libraries and headers along with some skeleton code that makes it easier to create applications that meet your needs without modifying the underlying libraries.Classes should be built to be closed with respect to modification but open with respect to extension
Some tools for achieving this in C++ are to:
Use virtual memeber functions to implement any functionality within your classes that may vary. When you do this it is important that 'upper levels' of your software are always manipulating pointers or references to objects from these classes, and that you provide a mechanism for the user to substitute objects they have written derived from your classes for your objects.
Use configuration files to set up your software so that it can dynamically work on sub or supersets of your 'standard configuration'.
Make use of some of the behavioral patterns in the book [Gamma et al. "Design Patterns Elements of Reusable Object-Oriented Software]. This books contains many patterns of interacting objects that can be used to achieve extensible software.
I want to complete this section with two subsections that demonstrate the first and last of these tools. These demonstrations will be done within a meaningful context. The context for both of these examples will be a Readout program for a facility supported device device. The code fragments given will be incomplete for the sake of clarity and brevity.
Suppose we have a facility device called nsclDevice. We have an event segment that reads this device. The device itself is composed of several 'segments' that are pretty much identical. A user is bringing additional detectors to extend some (but not all) of the segments of this detector. They want the data from these additional detectors to appear in-line with the data from the nscl supported device for clarity.
We will need to have written our software so that the following can be done:
The read out of each detector segment can be extended without modifying our source code.
The extended readout can be substituted for the actual readout of each segment at run time.
Let's assume that we have created an event segment class called:
CReadNSCLDevice. To add this supported device
to the readout of an experiment, the user modifies the Makefile for the
Production Readout skeleton to include our header file directories in the
header file search path, and links in the library that implements our
event segment. They then modify
CMyExperiment::SetupReadout in the production readout
skeleton to create and add an instance of this event segment to the
event segments maintained by the production readout sooftware.
The problem we must solve is how to write our CReadNSCLDevice so that the user can extend the readout of individual segments of our detector without having access to the source code of the readout. Since all of the segments of the standard detector system are identical, we'll use a collection of reader objects for each detector to read the segments.
The interface of an CEventSegment
perfectly matches our needs.
Furthermore, this class's operational methods (Initialize,
Read, and Clear) are already
virtual members, and can therefore be overridden.
We'll call the class that implements the
code for these objects CNSCLDeviceSegmentReader.
Bits of our event segment header might look like this:
Example 5-27. CReadNSCLDevice header code fragments
#define SEGMENT_COUNT ... // number of detector segments.
class CReadNSCLDevice : public CEventSegment
{
private:
CNSCLDeviceSegmentReader* m_SegmentReaders[SEGMENT_COUNT];
...
public:
...
CNSCLDeviceSegmentReader* getSegment(int i);
void replaceSegment(int i, CNSCLDeviceSegmentReader* pNewSegment);
};

m_SegmentReaders is an array of pointers to
CNSCLDeviceSegmentReader
objects. At construction time, it gets filled in
with pointers to 'standard' segment readers.

getSegment allows the user to get a
pointer to the reader for one of the segments. When the user
is constructing subtitutes for the segment that include the additional
detector, it will need to initialize its base class. To do this
properly, we'll write the CNSCLDeviceSegmentReaders to provide
functions to allow the parameters that distinguish each
object from every other object to be fetched.

replaceSegment allows an individual
segment reader to be replaced by a new segment reader. We will
use this to substitute the extended segment readers for the standard
ones.
Suppose that the CNSCLDeviceSegmentReader can be characterized by a segment number, that is it just uses the segment number to look up all other configuration information in some configuration array. Suppose further, that each segment of the detector encapsulates itself in a subsegment, whose id is computed from the segment id as well. We want to write CNSCLDeviceSegmentReader so that it can be extended with experiment specific data that may or may not live in this subsegment.
We do this by breaking up the Read member function into three actions that are represented by three member functions:
openSubpacketWrites the header part of the subpacket, and saves a pointer to the size so that it can be filled in later.
readSubpacketBodyReads the body of the subpacket.
closeSubpacketCloses the subpacket by writing the size of the packet to the saved size word.
This makes the key pieces of the header for
CNSCLDeviceSegmentReader
look like the following example:
Example 5-28. Key parts of the CNSCLDeviceSegmentReader header
class CNSCLDeviceSegmentReader : public CEventSegment
{
...
public:
CNSCLDeviceSegmentReader(int segmentNumber);
...
public:
virtual DAQWordBufferPtr& Read(DAQWordBufferPtr& buffer);
...
protected:
DAQWordBufferPtr& openSubpacket(DAQWordBufferPtr& buffer);
DAQWordBufferPtr& readSubpacketBody(DAQWordBufferPtr& buffer);
DAQWordBufferPtr& closeSubpacket(DAQWordBufferPtr& buffer);
};


Read is a virtual function in the
interface defined by the CEventSegment
class. As a virtual function, it can be overridden by subclasses
in a polymorphic manner. This is the key to this extension pattern.
The remainder of this extension pattern is a matter of implementing
this function in a way that its logical pieces are available to
subclasses.

openSubpacket
member will put the subpacket header into the buffer and remember
its location so that the word count can be computed and filled in
when the packet is completely read. The return value is a pointer
to the body of the packet. Since this and the next two functions
described are declared protected they can only be called by functions
within the CNSCLDeviceSegmentReader or
from classes that inherit from it.

readSubpacketBody reads the data from
the detector digizers into the storage pointed to by its parameter.
The return value is a pointer to the next piece of storage
beyond the packet
body.

closeSubpacket is passed a pointer to the
word of storage just past the subpacket for this segment. It uses
the pointer saved by openSubpacket to
compute and fill in the word count of the subpacket header.
Its return value is a pointer to the first free word of the buffer
following the subpacket.
While it is possible to write the experiment specific subclass from this header
alone, it is instructive to show the implementation of
CNSCLDeviceSegmentReader::Read:
Example 5-29. Implementation of CNSCLDeviceSegmentReader::Read
DAQWordBufferPtr&
CNSCLDeviceSegmentReader::Read(DAQWordBufferPtr& buffer)
{
buffer = openSubpacket(buffer);
buffer = readSubpacketBody(buffer);
return = closSubpacket(buffer);
}
Now let's look at the key parts of the header to the class that will be
written to add to the data for each segment. We will then look at two
implementations of the Read function for that class;
one which tucks its data into the subpacket, and one which puts is data
at the end of the subpacket (but not included within). We will call this
class CExtendedSegment, and assume that it too can
be constructed given only the segment number.
Example 5-30. Key parts of the CExtendedSegment header.
class CExtendedSegment : public CNSCLDeviceSegmentReader
{
...
public:
CExtendedSegment(int segmentNumber);
...
public:
virtual DAQWordBufferPtr& Read(DAQWordBufferPtr& buffer);
...
protected:
DAQWordBufferPtr& readExtraStuff(DAQWordBufferPtr& buffer);
};

CNSCLDeviceSegmentReader. Even if it
does not need the segment number itself, it will need to pass
it on to the base class constructor.

Read polymorphically.
This means that if we replace an element of
CNSCLDevice::m_SegmentReaders
with a pionter to a CExteneded pointer
calls through that element for Read
will invoke CExtendedSegment

Read function.
Second, this makes it easier to flip the code back and forth
between our data living in or out of the subpacket. Finally,
it allows this class to also be subclassed
and handled in a similar manner.
Now we'll have a
CMyExperiment::SetupReadout
that might look like this:
Example 5-31. CMyExperiment::SetupReadout for the extended
supported device
{
...
CNSCLDevice* pSupportedDevice = new CNSCLDevice;
for (it i =0; i < replacedSegments.size(); i++) {
pSupportedDevice->replaceSegment(replacedSegments[i],
new CExtendedSegment(replacedSegments[i]));
}
rExperiment.AddEventSegment(pSupportedDevice);
Let's look at an implementation of the
CExtendedSegment::Read
function that puts its data into the subpacket:
Example 5-32. CExtendedSegment::Read
implemented to put data into the subpacket
DAQWordBufferPtr&
CExtendedSegment::Read(DAQWordBufferPtr& buffer)
{
buffer = openSubpacket(buffer);
buffer = readSubpacketBody(buffer);
buffer = readExtraStuff(buffer);
return closeSubpacket(buffer);
}
While an implementation that places our data after the subpacket would be simply:
Suppose that in managing your device, you have determined that adding detectors to standard segments will be a fairly common activity, and that you want to have a bit more control over how the data for each segment is formatted. For example, you want to put all of the data from each segment into a single overarching subpacket and the data from each chunk of the segment (the standard piece, and any add on pieces) into sub-subpackets of this outer subpacket. For example:
Figure 5-1. Structure of a segment
+----------------------------+ \
| segment size | |
+----------------------------+ |
| Segment id | |
+----------------------------+ |
| size of 'standard data' | |
+----------------------------+ |
| 0 (id of std data) | |
+----------------------------+ |
... |
Body of std data |
... +-> segment size words
+----------------------------+ |
| size of first add-on | |
+----------------------------+ |
| 1 (id of first add on)| |
+----------------------------+ |
.... |
body of first add on |
.... |
+----------------------------+ |
.... |
+----------------------------+ /
This can be accomplished using a pattern of interacting objects that is very much like the Chain of Responsibility pattern described in Chapter 5 of [Gamma et al. "Design Patterns Elements of Reusable Object-Oriented Software].
The chain of resposibility pattern says that one way to handle
an action (in this case the reading of an event), in a way that is
decoupled from how this action must be done, is to set up an ordered
sequence of items that are all given a chance to perform all or part of
this action. You are setting up a chain of responsibility when you
register event segments in
CMyExperiment::SetupReadout
in the production readout Skeleton.cpp file.
Similarly, our segement readout classes can manage a modified chain of responsibility for reading out the standard part of the segment and all user add ons. In order to impose the level of control on packet formatting we want, we will modify the chain of responsibility pattern implementation slightly from that described in the Gamma book.
The Gamma book has each element of the chain of responsibility containing a reference to the next element in the chain. Each element does what it needs to do and continues to forward the request to the next element until the end of the chain is reached or until one of the elements decides that the chain should terminate. In our case, however we want to intervene between chain elements to enforce the buffer substructure we have described in the previous figure. To do this, we will have our event segment traversing the chain of responsibility; invoking each element in the chain to read the body of each sub-subpacket.
We will represent the event segment for a detector segment as an Standard Template Library (STL) list of event segments (see the additional resources at the end of this chapter for information about STL if you need it).
On construction the 'standard' body event segment is inserted as the first
element in the STL list. This ensures that the first element of the chain
of responsibility will read out the standard segment body.
We will provide additional member functions to support adding elements
to the list. This will build a structure very similar to that of the
CCompoundEventSegment.
The CReadNSCLDevice class from the previous section
is still good. Our changes will start at the leve of the
CNSCLDeviceSegmentReader. The important parts of the header
for that class will now be:
Example 5-34. CNSCLDeviceSegmentReader using the
chain of responsibility pattern
class CNSCLDeviceSegmentReader : public CEventSegment
{
private:
std::list<CEventSegment*> m_Subsegments;
...
public:
CNSCLDeviceSegmentReader(int segmentNumber);
...
public:
virtual DAQWordBufferPtr& Read(DAQWordBufferPtr& buffer);
...
void addUserSegment(CEventSegment* pSegment);
...
protected:
DAQWordBufferPtr& openSubpacket(DAQWordBufferPtr& buffer);
DAQWordBufferPtr& readSubpacketBody(DAQWordBufferPtr& buffer);
DAQWordBufferPtr& closeSubpacket(DAQWordBufferPtr& buffer);
DAQWordBufferPtr& openSubSubPacket(DAQWordBufferPtr& buffer, int id);
DAQWordBufferptr& closeSubSubPacket(DAQWordBufferPtr& buffer);
}; 
m_Subsegments is the chain of responsibility
for reading event bodies. The constructor will stock this initially
with an event segment that whose Read member
is much like the readSubpacketBody member
of the previous section.

m_Subsegments.push_back(pSegment)

openSubSubPacket and
closeSubSubPacket open and close
subpackets respectively. The additional id
argument is the id to assign the subsubpacket.
With these modifications, the implementation of readSubpacketBody
looks like this:
Example 5-35. Running the chain of responsibility
DAQWordBufferPtr&
CNSCLDeviceSegmentReader(DAQWordBufferPtr& buffer)
{
int id =0;
std::list<CEventSegment*>::iterator p = m_Subsegments.begin();
while(p != m_Subsegments.end()) {
CEventSegment* pseg = *p;
buffer = openSubSubpacket(buffer, id);
buffer = pseg->Read(buffer);
buffer = closeSubSubpacket()
p++;
id++;
}
return buffer;
}
By having the event segment only worry about overall subsegment structure, and by implementing the body as a chain of responsibility, we have produced a very flexible, extensible readout scheme. One which, furthermore, allows us to cleanly separate code for which we are responsible from code for which the user is responsible.