MetaClasses

A metaclass is a class which, when instantiated, returns a class, not an
object (actually the data type of that which is returned IS type "O"  and
can be called a "class object" just to screw with our heads).

In other words, a metaclass defines a class instead of instantiating one.

Example:

CREATE CLASS DbfClass FROM Class
export:
   method Init
end class

*---------------*

method init( cDbfName ), ( cDbfName, record() )
   local nScope      // required by EXPORT:
   local nJ, nOldSelect

   // save current work area
   nOldSelect := select()

   // temporarily open the file in a new work area
   dbUseArea( .t., NIL , cDbfName )

   EXPORT:

   for nJ := 1 to fcount()
      VAR ( fieldname( nJ ) )
   next nJ

   // This method is defined by Class(y) in the CLASS
   // class.
   ::makeClass()

   // close the file temporarily opened
   dbCloseArea()

   // return to prior select area
   select ( nOldSelect )

return self


You can immediately tell that the above is a metaclass definition because
the parent class is the CLASS class, which is the mother of all classes.

For a moment, lets ignore the line:
      method init( cDbfName ), ( cDbfName, record() )
and look at what happens within the Init method.  [Error handling
omitted for brevity's sake].  After temporarily opening up a file whose
name was received as a parameter, we're looping through all the field
names and declaring instance variables whose scope is EXPORTED.

The end result of this instantiation is that a class has been defined which
has instance variables for all of the field names in the dbf. Since it's
done dynamically at runtime, changes to the structure between runs will
be included on the next execution of the application.

Going back to the line (here's where it can get hairy):
      method init( cDbfName ), ( cDbfName, record() )

As you know, the parameters specified within the first set of quotes are
those which may be received by the call to this method.  The
parameters within the second set of parens specify that which is to be
passed to the parent or super class of this class (dbfclass).  Since we
have specified that the CLASS class is the parent class of DBFCLASS, we
pass those parameters which we know it wants:

      1.   the name of the class being created.  By using
           cDbfName we're asking for the new class to have the
           same name as the dbf file. This part can be confusing. 
           It's important to distinguish clearly  in your mind the
           difference between the instantiation of the  metaclass,
           and the instantiation of the class created by the 
           metaclass. Let's digress for a second by looking at how
           we're  going to use the metaclass once created.

           Example:
           // Using a metaclass, create a customer
           // class.
           oClass := dbfclass():new( "Customer" )

           // instantiate the customer class
           oCustomer := oClass:new()

           The metaclass's name is DBFCLASS, but the name of the
           class created  with the metaclass is CUSTOMER.

      2.   the parent of the class being created.  In our example, 
           we're specifically saying that the parent of class 
           CUSTOMER is class RECORD.  Obviously, we won't 
           always have a 2nd parameter to use here.  But in the 
           specific example above (a dbf class), there's a compelling
           reason to do so.


Since class DBFCLASS is a metaclass, any variables and/or methods
specified within the class declaration clause [ie between the CREATE
CLASS... and END CLASS] would be class variables/methods.  Though they
would be accessible by all instances of the class being created, they
would not have access to data contained within each object.  In other
words, state information (like record number) which varies for each
object could not be manipulated by class methods.

We could have added all of that which is in common to all dbf classes
(ie methods Open(), Close(), Seek(), etc) within class DBFCLASS in the
same way that we added the fieldnames as instance variables, but the
result would have been that we'd have duplication all over the place and
wasted memory. Once I'd instantiated both CUSTOMER and INVOICE I'd
have 2 Open methods, Close methods, etc.  In effect, I'd have flattened
the inheritance tree and forced larger blocks of memory to be swapped
in whenever I wanted to access these.  By having all dbf classes inherit
from a common ancestor, I wind up with only 1 of each of these
methods, and all my descendent classes point back at them.

So, my class RECORD contains all of the typical stuff:

create class record

protected:
   var    Indexes, IsOpen, IndexOrd, Relations, RecNo
export:
   var    IsRLocked READONLY
   var    IsFLocked READONLY
   var    Name, Shared, RO, Driver

   method Init
   method Open, Close, Seek, AddIndex, RecLock
   method FileLock, SetOrder
   method GoTo, read, write, AddRelate
   method SetFocus, SetCur, Skip, Unlock, Commit
   method Append, Replace

   message Eof     method fileEnd
   message Bof     method fileBeg
   message LastRec method NumRecs
   message Delete  method RecDel
end class

Problem:
In the RECORD class, how can I assign values to instance variables
(scatter/gather style) when I don't know the instance variables names?  [I
don't know their names because they've been dynamically created during
instantiation of the metaclass.]

Solution:
Let me start with examples:

 method read()
    local nJ
    local nOffSet := record():nSlotCount

    for nJ := 1 to ( ::Name )->( fcount() )
        self[ nJ + nOffSet] := ( ::Name )->( fieldget( nJ ) )
    next
 return self

Likewise,

 method write()
    local nJ
    local nOffSet := record():nSlotCount

    for nJ := 1 to ( ::Name )->( fcount() )
        ( ::Name )->( fieldput( nJ, self[ nJ + nOffSet] ) )
    next

    ::commit()
 return self

As you can see, I'm referencing the instance variables as offsets within
an array.  nSlotCount is an instance variable supplied by Class(y) [just
look with the object inspector].

The idea here is that my fieldname-instance variables are NOT the first
ivars in the class.  Class variables precede them.  In my first meddling in
this area I did the boneheaded thing of hard coding the actual numeric
offset (it happened to be 11 in my case).  By asking the RECORD class
how many slots it handed to my dynamically built class, I know where
my instance variables begin.

According to Anton, Class(y)/Clipper is not alone amongst OO languages
in internally storing objects as arrays.

It IS true that one could argue that there's a sense of violating
encapsulation in that the above examples demand knowledge of
structure which otherwise should be beyond my knowledge.

But who ever said I was a purist anyway?


Question:
If you had a single class - dbf for example, and you instantiated objects for
customer, invoice, etc., you wouldn't have duplicated methods, correct? So
memory usage would be essentially the same, right?

You mean one class which does not have an instance variable for each
field, right?  If so, then I think the pluses and minuses are both
apparent.  Memory savings would be real because only one class
definition would have occurred, but the interface to the data would
change, which you may or may not consider a minus.

Question:
but couldn't you also do that in compile time by creating an array - either of
field vars or, as some have done, of field objects?

Do you mean hard code an array specifying fields?  You must not mean
that, because the answer is so obviously yes, you can.  You'll have to hit
my thick head with a brick so I understand the question.

By the way, my example was NOT meant to serve as an argument
against field objects or any other alternate design.  It was intended only
as a real world example of a metaclass.

Question:
So it appears that metaclasses are most useful for shifting class creation to
runtime from compile time.  Are there any other advantages I'm missing?

Yes, exactly.  It may well be that 3rd party developers will make the
most extensive use of this kind of thing.  Too early to tell.  But part of
the dynamic nature of Class(y) is that since you can inherit directly from
the CLASS class, you can even extend Class(y) itself.

Question:
Since there is at least one more class involved, there must be some memory and
performance hit, eh? Is it significant?

Each class created adds to the symbol table, so depending on how near
the edge you're playing things, that's an issue.

Class creation does take longer than instantiation, which is quick.  So,
creating separate classes for each dbf will take longer than one class
that fits all.  But in a real world application that I have installed at
customer sites, the performance is an issue which we'd all have to
measure.  My users are unaware of any issue.

In order to protect against unnecessary class creation, I chose to do the
following:

// declare file-wide static
static oDbfCls

... other code

// only create class if it hasn't been done already
if oDbfCls == NIL
      oDbfCls := dbfclass():new( <cFileName> )
endif

// instantiate it
oDbfObj := oDbfCls:new()

The above works fine in my scenario.  Use of a file-wide static works
because my datafile classes are created during instantiation of my
MODULE class, which knows its own data, input screens, and carries
mechanisms for editing, etc.