This follows on from parts 1 and 2 which were published in CVu. Both Adrian and Francis feel that part 3 is more suitable for Overload. So here it is! - Ed.
We have a system which allows 'background' processing based on a simple cluster of classes (see previous articles). The class that we need to derive from is the ProcessBase class. This provides the virtual functions Prepare(), DoAction() and Cleanup(). Instances may be part of a sequence of operations or just running continuously. The latter case is the simplest to design for. The lifetime of a ProcessBase being similar to that of the object or objects being acted on, it is of little significance whether it is associated with, a part of, or a base of the object concerned.
Designing your classes
I'd like to look at a couple of situations where you could reasonably use sequences in an application. Needless to say, these will be broadly modelled on an actual application (without giving away any trade secrets, of course).
First, program start-up. Imagine that your ap plication has to read a number of files (or data tables perhaps) and build an in memory representation of that data. This is going to take some time (at least several seconds). Secondly, we'll look at performing a long winded 'query' within the program.
In the first case, it is really quite easy to design the overall structure of the classes that will hold the data in memory. If the data is in the form of a relational database, the classes or data structures would match records in the tables, foreign keys in detail tables will correspond to associations in the class design. Associations will probably be represented by pointers to classes. Additional classes will probably be needed to manage these objects, probably with some form of collection class. Anyway, this part of the design should more or less do itself. Let's call these classes 'In Memory Data' collectively (IMD).
Taking the line of least resistance, you could write all the initialisation code in the constructors of these classes, they could open the files or tables, read them, create objects etc. This wouldn't allow the work to be done in the background though.
Obviously, this isn't just going to happen in the background. If this work is going to be done in the background, just where do we put our ProcessBase derived objects. In that case, we must put that code in ProcessBase derived classes. The question is, just where do these classes fit in? It's tempting to use ProcessBase as a base class for some of the IMD classes. The obvious justification for this is that you can encapsulate the reading of the data in these classes. This isn't really going to be the case though! In practice, the whole process of reading the data is going to need knowledge of both the destination classes and the file or database mechanism. It is much more likely tha you will want to encapsulate the reading of a stream of some kind within the class. The point about streams being that you don't have to know what kind of stream it is within the class. Streams give us some abstraction of the source of the data, whether it is a file or a binary database field. Exactly where you put the 'glue' that provides the actual stream and uses the IMD class members to read from that stream is up to you. If there's only going to be one stream, as in the case of serialisation to a single file, then that would probably be opened before the sequence starts. In other cases, you will almost certainly want to open the data source in the Prepare() function of your ProcessBase derived object. It can then be closed in Cleanup().
If the IMD is complex, it may well be that the reading process is also complex. If so, consider deriving more than one ProcessBase class to break the task up into simpler parts. This is almost always the best course if you find that DoAction() needs to handle multiple states.
Summarising the program start-up situation, it is probably best to create separate 'reader' ProcessBase objects with short lifetimes, than to derive your IMD objects from ProcessBase. The latter option is still possible but inelegant.
Now consider the 'query' case. I'm assuming that you're reading data from a database, processing it, then formatting and displaying it. In this case, we will need to interact with objects that read data as well as user interface objects that display it. Precisely what is read will be determined by a query object of some kind. The layout and formatting of the data will be obtained from some kind of class representing the query and there must be something representing the displayed data (a view). Both of these are possible candidates for derivation from ProcessBase. The alternative is to derive one or more intermediate classes from 'layout' classes. That might be a part of the query object or it may be common to all queries. With this set of classes, there is no obvious candidate to double as a 'querier' object. A sophisticated design may well provide some abstraction of the display object, if so that eliminates one possibility. The query itself may seem a reasonable candidate but this is likely to be quite complex in its own right. It seems wrong to allow the query object too much knowledge of the display object, or any at all for that matter.
This is a key principle behind the MVC (Model-View-Controller) architecture - Ed.
In my opinion, the only valid solution is to have one or more intermediate ProcessBase derived objects to perform the query. There are two reasons why this is likely to be the best solution. First, the objects and resources needed to read the database are only needed during the querying process. Secondly, we can reduce the binding between the query and the view.
In summary, it's best to first consider creating short lifetime 'go between' classes.
Tying in with the user interface
When your application is busy doing some thing, it's usually best to let the user know that it is working and hasn't just expired. One way to do this is to display a 'progress' meter as well as updating a 'status' bar with descriptive text: 'Packaging today's data, 10% done' or more realistically, 'Corrupting today's data, 90% done'.
The natural way to generate this behaviour is to fire an 'event' whenever the status text or the progress fraction changes. The ProcessBase class can have member functions to set the status text and progress, e.g.
void SetStatus(const char *pText);
void SetProgress(double progress);
There are several choices as to what these functions do. Firstly, they may store the text and progress value within the class, or they may pass them straight through to the event handlers. There is an obvious temptation to make these functions virtual and provide the code to hook to the user interface in the derived code. Unfortunately, this is likely to increase the binding between the derived class and the rest of the application. The class that draws the text or updates the meter may well not be one that the ProcessBase class otherwise needs to be associated with. For example, the ProcessBase class may be associated with an IMD class and possibly a database class, neither of which are concerned with the user interface in any way. To allow this additional binding will make your classes less portable and maintainable. Suppose that your ProcessBase derived class is going to be re-used in a cluster of applications, do you want to have to derive new classes for each application, just to hook into theUI?
In procedural terms, this would be a natural candidate for a function pointer. ProcessBase can have a function pointer for each event. Just taking the progress event:
// either inside or outside of the
// ProcessBase class:
typedef void (*ProgressEventHandler)(double progress);
// private member of ProcessBase,
// init to 0 in constructor:
ProgressEventHandler m_PEHandler;
// public members:
void SetProgress(double progress);
void SetProgressEventHandler(ProgressEventHandler peh)
{ m_PEHandler = peh; }
void ProcessBase::SetProgress(double progress){
//. .
if(m_ProgressEventHandler)
m_ProgressEventHandler(progress);
}
In this case, the function pointer is used to call the supplied function with the progress value as a parameter, if the event has been set. The obvious deficiency in this approach is that a plain function isn't what we would ideally call. It would be much more natural to call a class member function directly.
Before we go on to do that, consider the 'progress' parameter being passed straight through. Would it be better to store it in the ProcessBase instance? If we do, then we will need to add a data member and an access function:
//private in ProcessBase
// init to 0.0 in constructor:
double m_Progress;
//public in ProcessBase
double GetProgress(){return m_Progress;}
You wouldn't make it a public data member, now would you?
Then in order to access the progress value, we could pass a class reference or pointer through to the handler function. That is, the handler function pointer type becomes:
typedef void (* ProcessEventHandler)
(const ProcessBase& process);
One advantage of this approach is that only one handler type is now needed for both progress and status events as well as any other events that we may think of. Also, the handler function may now access other ProcessBase properties, which gives a little more flexibility in how these are used. Other arrangements are possible. For example, you could also pass an enumeration for the kind of event to the handler. That's not necessarily the smartest way to work but some people like that kind of thing. Note that with the event handler being essentially a procedure and not being able to modify the ProcessBase instance, the SetProgress() member function doesn't rely on side effects of the handler. From ProcessBase's point of view, the presence or absence of event handlers has no effect on its behaviour. You may wish to sacrifice the constness of the ProcessBase reference passed to the handler. This may be valid, but it is a decision that you should reach with caution.
Dropping the procedural handlers.
How can we provide another class's member function as a handler? This is a harder question than it may seem!
Think what is needed to implement the proce dural handler above. There would need to be a class instance statically available to the function. This would then be used to call a member function to display the progress or whatever.
C++ has 'pointer to member functions', these nearly do the job! A combination of pointer to member function of the right type and pointer to the instance is what's actually needed.
Let's try it:
typedef
void (SomeClass::* ProcessEventHandler)
(const ProcessBase& process);
Oh! The problem is: what's 'SomeClass'? It needs to be the actual class of the instance whose member function is going to be used. That's no good, ProcessBase can't know about the class that's going to handle the event, see above! At least it must be a base of the actual class if you're prepared to cast it. This may be an acceptable solution if you're using something like MFC where you could use CObject as the base for everything. You may then cast the actual pointer to member in order to set it. This approach can work, if you have a suitable candidate base class and it isn't used as a virtual base (you cannot then cast it).
If you're happy to implement it this way, then go ahead, it is simple enough to do. If on the other hand you don't want to tie your class library to someone else's then you will have to go to more trouble.
If your compiler doesn't support templates, then go back one paragraph, or prepare to write some horrid macros (and still not fully solve the problem).
If we can't store a method pointer and use it for any arbitrary class, what can we do? Well, we could assign a pointer to a base class and call a virtual function! How does that help?
We can then derive from that class, using a template to give us the correct class of method pointer!
class ProcessBase;
class ProcessEvent
{
public:
virtual void DoEvent(const ProcessBase& process) = 0;
};
template <class HandlerClass>
class ProcessEventHandler
: public ProcessEvent
{
private:
typedef void (HandlerClass::* FnProcessEvent)
(const ProcessBase& process); HandlerClass *m_Handler;
FnProcessEvent m_OnProcessEvent;
public:
ProcessEventHandler()
: m_Handler(0), m_OnProcessEvent(0) {} ProcessEventHandler(HandlerClass
*pHandler, FnProcessEvent pe)
: m_Handler(pHandler),
m_OnProcessEvent(pe) {}
void SetHandler(HandlerClass *pHandler,
FnProcessEvent pe)
{ m_Handler = pHandler;
m_OnProcessEvent = pe; }
void DoEvent(const ProcessBase& process)
{ if(m_Handler)
(m_Handler->*in_OnProcessEvent)(process);
}
};
This template class will be instantiated for each handler class that is used to handle these events.
Here are the relevant parts of the ProcessBase class, with code inlined for compactness:
class ProcessBase
{
ProcessEvent *m_pPE;
double m_Progress;
public:
ProcessBase()
: m_Progress (0) , m_pPE(0) {}
virtual ~ProcessBase()
{ SetProgressEvent(0);}
void SetProgress(double progress)
{ m_Progress = progress;
if(m_pPE)
m_pPE->DoEvent(*this);
}
void SetProgressEvent(ProcessEvent *pPE)
{ if(m_pPE) delete m_pPE; m_pPE = pPE;}
};
Note that the progress event property is now a simple pointer to a ProcessEvent instance. In this implementation, the ProcessEvent once assigned is owned by the ProcessBase class. If you want to enforce this, you will need to do additional work.
In use, suppose we want to supply a member of a 'MainWindow' class:
class MainWindow
{
// public event handler:
void ShowProgress(const ProcessBase& process) {}
};
ProcessBase pb;
MainWindow mainWin;
//This is how to assign the event handler: pb.SetProgressEvent(
new ProcessEventHandler<Test>(&mainWin, MainWindow::ShowProgress));
Other events
Getting back to the ProcessBase and Sequence classes, there are several other events that you may wish to fire. One of the most useful events is after the Cleanup() function has finished, particularly for instances of the Sequence class. Think of the startup sequence. Until the sequence is finished, menu items and the like will need to be disabled, they will need to be enabled once it's finished. Given that the code that kicks off the sequence will have finished long ago, an event is the best place to do this.