5.4. Developing in a Maintainable and Sustainable Way

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:

5.4.1. Managing your source code

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:

While CVS is built to support multiple developers (CVS stands for Concurrent Version System) working on the same codebase simultaneously, we won't describe how that works. See the resources section for more information about CVS. CVS is capable of a lot more than we will describe here.

5.4.1.1. CVS Concepts

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.

5.4.1.2. Moving your code into CVS

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
                        
That is you have a top level Makefile and separate directories for your SpecTcl and your Readout software each with its own Makefile as well.

NoteDisclaimer
 

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

5.4.1.3. Checking out a sandbox from the respository and working a bit

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:

Example 5-20. Checking out a working directory


cd ..
cvs -d `pwd`/codeRepository co myDaq
                        

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:

Example 5-21. Adding a new file to an existing project


cvs add Readout/newFile.cpp
                        
CVS will warn you that you still must commit the file to the repository.

5.4.1.4. Comitting and logging changes 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:

Example 5-22. Committing a file


cvs commit 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.

5.4.1.5. Marking stable points of development

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:

Example 5-23. Tagging a version


cvs tag Version1_0
                        
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
                        
This set of commands creates a new directory, and makes a sandbox (named myDaq) from the myDaq project but, checks out the file revisions that were tagged as being Version1_0

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.

5.4.1.6. Creating special versions via CVS 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.

5.4.2. Build structures for your software

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

I also like to create build systems that can have as a minimum the following targets:

all (the default)

Compiles the software in place.

clean

Removes all products of compilationso that the next build starts from fresh source code.

install

Install the software in some designated target

dist

Builds a tarball that has only what's needed to build the software.

ivp

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.

5.4.2.1. Simple build systems using Make

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   (1)

#  If you have any switches that need to be added to the default c++ compilation
# rules, add them to the definition below:

USERCXXFLAGS=                                     (2)

#  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   (3)

#
#  Finally the makefile targets.
#


SpecTcl: $(OBJECTS)
        $(CXXLD)  -o SpecTcl $(OBJECTS) $(USERLDFLAGS) \  (4)
        $(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. "

                        
(1)
These two lines define where SpecTcl has been installed. If you take your SpecTcl to another system where SpecTcl is installed in a different place than at the NSCL, only the first line of the file needs to be included. The include is used to source additional macros and default build rules. One of the most important functions of the include files (written by the SpecTcl installation) is to define exactly how to compile a C++ source and the link flages required to build SpecTcl.
(3)
Since the includes define how to build most objects, and since the user has an opportunity to add their own compilation options by defining e.g. USERCXXFLAGS, in most cases the Makefile scales by simply adding the names of the objects that have to be built.
(4)
The actual link phase is almost completely controlled by Makefile variables.

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.

5.4.3. Release management

Release management is the process of making a new version of software available. New releases of software come out for several reasons:

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.

5.4.4. Managing special requirements while keeping your source code sacred

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:

Classes should be built to be closed with respect to modification but open with respect to extension

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.

Some tools for achieving this in C++ are to:

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.

5.4.4.1. Using Virtual Functions to Achieve the Open/Close Rule.

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];    (1)
   ...
public:
   ...
   CNSCLDeviceSegmentReader* getSegment(int i);                 (2)
   void replaceSegment(int i, CNSCLDeviceSegmentReader* pNewSegment); (3)
};
                        

(1)
m_SegmentReaders is an array of pointers to CNSCLDeviceSegmentReader objects. At construction time, it gets filled in with pointers to 'standard' segment readers.
(2)
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.
(3)
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:

openSubpacket

Writes the header part of the subpacket, and saves a pointer to the size so that it can be filled in later.

readSubpacketBody

Reads the body of the subpacket.

closeSubpacket

Closes 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);            (1)
...
public:
    virtual DAQWordBufferPtr& Read(DAQWordBufferPtr& buffer);  (2)
...
protected:
    DAQWordBufferPtr& openSubpacket(DAQWordBufferPtr& buffer); (3)
    DAQWordBufferPtr& readSubpacketBody(DAQWordBufferPtr& buffer); (4)
    DAQWordBufferPtr& closeSubpacket(DAQWordBufferPtr& buffer); (5)

};
                        
(1)
The constructor is parameterized by the segment number.
(2)
The 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.
(3)
As we have already described, the 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.
(4)
The 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.
(5)
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);            (1)
...
public:
    virtual DAQWordBufferPtr& Read(DAQWordBufferPtr& buffer); (2)
...
protected:
    DAQWordBufferPtr& readExtraStuff(DAQWordBufferPtr& buffer); (3)
};
                        
(1)
The constructor will need to invoke the constructor for CNSCLDeviceSegmentReader. Even if it does not need the segment number itself, it will need to pass it on to the base class constructor.
(2)
This overrides the 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
(3)
We have extracted our own read software into a separate function for three reasons. First pedagogically, now we don't have to display a bunch of ellipses where the readout would go in our 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:

Example 5-33. CExtendedSegment::Read Implemented to put our data after the end of the subpacket


DAQWordBufferPtr&
CExtendedSegment::Read(DAQWordBufferPtr& buffer)
{
    buffer = openSubpacket(buffer);
    buffer = readSubpacketBody(buffer);
    buffer = closeSubpacket(buffer);
    return readExtraStuff(buffer);

}
                            

5.4.4.2. Using a Behavioral Pattern to Implement the Open/Close Rule

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;                        (1)
...
public:
    CNSCLDeviceSegmentReader(int segmentNumber);
...
public:
    virtual DAQWordBufferPtr& Read(DAQWordBufferPtr& buffer);
...
    void addUserSegment(CEventSegment* pSegment);                 (2)
...
protected:
    DAQWordBufferPtr& openSubpacket(DAQWordBufferPtr& buffer);
    DAQWordBufferPtr& readSubpacketBody(DAQWordBufferPtr& buffer);
    DAQWordBufferPtr& closeSubpacket(DAQWordBufferPtr& buffer);
    DAQWordBufferPtr& openSubSubPacket(DAQWordBufferPtr& buffer, int id); (3)
    DAQWordBufferptr& closeSubSubPacket(DAQWordBufferPtr& buffer);

};                        
(1)
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.
(2)
This member function allows additional event segments to be appended to the chain.. it will just invoke

                                m_Subsegments.push_back(pSegment)
                                
(3)
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.