this implementation uses the abstract factory pattern to avoid one of the most difficult code maintenance issues. this is the problem that arises whenever we need to change either the class name, or the source library (or both) from which we want to instantiate an object, or simply need to make changes to an existing class definition.

to change a name is fraught with peril and the only way to do it is to search the source code for all occurrences of the item that we want to change and replace it with the new one. this works just fine providing that we don’t miss one, or mistype either the name (or library, or both!) the more occurrences we have to fix the more likely it is that a mistake will be made – even using tools like “code references”.

even routine maintenance and enhancements can cause problems, especially when a team of developers is involved because a class library cannot be shared. so there is always a danger that changes made by one person get overwritten by changes in a different class made by another.

the implementation described here addresses both of these issues.

what is a factory?

a factory class is basically one whose function is to instantiate other classes. it is an example of the more general ‘abstract factory’ design pattern, which is defined in “design patterns – elements of reusable object-oriented software” (gamma, helm, johnson & vlissides, addison wesley 1997) as:

provide an interface for creating families of related or dependent objects without specifying their concrete classes

what are the components of the factory?

the factory pattern requires a minimum of two components and, usually three (see figure 3).

  • the client – the object that requires an instance of a class. this is where, in conventional code, the createobject() (or newobject()) function call would normally be located
  • the factory - this is usually instantiated as a stand-alone object at application startup, although it can also be implemented as a method on an application object.
  • the metadata - this, is what makes this implementation so useful in my opinion, and why vfp is such an ideal tool in which to implement this particular pattern

figure 1: abstract factory components

when should i use a class factory?

the short answer is always! there is really no reason not to use a factory to instantiate objects and the only real question is whether it should be implemented as a global object in its own right, or as a method on some other (i.e. application) object. my personal preference is to create the factory as a separate object because i always use metadata to drive it, and it helps keep things tidy if the factory can run in its own private datasession.

the key thing to remember is that the role of the metadata in the abstract factory is to separate the functionality from the information that is used to drive it. in this case we are concerned with instantiating objects which would normally be handled explicitly by using code like this:

loobject = createobject( ‘myclass’ )

or

loobject = newobject(  ‘myclass’, myclasslibrary’ )

as noted above, problems arise with this if either the class name, or the source library changes, or even if we want to test a new version of a class because we must either change the original class definition, or change the calling code. either way we have to update, recompile and re-distribute the application. the solution is that, instead of hard coding the instantiation directly into the application, we hand the task off to another object.

in order to do that we need to associate a non-definitive “key” (that will be used in the code) with the actual class, and library names that can be looked up at run time to instantiate the correct class. since the interpretation of the key name is handled inside the factory, there is never any need to amend the source code when changes to either class, or class library, are made. all that is needed is to change the metadata.

implementation

the metadata table for the factory class is shown in table 1. notice that the metadata, in addition to the key, class and library names includes a memo field which we use to define default values for specific properties by listing them as attribute/value pairs. there is also a logical field which allows us to disable particular classes when necessary (more on that later).

table 1: factory class metatdata

the factory class itself is defined programmatically using the session base class so that it always runs in its own private datasession. this obviates any possibility that the tables it uses will be affected by the application itself. the interface is very simple as shown in table 2:

table 2: factory class interface

in practice we define two identical tables. the first is named “classes.dbf” and is the production version of the table, the second is named “wipclasses.dbf” (for work-in-progress). the code in the factory always checks for the key in the wipclasses table first so that we don’t actually have to modify things on the production side (classes.dbf) until we are sure that the new version is correct.  the consequence is that new classes can be tested by the developer, actually in the exe, without re-compiling the code. all that is needed is an active entry in wipclasses.dbf to override the current ‘production’ version.

another consequence is that, in a team environment, each developer can have their own local wipclasses table, and a read-only copy of the main classes.dbf. that way when someone is developing a class they don’t affect everyone else’s code while they are de-bugging their changes. once a new class, or sub-class, is ready for production, all that needs to be done is to update the appropriate class library and (if needed) modify the entry classes table and the application begins using the new class immediately.

the factory class is defined in code as a sub-class of the session base class. this, as noted above, means that the factory can be instantiated in the application environment as a global object that maintains its own private datasession. the class includes a property (lwiptable) this serves as a flag that is set when the class is instantiated so that we don't have to keep testing for the presence of the wip table. the init() method is responsible for setting up the metadata, and setting this flag.

all of the real work is handled in the exposed new() method. it accepts a key name, and up to five additional parameters which (if present) are passed directly to the init() of the object. if the object is instantiated correctly, a reference to the new object is returned to the client, otherwise a null value is handed back.

note that the class determines, from the extension of the specified class library, whether to use createobject() if the library has been explicitly declared, or newobject() if not. if the object is instantiated successfully, and there are settings defined as defaults in the properties column, then the factory will attempt to set those values as well (see the getproperties() and str2exp() methods) – this is, of course, in addition to any parameters that may be passed and handled by the class code directly.

so to create an instance of an object defined in the application source code all that has to be done is for the application to have access to the factory object and to call its new() method with the key name. the return value will either be an object reference or null, and the client handles both possibilities:

try
  loobject = ofactory.new( ‘required_object_keyname’ )
  if isnull( loobject )
    error “unable to instantiate” + required_object_keyname )
  endif
catch to loerr
  messagebox( loerr.message + “ in ” + loerr.procedure, 16, “factory error” )
endtry

you may be wondering why this does not create a dangling object reference. the reason is simply that the object is created as local inside the factory.new() method. so when the method completes, the local reference goes out of scope and is released, leaving the returned reference as the only one in existence.

summary

the implementation of the factory pattern illustrated here offers two major benefits. first, and most obviously, it avoids the issues that arise when class and library names are embedded directly in code. second, it allows developers to work on, and test in a production environment, code that is still ‘work-in-progress’ without breaking anyone else’s version of the application.

the code for our factory class is here:

******************************************************************************************
*** program: factory.prg
*** written by andy kramek & marcia g. akins
*** abstract: abstract factory responsible for object instantiation
*** parameters: keyword from classes table to look up class info and up to 4 optional parameters
*** compiler.: visual foxpro 09.00.0000.3504 for windows
*****************************************************************************************
define class factory as session
*** give it a private data session so we can set exact on
*** so we can accurately use a seek into the classes table
datasession = 2
lwiptable = .f.
******************************************************
function init()
******************************************************
  *** this is a private data session so just force exact on
  set exact on
  *** open the classes table 
  if not used( 'classes' )
    use classes again in 0
  endif
  *** open wipclasses if we have it available, and set property
  if not used( 'wipclasses' )
    if file( 'wipclasses.dbf' )
      use wipclasses again in 0
      this.lwiptable = .t.
    endif
  else
    this.lwiptable = .t.
  endif
endfunc
 
****************************************************************************
function new( tckey, tuparam1, tuparam2, tuparam3, tuparam4, tuparam5 )
*****************************************************************************
local lclibtype, lccommand, lnparm, lnparmcount, loobject, llfound, lckey, lnselect, llcreate, lcproperties
local lclibrary, lcclassname
 
*** make sure we got a keyword
if empty( tckey )
  return .null.
endif
 
*** get the class information
lckey = upper( alltrim( tckey ) )
 
*** check to see if the developers are using a local classes table
*** to test work in progress. if there is one, use the information
*** from the local table  if it is there. only check the application
*** classes table if the keyword can't be found in the local one
lnselect = select()
if this.lwiptable
  select wipclasses
  llfound = seek( lckey, 'wipclasses', 'ckey' ) and wipclasses.lactive
endif
   
*** now check the application classes table if
*** we need to
if not llfound
  select classes
  llfound = seek( lckey, 'classes', 'ckey' ) and classes.lactive
endif
   
if not llfound
  *** make sure keyword was found in classes table
  return .null.
endif
 
*** save pertinent info and force to upper case
lcproperties = alltrim( properties )   
lclibrary = upper( alltrim( clibrary ) )
lcclassname = upper( alltrim( cclassname ) )
 
*** is this class in a vcx or a prg?
lclibtype = this.chklibtype( lclibrary )
   
*** make sure our class library has an extension
*** just in case one was not specified in classes.dbf
if empty( lclibtype )
  return .null.
else
  lclibrary = forceext( lclibrary, lclibtype )
endif
 
*** now, see if our prg or vcx has been set
if lclibtype = 'prg'
  llcreate = ( lclibrary $ set( 'procedure' ) ) or ;
    ( forceext( lclibrary, 'fxp' ) $ set( 'procedure' ) ) 
else
  llcreate = lclibrary $ set( 'classlib' )
endif
 
if llcreate
  lccommand = 'createobject( "' + lcclassname + '"'
else
  lccommand = 'newobject( "' + lcclassname + '", "' + lclibrary + '"'
endif
 
*** now tack the parameters on to the end of the command
*** if any were passed 
lnparmcount = pcount() - 1
if  lnparmcount > 0
  *** only add the third parameter if we are using newobject
  if not llcreate
    lccommand = lccommand + ', ""'
  endif
  for lnparm = 1 to  lnparmcount
    lccommand = lccommand + ', tuparam' + transform( lnparm )
  endfor
endif
lccommand = lccommand + ' )'
 
*** go ahead and instantiate the object
loobject = &lccommand
 
*** now see if we have some properties to set
if not empty( lcproperties )
  *** first make sure we have an object
  if vartype( loobject ) = 'o'
    this.getproperties( loobject, lcproperties )
  endif
endif
 
select ( lnselect )   
 
return loobject
         
endfunc
 
*****************************************************************************
protected function getproperties( toobject, tcproperties )
*****************************************************************************
local lntotal, laprops[ 1 ], lncnt, lcpropname, lcpropvalue, lctype, loitem, luvalue
 
*** get all the attribute/value pairs in the memo field into a single array element
lntotal = alines( laprops, tcproperties ) 
*** and process each pair in the array
for lncnt = 1 to lntotal
  *** get the name of the property
  lcpropname = alltrim( getwordnum( laprops[ lncnt ], 1, '=' ) )
  lcpropvalue = alltrim( getwordnum( laprops[ lncnt ], 2, '=' ) )
  *** make sure it is a property on the object
  if pemstatus( toobject, lcpropname, 5 )
    *** see if we can get its data type
    loitem = 'toobject.' + lcpropname
    lctype = type( loitem )
    *** now get back the value in the appropriate data type
    luvalue = this.str2exp( lcpropvalue, lctype )
    &loitem = luvalue   
  endif
endfor
 
********************************************************************************
protected function str2exp( tcexp, tctype )
********************************************************************************
local lcexp, luretval, lctype, lcstr
 
*** verify parameters
if vartype( tcexp ) # 'c'
  assert .f. message transform( tcexp ) + ' is not a character expression and you must pass a character expression to str2exp!'
  return tcexp
endif
if empty( tctype )
  assert .f. message 'you must pass a data type to str2exp!'
  return tcexp
endif
 
*** if no type passed -- map to expression type
lctype = upper( alltrim( tctype ) )
*** remove any null characters, and leading/trailing spaces
lcexp = chrtran( alltrim( tcexp ), chr( 0 ), '' )
*** convert from character to the correct type
do case
  *** integers
  case inlist( lctype, 'i', 'n' ) and int( val( lcexp ) ) == val( lcexp )
    luretval = int( val( lcexp ) )
  *** other numeric
  case inlist( lctype, 'n', 'b' )
    luretval = val( lcexp )
  *** currency
  case lctype = "y"
    luretval = ntom( val( lcexp ))
  *** character or memo
  case inlist( lctype, 'c', 'm' )
    *** remove delimiting marks if present.
    if inlist( left(lcexp,1), chr(91), chr(34), chr(39))
      *** we begin with a delimiter
      lcexp = substr( lcexp, 2 )
      *** so we should end with a delimiter
      if inlist( right(lcexp,1), chr(93), chr(34), chr(39))
        lcexp = left( lcexp, len( lcexp )- 1 )
      endif
    endif
    luretval = lcexp
  *** logical
  case lctype = 'l'
    luretval = iif( !empty( chrtran( lcexp, 'ff0.', "" ) ), .t., .f.)
  *** date
  case lctype = 'd' && date
    *** check for separators in the string
    if chrtran( lcexp, "/.-", "" ) == lcexp
      *** we are in yyyymmdd format
      lcstr = left( lcexp, 4) + "," + substr( lcexp, 5, 2 ) + "," + right( lcexp, 2)
      luretval = date( &lcstr )
    else
      *** we are in dtoc() format
      luretval = ctod( lcexp )
    endif
  *** datetime
  case lctype = 't' && datetime
    *** check for date separators in the string
    if chrtran( lcexp, "/.-", "" ) == lcexp
      *** no separators so we have something other than ttoc() format
      if len( lcexp ) > 8
        *** this one must be in yyyymmddhhmmss format
        *** so get the date part first
        lcstr = left( lcexp, 4) + "," + substr( lcexp, 5, 2 ) + "," + substr( lcexp, 7, 2)
        *** and convert to the correct date string format
        lcstr = dtoc( date( &lcstr ))
        *** now tack on the hours part
        lcstr = lcstr + " " + substr( lcexp, 9, 2 )
        *** minutes
        if len( lcexp ) > 10
          lcstr = lcstr + ":" + substr( lcexp, 11, 2 )
        else
          lcstr = lcstr + ":00"
        endif
        *** seconds
        if len( lcexp ) > 12
          lcstr = lcstr + ":" + substr( lcexp, 13, 2 )
        else
          lcstr = lcstr + ":00"
        endif
        luretval = ctot( lcstr )
      else 
        *** this must be a date in yyyymmdd format which we want to force to datetime format
        lcstr = left( lcexp, 4) + "," + substr( lcexp, 5, 2 ) + "," + right( lcexp, 2)
        luretval = dtot( date( &lcstr ))
      endif
    else
      *** we are already in ttoc() format
      luretval = ctot( lcexp )
    endif
 
  otherwise
    *** we have an invalid combination of value and data type
    messagebox( "cannot convert " + lcexp + " to data type " + tctype, 16, "conversion failed " )
    luretval = lcexp
endcase
*** return value as data type
return luretval
 
*****************************************************************************
protected function chklibtype( tclibrary )
*****************************************************************************
local lclibtype
 
*** checks for file extension on library name, and
*** figures out what it should be if not supplied
lclibtype = upper( justext( tclibrary ) )
 
if not empty( lclibtype )
  lclibtype = iif( file( tclibrary ), lclibtype, '' )
else
  *** see if we have a vcx here
  lclibtype = iif( file( forceext( tclibrary,'vcx' ) ), 'vcx', '' )
  if empty ( lclibtype )
    lclibtype = iif( file( forceext( tclibrary,'prg' ) ), 'prg', '' )
  endif
endif   
   
return lclibtype
 
enddefine

Leave a Reply

Your email address will not be published. Required fields are marked *