A Class Wrapper For Files

By Jim Kane

Posted December 21 1999

Printer-friendly version

While I'd dearly love to fill your heads with visions of assembler, registers, and COM, every now and again I have to write code for more mundane things like validating a product order and determining a projected delivery date. Naturally I'd like to get these things done fast and efficiently so I can get back to rearranging registers, stacks, and the API - who wouldn't?

The quickest way I can imagine to complete a task is to find that the code exists and can simply be plugged in. Nothing pleases me more that making a step by step list of how to complete a programming task only to find I have a pre-written class that covers each step. Those days do happen and are becoming more frequent as I expand my class libraries. Unfortunately, it's rare that something like an order entry system can be written from scratch. Usually there is a pre-existing file structure that must be followed. Since no one designs a file structure as well as (insert your own name here) the fields are never laid out quite right or named the correct way.

So how can you write the code once, yet fit as many file structures and field names as possible? The short answer is it can be done, in Clarion, with classes.

Inspiration

The task that inspired me to write the code this article is based on was one of those jobs that seemed simple at first. The company in question currently accepted orders from sales people who collected orders over the day and zipped them at night followed by an ftp upload. Other orders were taken over the phone, while still others came by fax. A different program took each order, depending on if it came via the sales force or from in-house (fax or phone).

Typically each order consisted of a main item with zero to 25 accessories. The order could not be shipped until all the accessories were available. Unfortunately the logic was fairly complex. If a customer ordered two types of input devices and only one was available, the order could be shipped. If they ordered only one input device and it was not available, the order could not be shipped.

My task was to write a web order entry system that calculated estimated shipping cost and date while the customer was still on-line. It sounded manageable at first, but when I entered some test orders the program that took orders from the sales force gave one answer for shipping date and shipping cost, and the program that took orders from telephone/fax gave a different answer. The file record structure and field names were also different for each order type. To add insult to injury, with the addition of a SQL based web order entry system a third new record structure was about to be introduced. This structure included a few new fields not needed by the other systems, such as an email confirmation address.

Due to some legacy reporting system requirements, file formats could not be merged. Also the discrepancy in the existing systems had to be resolved, as it was common to find calculated freight costs hundreds of dollars different from each other. I'd have a better chance of getting paid and coming out of this project alive if the calculated shipping cost was not only consistent, but accurate or higher than the real shipping cost!

The goal became to write one fairly complex piece of logic that took input from a minimum of three different file formats and wrote the projected ship date and estimated shipping cost back to the same order record the information came from. In spite of the fact that three different programs were involved, the results needed to be consistent. Since I was fairly confident I'd be learning business rules as I went along (read as: the customer didn't have a clue what he wanted but he'd sure be fast to tell me if I didn't deliver it!), I really wanted to only have to maintain the logic in one place.

The best way for me to do that was to write one encapsulated piece of code that could be called from each of the three programs and operate on different files. Nothing to it. All I had to do was write code that read and wrote files whose structure I did not know at the time I was writing the code.

Design Issues

FileManager, one of the ABC classes, already provides a file independent method for many file operations, but what is missing is a way to address individual fields. ABC uses the field pairs class to copy from a file to a queue but that was not what I needed. I needed to be able the address fields like Product ID and Quantity Ordered as input regardless of the actual field name and write back the anticipated ship date and shipping cost.

For illustration purposes I'll define a few files and only deal with finding the estimated shipping date:

WebOrder:
Sysid
Estshipdate
EmailAddr

WebOrderDetail:
Sysid
WOSysid
OrderDate_Date
OrderDate_Time
ProductID
Qty
PhoneOrder
Sysid
Estdate

PhoneOrderDetail:
Uid
POSysid
OrderDate
ProductID
QtyOrdered

Inventory:
Sysid
ProductID
QtyOnHand
AvailDate

Both WebOrder and PhoneOrder are header files for an order. I will refer to them generically as Order. To the code that enforces the business rule, either WebOrder or PhoneOrder has a functional appearance of:

Order:
Sysid
EstShipDate

For each of the real files, WebOrder and PhoneOrder, the actual field names and data types may differ. Each file may contain additional fields not logically important. Using the same thought process I developed a generic Detail file to virtualize WebOrderDetail and PhoneOrderDetail:

Detail:
ParentSysid
ProductID
QtyOrdered

The business rule which needs to be enforced is written in pseudo code as:

! set() on primary key of order file
Set(keyOnOrderSysid)  
Loop
  Next(0rder); exit on error
  Clear(EstShipDate)
  LastParent = Sysid !save the linking field
  !Set up child key fields
  Set(DetailKey)
  Loop
    Next(Detail) 
    If Detail.ParentSysid<>LastDetailParent or error
         Order.EstShipDate=EstShipDate !Save the ship date
         Update(Order)
         Break
    End
    Inventory.ProductID  = Order.ProductID
    ! Fetch gets QtyOnHand and AvailDate
    Fetch(KeyOnProductID in Inventory file) 
    If  QtyOnHand<=QtyOrdered and AvailDate>EstShipDate 
       EstShipDate = AvailDate.
    End
 End !loop Detail - child
End !loop Order - parent

The requirement is to be able to do Set, Next, Fetch, and Update on files. As well, the code must get and set a field's value in spite of the fact that the fields are unknown at the time of writing. All that is known is a generic, virtualized version of the files involved: Order, Detail, and Inventory. The real files may have more fields and the field names may vary.

Since the main work to be done is to manage access to unknown fields, I hereby christen the new class that will solve the problem FldMgrCl, short for Field Manager Class. At its heart it will have to manage a list of fields (FieldListQ) and keys (KeyQ) plus have the ability to read and write those fields. Since the data type of the fields is not know, the Clarion Any type sounds like the approach to take. In the Init and Kill methods, the primary task will be to create and destroy the FieldListQ and KeyQ. In addition the class will reuse ABC file manager methods so the class will have to store references to these objects. Putting these bon mots (said with a New York accent if you know me) into code what you have so far is:

!ABCIncludeFile
OMIT('_EndOfInclude_',_FldMgrClPresent_)
_FldMgrClPresent_ EQUATE(1)
!Other Classes
   Include('ABFILE.INC')
!Equates - will be seen by using program
FieldListQtype  Queue,Type
FieldName   string(20)
FieldRef    Any
  End
KeyQtype    Queue,type
KeyName     string(80)
TheKey      &key
   end

FldMgrClType Class,type,module('FldMgrCl.CLW'),
   LINK('FldMgrCl.CLW',_ABCLinkMode_),DLL(_ABCDllMode_)
!Member Data
FieldListQ   &FieldListQType   !List of fields
KeyQ         &KeyQType         !List of Keys 
File         &File             !ref to file 
Buffer       &Group            !ref to buffer
FM           &FileManager      !ref to FileManager
RM           &RelationManager  !ref to RelationManager
!Set up Methods - Calls are template generated
Init         Procedure()
Kill         Procedure()
AddFile      Procedure(File pFile, *Group pBuffer)
AddManagers  Procedure(FileManager pFM, RelationManager pRM)

Notice how ABFile.inc is simply included in the new class. With that done FileManager and RelationManager can be referenced and used freely. Not bad for one line of code! OOP is plug and play; this is but one example of how classes can be combined to make something powerful.

Before I move on I need to comment on the code in Kill, as the ANY data type requires some special care and feeding or it will bite you (and not in a good or playful way!).

FldMgrClType.Kill              Procedure()
!destroy any dynamic objects and clean up
Recs long,auto         !junk queue record count
I    long,auto         !junk index variable
    Code
    !Null the ANYs in the FieldHdrList 
    !then then dispose of the Q
    If ~SELF.FieldListQ &= NULL Then
      Recs = Records(SELF.FieldListQ)
      Loop I = 1 to Recs
        Get(SELF.FieldListQ,I)
        !ANY field set to null
        SELF.FieldListQ.FieldRef &= NULL  
        Put(SELF.FieldListQ)
      End
      Dispose(SELF.FieldListQ)
    End
    If ~SELF.KeyQ &= NULL then Dispose(SELF.KeyQ).
    RETURN

Notice where the ANY field in the queue, FieldRef, gets set to NULL. This allows the ANY variable a chance to clean up and release its memory. Also note that since in Init the queues were NEW()ed, in Kill they are DISPOSE()ed. By cleaning up the ANY's and DISPOSE()ing all that is NEW()ed, memory leaks are avoided. If you create memory leaks by forgetting these things, it's quite possible for the program to run out of memory after a while resulting in all kinds of hard-to-trace errors. Since I have brief moments of sanity, I try hard to use them to match all memory allocating activities with memory freeing activities and avoid the problems.

At this point I suggest downloading and opening the sample code. In particular look at fldmgrcl.clw and fildmgrcl.inc for the Init and Kill code, plus the rather mundane code to store away the file, buffer, FileManager and RelationManager.

I suppose it's a good thing I like to program. I do not think I would make it in advertising, since I just told the reader (that's you!) to download and open something, then warned that is only mundane. Nonetheless, read the code.

Now that FieldListQ and KeyQ have been created, the logical thing, as Mr. Spock would say, to do is fill them:

AddKey         Procedure(string pKeyName, Key pKey)
AddField       Procedure(String pFieldName, *? pField)

Each of these methods takes the name of a field or key and the field or key itself so it can store a reference. For those for whom references are unfamiliar, think of them as pointers, and a way for a computer to remember where a field or key lives. A typical call to these methods for the order file would be:

WebOrderFieldMgr.AddKey('WEB:BY_SYSID', WEB:BY_SYSID)
WebOrderFieldMgr.AddField('SYSID',SYSID)

Now inside the WebOrderFieldMgr class the Web:By_Sysid key or the Sysid field can be referred to by the strings 'Web:By_sysid' and 'Sysid'. I could have chosen to use equates rather than strings which probably would have been faster. I chose not to because for a large dictionary and many files, that would create a large number of labels. In one case this brought back some pool limits I'd rather not see again.

In my implementation, the strings are not case sensitive. The code for adding the key or field to its respective queue is straightforward. Perhaps more interesting is the code to get or set the field values:

Add a field:
FldMgrClType.AddField       Procedure(String pFieldName, 
               *? pField)
  code
  Assert(~SELF.FieldListQ&=NULL)
  Clear(SELF.FieldListQ)
  !Save the reference
  SELF.FieldListQ.FieldRef &= pField    
  !save the token to identify the field
  SELF.FieldListQ.FieldName = Upper(pFieldName)  
  ADD(SELF.FieldListQ,SELF.FieldListQ.FieldName)
  Return

Save or set a field value:
FldMgrclType.SetField       Procedure(String pFieldName, 
              ? pTheValue)
  Code
  SELF.FieldListQ.FieldName = Upper(pFieldName)
  Get(SELF.FieldListQ,SELF.FieldListQ.FieldName)
  If ~Errorcode() then
    !Save the value passed in the actual 
    ! field referenced by fieldref
    SELF.FieldListQ.FieldRef = pTheValue  
  else
    If Errorcode() 
      message('Error locating field:'&clip(pfieldName)|
        &' Error:'&Clip(Error()),'SetFieldError')
    end  
  end
  Return

Return or get a field value:
FldMgrClType.GetField       Procedure(String pFieldName)
  code
    Assert(~SELF.FieldListQ&=NULL)
  SELF.FieldListQ.FieldName = Upper(pFieldName)
  Get(SELF.FieldListQ,SELF.FieldListQ.FieldName)
  Assert(ErrorCode()=0)
  Return SELF.FieldListQ.FieldRef

Notice in the prototypes the use of the ? and *? data types. These can be thought of as parameters able to pass any data type. They're ideal for the current work at hand. For those not use to working with references notice two different syntaxes for two different purposes:

FieldRef &= PRE:FIELD

means that FieldRef now points to a field called PRE:FIELD, while

FieldRef = 12

means the value 12 should be stored in what ever FieldRef points to (PRE:FIELD in this case). This can be confusing because the ANY data type can be used either way.

The net result of all this is the ability to refer to a field or key by an equivalent string token. You can even alias or provide an alternative string token that can then be used to reference a field. For example:

FldMgrClType.AliasField    Procedure(String pFieldName, 
                 String pAliasName)
TempAny Any
  Code
SELF.FieldListQ.FieldName = Upper(pFieldName)
  Get(SELF.FieldListQ,SELF.FieldListQ.FieldName)
  Assert(ErrorCode()=0)
TempAny &= SELF.FieldListQ.FieldRef
!must clear a queue containing 
!any an ANY before reusing the Any
Clear(SELF.FieldListQ) 
!Set up and save two fields - An Alias is born
SELF.FieldListQ.FieldRef &= TempAny   
SELF.FieldListQ.FieldName = Upper(pAliasName)
ADD(SELF.FieldListQ,SELF.FieldListQ.FieldName)
  Return

Now either the AliasName or the FieldName string can be used to reference the field. The above code takes the field name passed and locates it in the FieldListQ. If it is not found, usually a typing error, an Assert() fires. Once the FieldListQ entry is found, the reference to the "real" field is saved in a temporary variable (TempAny). After clearing the queue buffer (required when the ANY data type is used in a queue), the AliasName and reference to the real field is stored in the queue.

On subsequent accesses, whether the lookup into the queue is done using the fieldname or the alias name, the same field reference is found. This allows fields with different names in different files that have the same purpose to be referred to in code by the same name. For example, consider two files WebOrder with a field Qty and PhoneOrder with a field QtyBought. Either WebOrderFldMgrCl or PhoneOrderFldMgrCl could be passed as an input parameter to an OrderProcessing Class. Use of aliases makes it easy to pass either WebOrderFldMgr or PhoneOrderFldMgr even though the field names differ and get the job done. For example:

!Set up a QuantityOrdered Alias in both files
Weborderfldmgr.AliasField('Qty','QuantityOrdered')
PhoneOrderFldMgr.AliasField('QtyBought','QuantityOrdered'

!Pass either fieldManager class to the OrderProcessClass:
OrderProcessClass.AddOrderFile(WeborderFldMgr) 
!or 
orderProcessClass.AddOrderFile(PhoneOrderFldMgr)

!Code in Order ProcessClass
OrderProcessClassType.AddOrderFile(FldMgrClType OrderFile)

  Code
  SELF.OrderFile &= OrderFile
  !Inside OrderProcessClass the totals 
  ! are calculated the same:
  LineItemCost = |
     SELF.OrderFile.GetField('QuantityOrdered')  * UnitCost  
  !Note: I don't care if this is WebOrder or 
  ! Phone Order! Both have the same alias

Time for a test! I said near the start the idea was to encapsulate the business rule (three points for Kane for successfully using a buzz word/phrase). The first step is to create FieldManager classes for the WebOrder, WebOrderDetail, PhoneOrder, PhoneOrderDetail, and Inventory file. Since typing all the field names resembles work, its time for a template. The accompanying code contains a template to generate a FldMgrClass wrapper for each of the files. Thanks to the use of the Alias method above, all the Detail files (WebDetail and PhoneDetail) now have a ParentSysid, ProductID field. For example:

PhoneOrderDetailFldMgr.AddField('POSysid', PDet:POSysid)  
PhoneOrderDetailFldMgr.AliasField('POSysid','ParentSysid')
WebOrderDetailFldMgr.AddField('WOSysid', WDet:WOSysid)
WebOrderDetailFlgMgr.AliasField('WOSysid','ParentSysid')

Now both files have a field that can be referred to by the string 'ParentSysid'. Notice also that the template generated the bulk of the code. All that needed to be typed were some aliases. No carpal tunnel syndrome here!

Time to put it to use:

ProcessOrderClType Class,type,module('FldMgrCl.CLW'),
   LINK('FldMgrCl.CLW',_ABCLinkMode_),DLL(_ABCDllMode_)
OrderFile  &FldMgrClType
DetailFile  &FldMgrClType
InventoryFile &FldMgrClType
Init    Procedure(FldMgrClType pOrderFile, 
   FldMgrClType pDetailFile, FldMgrClType pInventoryFile)
Kill        Procedure()
DoIt      Procedure()
End

Using SELF.OrderFile.GetField('ParentSysid') you can obtain the value of either WOSysid or POSysid depending on which file was passed in the DetailFile parameter. In other words, after setting up all the field manager classes both the WebOrder and PhoneOrder files can be processed with the same code in the DoIt method of the ProcessOrderCl.

ProcessOrderCl ProcessOrderClType
Code
!set up field managers here
!add in aliases for ProcessOrderCl
ProcessOrderCl.Init(WebOrderFldMgrCl, 
   WebOrderDetailFldMgrCl, InventoryFldMgrCl)
ProcessOrderCl.DoIt
ProcessOrderCl.Kill
ProcessOrderCl.Init(PhoneOrderFldMgrCl, 
    PhoneOrderDetailFldMgrCl, InventoryFldMgrCl)
ProcessOrderCl.DoIt
ProcessOrderCl.Kill

Success! The code is using files inside a class and small changes in field names or file formats just don't matter: they can still be processed. Consider the possibilities in writing generic code that works on version 1, version 2, and version 3 file formats for a program that has evolved over the years.

At the start I said I wasn't going to fill your head with COM or other low level stuff. Notice the word fill is in italics so that means I can still legally at least mention COM.

Consider the scenario I outlined through out the article of the order entry system. To update each program I need to insert the ProcessOrderCl, and recompile if the process of assigning an estimated shipping date changes. In the real world the code is much more complex and in practice changes at least once per month. On the other hand, if I could write the code as a COM object I could replace that object at any time. The next time any of the programs that use the COM object ran, the new object would be loaded into memory and executed. No recompile.

For connected uses, I could even employ DCOM and only update the COM object on one machine and any one across the enterprise would have an instant update the next time they ran a program that needed this COM object. Not only that but because the COM objects are universal, the same COM object could be used in programs written in any COM aware language, including Active Server Page scripts.

While in this case the code is only going into three or so programs and recompiling and redistributing those programs is not a large chore, consider a more common piece of code or business rule like perhaps a discounting scheme that gets used in Order Entry, Accounting, Sales Projections on and on through out the enterprise. It would not be surprising for some rules to touch many, many programs. By encapsulating the rule in a COM object, I could hand out updates rather easily. Very attractive. Microsoft has a name for this concept; it's called DNA, or Distributed interNet Applications. Of course, putting this into practice requires a development tool that can write COM objects.

Maybe some day Top Speed will see the light. Until that time, at least this code comes one step closer, in that it is only necessary to recompile to take advantage of updated order processing code without rewriting the code for three different file structures.

Download the source code

Jim Kane was not born any where near a log cabin. In fact he was born in New York City. After attending college at New York University, he went on to dental school at Harvard University. Troubled by vast numbers of unpaid bills, he accepted a U.S. Air Force Scholarship for dental school, and after graduating served in the US Air Force. He is now retired from the Air Force and writing software for ProDoc Inc., developer of legal document automation systems. In his spare time, he runs a computer consulting service, Productive Software Solutions. He is married to the former Jane Callahan of Cando, North Dakota. Jim and Jane have two children, Thomas and Amy.

Article comments

Post a comment

You must be logged on to post comments.

Clarion Roadmap

Try the roadmap (beta)

Search ClarionMag

 

Advanced search

From the archives

DevCon Details: Web Edition 2 and iBuild@TopSpeed

10/6/1999 12:00:00 PM

Details of Bob Zaunere's general session covering the upcoming Web Edition 2 and iBuild@TopSpeed products.