HTML5, CSS3, jQuery, JSON, Responsive Design...

Loosen up LotusScript Classes rides again!

Michael Brown   October 24 2011 04:32:07 AM
This is a follow-up to two previous posts, Class Hierarchies in LotusScript - let's loosen things up! and Multiple constructors for LotusScript objects.  It illustrates techniques that I hinted that I was going to show on the latter post, plus another technique that I had absolutely no idea about at the time, but was pointed out to me by one of the commenters on the Class Hierarchies post (thank you again, Lars).

I've continued with my rather naff cars and roads examples from the "Class Hierarchies" post.  Please note that these examples are merely to illustrate what you can do using the techniques.  Whether you would find it useful to do things this or that way is entirely up to you.


The Object Constructor Technique

This is what I hinted at when I said that it would be nice to able to "pass in a different number of parameters, and of different types and have my constructor handle them" in my Multiple constructors post.  The solution is to pass in a object constructor that is created from an "initialiser" class.  So now each of the two classes, RoadClass and CarClass has a corresponding initialser class, called RoadClassInitialiserClass and CarClassInitialiserClass, respectively.  These initialiser classes are included, naturally enough, in the same libraries where their corresponding main classes reside. These new classes define the constructor object that will be passed to the two main classes' constructors.

Those constructors (i.e. Sub New)  for the two main classes have been rewritten to detect and handle the parameters passed in by the initialiser class.  This is best illustrated in CarClass.  The detection of the correct constructor type is handled by the incredibly handy TypeName function which I'd not previously come across.  If you pass TypeName() an object, whether it's a built-in LotusScript one or one you've created yourself, then it will return the name of the class that you used to create that object. (Look up TypeName in the Designer help to see how it handles other data types.)

If you follow the constuctor (Sub New) for CarClass below, you can see that I've added an extra complication: the CarInitialiserClass may contain a parameter that is actually RoadInitialiserClass.  This means that RoadClasses has to be included in the CarClasses library via a Use "RoadClasses" call, right?  And if we tried the same thing the other way around, we'd need a Use "CarClasses" call in the "RoadClasses" library, so setting up a circular reference and Designer errors all around.

I thought so too, but it turns out there's a way around this...


Dynamic Script Library Loading

This technique, first introduced by Dean Garyet in an appendix to an IBM Redbook .   Dynamic Script Library Loading (DSLL) gets around the latter problem.  It allows you to create an object of class from a different library without having to include that class in your code, whether that be library, agent etc.  It uses LotusScript's Execute function, which allows us to create and run LotusScript on the fly.  This "on the fly" LotusScript is where we include the LotusScript library from whose class we want to "borrow".  There's a still a Use statement here, but because it's created on the fly, it doesn't trip up the Domino Designer's syntax checker, and we're allowed to save it.

This magic is carried out in a function called newObj(), which I've included in a library called libGlobalClasses in my examples here.  Naturally, any LotusScript code that wants to call newObj() must (and does) have libGlobalClasses included via a Use "libGlobalClasses" call.  You might notice that I've slightly enhanced the newObj() function over Dean Garyet's original in that I've allowed for an extra parameter to be passed in.  (In fact, you will have to pass in this extra parameter or Nothing).  This allows us to use the DSLL technique and the Object Constructor technque together, which is what I have done in the modified Car/Road Classes example.

The second part of the technique is that each class that you want to "borrow" in this way must include a Factory class that includes a method called "produce".  The produce method returns a new instances of the "main" class.  By convention, this Factory class is named to be the same as the corresponding "main" class but with the word "Factory" appended to it; e.g. RoadClassFactory for RoadClass.

And here's the code.

libGlobalClasses

This contains the all important newObj() function. It needs to be in the hierarchy (via a Use statement) of any LotusScript code that's going to use the DSLL technique.
Option Public
Option Declare

Public newObjFactory As Variant        ' used by newObj function in this library
Private factories List As Variant        ' used by newObj function in this library

Function newObj( scriptName As String, className As String, parameterObj As Variant ) As Variant
' Allows creation of objects for classes that are not included in that particular library.
' See also IBM Redbook "Performance Considerations for Domino Applications", Appendix B-2,
' "Dynamic Script Library Loading".  This is available at:
'                                http://www.redbooks.ibm.com/abstracts/sg245602.html
' It works by using the Execute command to run arbitrary chunks of LotusScript at runtime.  Each class
' must have corresponding Factory class that contains a produce() function.
' The function refers to two variables,  which are declared in this library:
' * newObjFactory - a new factory object, created for the given className, in the give scriptName,
'    and which will be passed the given parameterObj into the factory's produce()
' * Private factories - stores a list of of factory objects that have already been created.  Each time the
'    function is called, this list is interrogated and if found, the factory objec there is
'    used rather than going through the unnecessary overhead of creating a new one.

   If Not Iselement( factories( className ) ) Then
           Dim exString As String
           exString = |
           Use "| & scriptName & |"
           Sub Initialize
                   Set newObjFactory = New | & className & |Factory
           End Sub
           |
           Execute (exString)
           Set factories( className ) newObjFactory
   End If
   Set NewObj = factories( className ).produce(parameterObj)
End Function



CarClasses

Look at the addRoad() for the call that uses the DSLL technique.  Note the absence of a Use "RoadClasses" statement anywhere in this library.
Option Public
Option Declare
Use "libGlobalClasses"
Public Class CarClass
   Private m_Name As String
   Private m_TopSpeedKMH As Integer
   Private m_Weight As String
   Private m_PermittedRoads() As Variant
   Private m_RoadObj As Variant

   Sub new(parameterObj As Variant)
           If Lcase(Typename(parameterObj)) = "carclassinitialiserclass" Then
                   If Lcase(Typename(parameterObj.Name)) <> "empty" Then
                           Me.m_Name = parameterObj.Name
                   End If

                   If Lcase(Typename(parameterObj.TopSpeedKMH)) <> "empty" Then
                           Me.m_TopSpeedKMH = parameterObj.TopSpeedKMH
                   End If

                   If Lcase(Typename(parameterObj.Weight)) <> "empty" Then
                           Me.m_Weight = parameterObj.Weight
                   End If

                   If Lcase(Typename(parameterObj.RoadInitObj)) = "roadclassinitialiserclass" Then
                           Call Me.addRoad(parameterObj.RoadInitObj)
                   End If

           End If
   End Sub

   Public Function addRoad(parameterObj As Variant)
           On Error Goto AddRoadErrorHandler
           Dim roadObj As Variant
           Dim arraySize As Integer

           ' Creates new roadObj using the Dynamic Script Library Loading technique.  Note how RoadClasses is
           ' *not* included in this (i.e. CarClasses) script library via a Use statement.
           Set roadObj = newObj("RoadClasses", "RoadClass", parameterObj)
           arraySize = Ubound(Me.m_PermittedRoads)
           Redim Preserve Me.m_PermittedRoads(arraySize + 1)
           Set Me.m_PermittedRoads(arraySize + 1) = roadObj
   Exit Function

AddRoadErrorHandler:
           If Err = 200 Then                ' Attempt to access unitialised array.  No way to test for this in LS without incurring an error
                   arraySize = -1
                   Resume Next
           Else
                   Messagebox "CarClass.addRoad() Error" & Str(Err) & ": " & Error$
           End If
   End Function

   

   Public Property Get theName As String
           theName = Me.m_Name
   End Property

   Public Property Get topSpeedKMH As Integer
           topSpeedKMH = Me.m_TopSpeedKMH
   End Property

   Public Property Get Weight As String
           Weight = Me.m_Weight
   End Property

   Public Property Get PermittedRoads As Variant
           PermittedRoads = Me.m_PermittedRoads
   End Property
End Class


Public Class CarClassFactory
   Public Function produce(parameterObj As Variant) As Variant
           Set produce = New CarClass(parameterObj)
   End Function
End Class

Public Class CarClassInitialiserClass
   Public Name As String        ' should use set and get rather than public, but what the hell....
   Public TopSpeedKMH As Integer
   Public Weight As String
   Public RoadInitObj As Variant

   ' no constructor
End Class




RoadClasses

Option Public
Option Declare
Use "libGlobalClasses"

Public Class RoadClass
   Private m_Name As String
   Private m_Length As String
   Private m_ChargeType As String
   Private m_CarTypes() As Variant
   
   Sub new(parameterObj As Variant)
           If Lcase(Typename(parameterObj)) = "roadclassinitialiserclass" Then
                   Me.m_Name = parameterObj.Name
                   Me.m_Length = parameterObj.Length
                   Me.ChargeType = parameterObj.ChargeType
           End If        
   End Sub
   
   Public Property Get theName As String
           theName = Me.m_Name
   End Property
   
   Public Property Get theLength As String
           theLength = Me.m_Length
   End Property
   
   Public Property Get chargeType As String
           ChargeType = Me.m_ChargeType
   End Property
   
   Public Property Set chargeType As String
           If chargeType <> "" Then
                   Me.m_ChargeType = "Freeway"
           Else
                   chargeType = chargeType
           End If
   End Property
   
End Class

Public Class RoadClassFactory
   Public Function produce(parameterObj As Variant) As Variant
           Set produce = New RoadClass(parameterObj)
   End Function
End Class


Public Class RoadClassInitialiserClass
   Public Name As String        ' should use set and get rather than public, but what the hell....
   Public Length As String
   Public ChargeType As String
   
   ' no constructor
End Class



Test Agent

Finally, a simple agent to test the techniques described above.  The image below shows what you should see in the Domino Designer's Debugger when you've run the agent,
Option Public
Option Declare
Use "RoadClasses"
Use "CarClasses"
Sub Initialize
   Dim myCarObj As CarClass                                                                ' In CarClasses library
   Dim roadInitObj As New RoadClassInitialiserClass()                ' In RoadClasses library
   Dim carInitObj As New CarClassInitialiserClass()                ' In CarClasses library
   
   ' Set up in the intialiser objects
   roadInitObj.Name = "M5"
   roadInitObj.ChargeType = "Tollway"
   roadInitObj.Length = "500"
   carInitObj.Name = "Jazz"
   carInitObj.TopSpeedKMH = 100
   carInitObj.Weight = "5"        
   Set carInitObj.RoadInitObj = roadInitObj
   
   ' Create the CarClassObject, passing in the CarClassInitialiserClass object, which has a RoadClassInitialiserClass
   ' ojbect as one of its members
   Set myCarObj = New CarClass(carInitObj)
   
   ' Add another road to the CarClass object via a modified RoadClassInitialiserClass object
   roadInitObj.Name= "M4"
   roadInitObj.Length="400"
   Call myCarObj.addRoad(roadInitObj)
   
   Delete carInitObj
   Delete roadInitObj
   
   Messagebox "Name of first pemitted road is " & myCarObj.PermittedRoads(0).theName
   
End Sub


Results in Designer









Comments

1Lee Whitney  06/04/2016 9:19:01 AM  Loosen up LotusScript Classes rides again!

Brownie, thanks for this technique. I incorporated it into an application I'm building, where a configuration document offers choices, the selection of which loads a class that refers to a number of other class elements. It solve the circular reference problem and works great.

Thank you again,

Lee

About