![]() |
|
Published 1999-02-22 Printer-friendly version
Who can
understand his errors?
Cleanse thou me from secret faults. Ps 19:12
In Psalm 19 David meditates on the judgements of God and the way that suitable correction can keep the servant profitable. But then he raises the important question, do we always understand the nature of the error? Does the method of correction give us enough information to avoid repetition of the problem? Are we prepared to have the error corrected or would we rather continue as we are? It raises a second issue, the undetected error. Whilst we might happily continue, confidant in our own minds that we are perfect, it is quite possible that faults and defects lie under the surface that are detracting from our overall performance.
Now David was clearly considering issues more fundamental and personal than a computer program but in many ways the construction of the ErrorClass has to reflect many of the concerns expressed in our opening verse. For the ErrorClass to be invoked it is probably the case that a condition or defect has been detected outside of the will of the creator of the program (you). It is when the chips are down that real quality shines. A chain is only as strong as it's weakest link. As such the ErrorClass is one of the most fundamental and basic of the ABC classes, it is the one written first and it is at the bottom of the pile (it doesn't use other classes).
Consider the external (developer) requirements placed upon it :
Now on top of the actual requirements placed upon the class when written, there is a hidden agenda. Put another way, I like to keep my options as open as possible for as long as possible. There were three extra issues I wanted taken into account :
The aims are always there to depress. When reading that kind of spec, I look out for repeated words, especially ominous ones. Looking at the above the word is 'tailor'. It means everything has to be soft. No quick hacks or hard-coding. We are writing an engine, not a solution.
The next thing to consider is 'what is on my side'.
Number one is that although the ErrorClass must be light (not use too many resources when dormant) it does not have to be that efficient (it being invoked will typically result in user interaction). Further there will probably not be that many of them floating around so memory consumption is not a horribly big issue.
The next thing to consider is who is going to be use the various parts of the specification and how. It is the insight at this point that leads to the design of the class. If you look at the above the class falls into two very distinct usage camps, you and me. More accurately, 1, 7, 8, a, b & c are mostly the concern of people writing ABC classes (or extensions). 3,4,5,6 (possibly 7, although templates help here) are the concern of the application employers. 2 is the concern of the end user, any problems here I can blame someone else for because of 3,4,5 & 6. This duality of problem means we may split the method of solution. This may or may not help, but at this stage we're looking for anything that may help.
Also you need to consider what level of developer will wish to make use of this functionality. Is it some baroque tit-bit to satisfy the bit heads, or day by day functionality to be used by everyone. In this case 3,4,5 definitely fall into the latter camp. This means we need a no-coding solution. The part of the solution for 'ABC' coders can be tougher if needs be.
Another plus is that most people will be content to do 3-6 at compile time so we can make that a restriction of the easy way, run-time tailoring can call for more work.
In any given class, I like to have a unifying theme or concept that makes the class a class (as opposed to a collection of classes). In this case the whole class hangs around the .trn file. I will explain the structure of this file presently, however the notion is simply this. Define all errors and actions in a data item (or structure). This structure can clearly be altered and therefore all the required tailoring can be done at run-time (code tends to need compile time modifications). The defaults for this structure can be stored in a simple ascii file (the .trn) which can be safely tackled by any developer. Further the .trn is independent of both the class implementation and interface and can therefore remain constant throughout future releases. You may almost think of the .trn as a program (in a peculiar language) that is interpreted by the error class. Hey! Interpretation is slow! Yup, my design considerations say that is ok.
So what data actually needs storing for each error? We choose four :
Now I eventually came up with a clever method of storing & using this data, it is detailed under the AddErrors(ErrorBlock) method below.
Probably the way to describe the implementation of the class is simply to talk through the various methods. I am using the C5 sources. There are two obvious orders to use, either alpha or the order in the source file. I'm going to use a third, the order they build upon each other. I assume you have these sources to hand and will not repeat information contained therein.
This method is really the heart of our implementation and trn file methodology. In order to work upon our data list we eventually want it stored in some easy data format (we choose a queue). We don't really want the user of the class to have to pre-fill the Q (or even know it exists) so the logical thing to do is have an AddError method that takes the four data types as parameters. The problem is this makes the code to initialize the error manager long and tedious. It also means that to alter how the ErrorManager works would require altering a code file. Further our executable would end up with two copies of the data (one to initialise, one stored). It would also mean imposing an upper limit on the length of an error message.
About Three O'Clock in the morning I had a brain-wave that later became central to the way ABC handles constants (embodied in the ConstantClass). It essentially uses a low-level detail of Pstrings, that the length of the string can be intuited from the data. Using this fact, and known details of the layout of bytes and shorts, it is possible to step through a constant group at a 'byte by byte' level and select out the data items.
This method therefore does precisely that, it contains an extra trick that prevents the strings being duplicated (it is this trick that stops us re-using the constant class). Rather than copy the strings into the queue, we have string references in the queue and simply assign them to the string slice that already exists within the group. This drastically cuts down memory consumption and removes any length limitation imposed by the queue (the pstring is limited to 255 characters of course).
NB: There is one hidden gotcha though. It means that if you do an AddErrors call from within a procedure using a group that is local data you must have the static attribute on the group if that error id is going to be used outside of that procedure (ie once that procedure scope has gone).
This method implements the next key feature, finding the correct data record for a given message Id. This is not just a queue get because we want to allow scoping. What I mean by that is we want to be able to alter the behaviour of a given error message within a limited piece of the program execution. We do this simply by saying that AddErrors always appends and that SetId always searches backwards to find the matching Id. The two parameter form is to allow the search to start in mid-list (used by the %previous macro). The one parameter (more common) form does a search from the end of the list.
The Take methods are used at the point some form of 'user
interaction' is required. TakeError is really the
despatcher, it simply calls one of the other Take methods that
correspond to the differing error levels available. Now when
embarking upon a group of methods, such as the other Take methods
my mind automatically does a form of 'parent-hunting' or
'common denominator finding'. If you have to write 5
similar methods, find out why they are similar and common up the
code.
Each take method is responsible for taking the data for
its' error message, substituting the macros with values from
the class' data and then displaying the results and acting
upon the user response. The 'action' is dependent on
error level, the substitution isn't, so a SubString method was created to handle the common
work. The Take methods then become fairly trivial.
Message was used in preference to a window to comply
with requirement 1.
There is one 'extra' take method,
TakeOther, this is really there for internal /
3rd party usage. Essentially it allows for new error
levels to be defined which will then be re-routed to this method.
It corresponds to the ELSE clause of the
TakeError method.
Requirement 6 is met by making each Take method
virtual. TakeError does not need to be virtual as it
is not called from 'underneath' only from on top.
This procedure performs the mapping from a string with macros to a string with the macros substituted. This allows all the private data inside the class to be code at, but via an interface. This satisfies requirement b. If errorcode et al begin to disappear Substring will become more complex, but user code is preserved. The beginnings of this are happening already as ErrorText is replaced with FileError rather than Error for an errorcode 90. For reasons of efficiency, the SubString method assumes the Id has already been selected using SetId
Note too the %Previous macro, this allows the user to append a message to the one generated by the system. Up to C5EE these macros have been case sensitive, we will probably change this for a future release.
SetErrors is the main routine to implement requirement 8. If the
Takes report the errors, then the
SetErrors records them. Note too SetFile &
SetMessage, these are simply providing additional information that
the error class might find useful. At first sight the might seem an
overly wide interface, why not set all the 'error state'
in one shot? This is really a concession to usage. This form of
separation allows, for example, the file class to
'pre-stuff' information into the error handler that
may or may not be used later. Thus the FileClass SetThread
routine (called at the start of every file class action) does a
SetFile. This avoids duplication of information and helps
requirement c.
At this point we have implemented most of the initial requirements, some extra methods were added to make the interface simpler to use (especially with regard to requirement c). The Throw procedure really undoes requirement 8 to fulfil requirement 7. If you like we are now playing the numbers game. First you implement the methods that give you the functionality, then you try to find the way that interface is used 80%+ of the time, and simplify it. In this case it turns out that quite often you want the error processed immediately, throw does this. We also found that SetMessage happened just before the throw quite frequently, hence ThrowMessage.
SetFatality is a short cut (you can do the same with scoped AddErrors) to allow the action taken on certain errors to be ignored (or escalated). Thus if you wished to turn off the 'NoRecords' message around a report you can simply do
In the opening embed of the report. (You need to reset the fatality level unless you want the change to be permanent).
I left RemoveErrors to last because it in one of those rare instances of a method that is completely mis-coded and mis-designed purely to make using it easier. Logically RemoveErrors should work on a list of Ids, or an individual Id. The only information you need to remove an error is the index of it. However, the time you use RemoveErrors it will almost certainly be paired with an AddErrors call, you are effectively defining an error scope. In fact the AddErrors will nearly always be at the head of the procedure, the RemoveErrors at the end. Now, if you have an AddErrors you want to make the RemoveErrors match 'perfectly'. So having two different data streams is dangerous. Further the ErrorBlock used by AddErrors is still available (remember it has to be static) so the easiest thing to pass in is the same ErrorBlock again. Thus if you look at the implementation you will find most of the procedure is simply skipping over the information it doesn't want, finding the Id, and then deleting that field.
David Bayliss is a Systems Architect for The TopSpeed Development Center. He has worked upon TopSpeed's compiler and was the chief architect of the Application Builder Classes.
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $189
(includes all back issues since '99)
Renewals from $139
Two years: $289
Renewals from $239